Read file refactor + Fix include path errors (#633)

* refactor: read_file to read_prompt_file * fix: ignore absolute paths in includes

The Master committed Jul 31, 2025 at 16:21 UTC 3eb6d8ff61d4c09eb4a706fe6d8d70479e83c37a
7 files changed +44 -9
agent.py
+1 -1
@@ -506,7 +506,7 @@ class Agent:
506 ): # if agent has custom folder, use it and use default as backup
507 prompt_dir = files.get_abs_path("agents", self.config.profile, "prompts")
508 backup_dir.append(files.get_abs_path("prompts"))
509 - prompt = files.read_file(
509 + prompt = files.read_prompt_file(
510 files.get_abs_path(prompt_dir, file), _backup_dirs=backup_dir, **kwargs
511 )
512 prompt = files.remove_code_fences(prompt)
prompts/agent.system.tool.call_sub.py
+1 -1
@@ -13,7 +13,7 @@ class CallSubordinate(VariablesPlugin):
13 agent_subdirs = files.get_subdirectories("agents", exclude=["_example"])
14 for agent_subdir in agent_subdirs:
15 try:
16 - context = files.read_file(
16 + context = files.read_prompt_file(
17 files.get_abs_path("agents", agent_subdir, "_context.md")
18 )
19 profiles.append({"name": agent_subdir, "context": context})
prompts/agent.system.tools.py
+1 -1
@@ -22,7 +22,7 @@ class CallSubordinate(VariablesPlugin):
22 tools = []
23 for prompt_file in prompt_files:
24 try:
25 - tool = files.read_file(prompt_file)
25 + tool = files.read_prompt_file(prompt_file)
26 tools.append(tool)
27 except Exception as e:
28 PrintStyle().error(f"Error loading tool '{prompt_file}': {e}")
python/extensions/system_prompt/_20_behaviour_prompt.py
+1 -1
@@ -16,7 +16,7 @@ def get_custom_rules_file(agent: Agent):
16 def read_rules(agent: Agent):
17 rules_file = get_custom_rules_file(agent)
18 if files.exists(rules_file):
19 - rules = files.read_file(rules_file)
19 + rules = files.read_prompt_file(rules_file)
20 return agent.read_prompt("agent.system.behaviour.md", rules=rules)
21 else:
22 rules = agent.read_prompt("agent.system.behaviour_default.md")
python/helpers/files.py
+36 -3
@@ -73,7 +73,17 @@ from python.helpers.strings import sanitize_string
73
74
75 def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
76 - content = read_file(_relative_path, _backup_dirs, _encoding)
76 + if _backup_dirs is None:
77 + _backup_dirs = []
78 +
79 + # Try to get the absolute path for the file from the original directory or backup directories
80 + absolute_path = find_file_in_dirs(_relative_path, _backup_dirs)
81 +
82 + # Read the file content
83 + with open(absolute_path, "r", encoding=_encoding) as f:
84 + # content = remove_code_fences(f.read())
85 + content = f.read()
86 +
87 is_json = is_full_json_template(content)
88 content = remove_code_fences(content)
89 variables = load_plugin_variables(_relative_path, _backup_dirs) or {} # type: ignore
@@ -85,10 +95,15 @@ def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
95 return obj
96 else:
97 content = replace_placeholders_text(content, **variables)
98 + # Process include statements
99 + content = process_includes(
100 + # here we use kwargs, the plugin variables are not inherited
101 + content, os.path.dirname(_relative_path), _backup_dirs, **kwargs
102 + )
103 return content
104
105
91 -def read_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
106 +def read_prompt_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
107 if _backup_dirs is None:
108 _backup_dirs = []
109
@@ -115,6 +130,18 @@ def read_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
130 return content
131
132
133 +def read_file(_relative_path, _backup_dirs=None, _encoding="utf-8"):
134 + if _backup_dirs is None:
135 + _backup_dirs = []
136 +
137 + # Try to get the absolute path for the file from the original directory or backup directories
138 + absolute_path = find_file_in_dirs(_relative_path, _backup_dirs)
139 +
140 + # Read the file content
141 + with open(absolute_path, "r", encoding=_encoding) as f:
142 + return f.read()
143 +
144 +
145 def read_file_bin(_relative_path, _backup_dirs=None):
146 # init backup dirs
147 if _backup_dirs is None:
@@ -194,13 +221,16 @@ def process_includes(_content, _base_path, _backup_dirs, **kwargs):
221
222 def replace_include(match):
223 include_path = match.group(1)
224 + # if the path is absolute, do not process it
225 + if os.path.isabs(include_path):
226 + return match.group(0)
227 # First attempt to resolve the include relative to the base path
228 full_include_path = find_file_in_dirs(
229 os.path.join(_base_path, include_path), _backup_dirs
230 )
231
232 # Recursively read the included file content, keeping the original base path
203 - included_content = read_file(full_include_path, _backup_dirs, **kwargs)
233 + included_content = read_prompt_file(full_include_path, _backup_dirs, **kwargs)
234 return included_content
235
236 # Replace all includes with the file content
@@ -219,6 +249,9 @@ def find_file_in_dirs(file_path, backup_dirs):
249
250 # Loop through the backup directories
251 for backup_dir in backup_dirs:
252 + # backup path should be os.path.join(backup_dir, file_path) but that can lead to problems
253 + # with absolute paths in file_path. So we use basename.
254 + # This means that we don't support include paths like "subdir/something.md" from backups
255 backup_path = os.path.join(backup_dir, os.path.basename(file_path))
256 if os.path.isfile(get_abs_path(backup_path)):
257 return get_abs_path(backup_path)
python/tools/behaviour_adjustment.py
+1 -1
@@ -58,7 +58,7 @@ def get_custom_rules_file(agent: Agent):
58 def read_rules(agent: Agent):
59 rules_file = get_custom_rules_file(agent)
60 if files.exists(rules_file):
61 - rules = files.read_file(rules_file)
61 + rules = files.read_prompt_file(rules_file)
62 return agent.read_prompt("agent.system.behaviour.md", rules=rules)
63 else:
64 rules = agent.read_prompt("agent.system.behaviour_default.md")
run_ui.py
+3 -1
@@ -157,8 +157,10 @@ async def serve_index():
157 "version": "unknown",
158 "commit_time": "unknown",
159 }
160 - return files.read_file(
160 + return files.read_prompt_file(
161 "./webui/index.html",
162 + _backup_dirs=[],
163 + _encoding="utf-8",
164 version_no=gitinfo["version"],
165 version_time=gitinfo["commit_time"],
166 )