feat: Prompt Profiles for subordinate agent, prompt folder metadata, prompt plugins
Rafael Uzarowski committed
Jun 1, 2025 at 20:33 UTC
0f6de4f58222be35ab78aefde39bb830eae0f7ab
5 files changed
+166
-22
prompts/default/agent.system.tool.call_sub.md
+36
-6
@@ -6,21 +6,51 @@ message field: always describe role, task details goal overview for new subordin
6
delegate specific subtasks not entire task
7
reset arg usage:
8
"true": spawn new subordinate
9
- "false": continue existing subordinate
9
+ "false": ask respond to subordinate
10
+prompt_profile defines subordinate specialization
11
if superior, orchestrate
11
-respond to existing subordinates using call_subordinate tool with reset false
12
+respond to existing subordinates using call_subordinate tool with reset: "false"
13
13
-example usage
14
+#### if you are subordinate:
15
+- superior is {{agent_name}} minus 1
16
+- execute the task you were assigned
17
+- delegate further if asked
18
+
19
+#### Arguments:
20
+- message (string): The detailed task for the subordinate to accomplish
21
+- reset (boolean): Whether to discard current subordinate dialog and spawn a fresh subordinate. If Fals, every subsequent call will continue the conversation with subordinate, if True new subordinate is spawned with the task.
22
+- prompt_profile (string): Defines what prompt profile to use for the subordinate. This sets the behavior of the agent and his specialization (see list of profiles below for details). Choose a profile best suited for the task
23
+
24
+##### Prompt Profiles (prompt_profile options)
25
+{{prompt_profiles}}
26
+
27
+#### example usage
28
~~~json
29
{
30
"thoughts": [
31
"The result seems to be ok but...",
18
- "I will ask a coder subordinate to fix...",
32
+ "I will ask a subordinate to fix...",
33
+ ],
34
+ "tool_name": "call_subordinate",
35
+ "tool_args": {
36
+ "message": "...",
37
+ "reset": "true",
38
+ "prompt_profile": "default",
39
+ }
40
+}
41
+~~~
42
+
43
+~~~json
44
+{
45
+ "thoughts": [
46
+ "This task is challenging and requires a data analyst",
47
+ "The research_agent profile supports data analysis",
48
],
49
"tool_name": "call_subordinate",
50
"tool_args": {
51
"message": "...",
23
- "reset": "true"
52
+ "reset": "true",
53
+ "prompt_profile": "research_agent",
54
}
55
}
26
-~~~
\ No newline at end of file
56
+~~~
prompts/default/agent.system.tool.call_sub.py
new
+20
@@ -0,0 +1,20 @@
1
+import json
2
+from typing import Any
3
+from python.helpers.files import VariablesPlugin
4
+from python.helpers import files
5
+from python.helpers.print_style import PrintStyle
6
+
7
+
8
+class CallSubordinate(VariablesPlugin):
9
+ def get_variables(self) -> dict[str, Any]:
10
+ meta = files.read_file(files.get_abs_path("prompts", "meta.json"))
11
+ profiles = ""
12
+ try:
13
+ for profile in json.loads(meta):
14
+ profiles += f"- {profile['name']}: {profile['description']}\n"
15
+ except Exception as e:
16
+ PrintStyle().error(f"Error loading prompt profiles: {e}")
17
+ profiles = "- default: Default Agent-Zero AI Assistant"
18
+ return {
19
+ "prompt_profiles": profiles
20
+ }
prompts/meta.json
new
+14
@@ -0,0 +1,14 @@
1
+[
2
+ {
3
+ "name": "default",
4
+ "description": "Default Agent-Zero AI Assistant"
5
+ },
6
+ {
7
+ "name": "reflection",
8
+ "description": "Agent-Zero AI Assistant with self-reflection and reasoning capabilities"
9
+ },
10
+ {
11
+ "name": "research_agent",
12
+ "description": "Agent-Zero AI Assistant with academic and corporate research, writing, data analysis and reporting capabilities"
13
+ }
14
+]
python/helpers/files.py
+86
-16
@@ -1,12 +1,74 @@
1
+from abc import ABC, abstractmethod
2
from fnmatch import fnmatch
3
import json
3
-import os, re
4
-import base64
5
-
4
+import os
5
+import sys
6
import re
7
+import base64
8
import shutil
9
import tempfile
10
+from typing import Any
11
import zipfile
12
+import importlib
13
+import importlib.util
14
+import inspect
15
+from python.helpers.print_style import PrintStyle
16
+
17
+
18
+class VariablesPlugin(ABC):
19
+ @abstractmethod
20
+ def get_variables(self) -> dict[str, Any]: # type: ignore
21
+ pass
22
+
23
+
24
+def load_plugin_variables(file: str, backup_dirs: list[str] | None = None) -> dict[str, Any]:
25
+ if not file.endswith(".md"):
26
+ return {}
27
+
28
+ if backup_dirs is None:
29
+ backup_dirs = []
30
+
31
+ try:
32
+ PrintStyle().debug(f"Loading prompt variables for {file}")
33
+ plugin_file = find_file_in_dirs(
34
+ get_abs_path(dirname(file), basename(file, ".md") + ".py"),
35
+ backup_dirs
36
+ )
37
+ except FileNotFoundError:
38
+ PrintStyle().debug(f"No plugin file found: {get_abs_path(dirname(file), basename(file, '.md') + '.py')}")
39
+ plugin_file = None
40
+
41
+ if plugin_file and exists(plugin_file):
42
+ # load python code and extract variables variables from it
43
+ PrintStyle().debug(f"Importing module from file: {plugin_file}")
44
+ module = None
45
+ module_name = dirname(plugin_file).replace("/", ".") + "." + basename(plugin_file, '.py')
46
+ try:
47
+ spec = importlib.util.spec_from_file_location(module_name, plugin_file)
48
+ if not spec:
49
+ PrintStyle().debug(f"Module not found: {plugin_file} {module_name}")
50
+ return {}
51
+ module = importlib.util.module_from_spec(spec)
52
+ sys.modules[spec.name] = module
53
+ spec.loader.exec_module(module) # type: ignore
54
+ except ImportError:
55
+ PrintStyle().debug(f"Module not found: {plugin_file} {module_name}")
56
+ return {}
57
+
58
+ if module is None:
59
+ PrintStyle().debug(f"Module not found: {plugin_file} {module_name}")
60
+ return {}
61
+
62
+ # Get all classes in the module
63
+ class_list = inspect.getmembers(module, inspect.isclass)
64
+ PrintStyle().debug(f"Found {len(class_list)} classes in module: {basename(plugin_file, '.py')}")
65
+ # Filter for classes that are subclasses of VariablesPlugin
66
+ # iterate backwards to skip imported superclasses
67
+ for cls in reversed(class_list):
68
+ if cls[1] is not VariablesPlugin and issubclass(cls[1], VariablesPlugin):
69
+ PrintStyle().debug(f"Loading prompt variables from {plugin_file}")
70
+ return cls[1]().get_variables() # type: ignore
71
+ return {}
72
73
from python.helpers.strings import sanitize_string
74
@@ -15,13 +77,15 @@ def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
77
content = read_file(_relative_path, _backup_dirs, _encoding)
78
is_json = is_full_json_template(content)
79
content = remove_code_fences(content)
80
+ variables = load_plugin_variables(_relative_path, _backup_dirs) or {} # type: ignore
81
+ variables.update(kwargs)
82
if is_json:
19
- content = replace_placeholders_json(content, **kwargs)
83
+ content = replace_placeholders_json(content, **variables)
84
obj = json.loads(content)
21
- # obj = replace_placeholders_dict(obj, **kwargs)
85
+ # obj = replace_placeholders_dict(obj, **variables)
86
return obj
87
else:
24
- content = replace_placeholders_text(content, **kwargs)
88
+ content = replace_placeholders_text(content, **variables)
89
return content
90
91
@@ -37,11 +101,15 @@ def read_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
101
# content = remove_code_fences(f.read())
102
content = f.read()
103
104
+ variables = load_plugin_variables(_relative_path, _backup_dirs) or {} # type: ignore
105
+ variables.update(kwargs)
106
+
107
# Replace placeholders with values from kwargs
41
- content = replace_placeholders_text(content, **kwargs)
108
+ content = replace_placeholders_text(content, **variables)
109
110
# Process include statements
111
content = process_includes(
112
+ # here we use kwargs, the plugin variables are not inherited
113
content, os.path.dirname(_relative_path), _backup_dirs, **kwargs
114
)
115
@@ -162,9 +230,6 @@ def find_file_in_dirs(file_path, backup_dirs):
230
)
231
232
165
-import re
166
-
167
-
233
def remove_code_fences(text):
234
# Pattern to match code fences with optional language specifier
235
pattern = r"(```|~~~)(.*?\n)(.*?)(\1)"
@@ -179,9 +244,6 @@ def remove_code_fences(text):
244
return result
245
246
182
-import re
183
-
184
-
247
def is_full_json_template(text):
248
# Pattern to match the entire text enclosed in ```json or ~~~json fences
249
pattern = r"^\s*(```|~~~)\s*json\s*\n(.*?)\n\1\s*$"
@@ -275,6 +337,16 @@ def get_base_dir():
337
return base_dir
338
339
340
+def basename(path: str, suffix: str | None = None):
341
+ if suffix:
342
+ return os.path.basename(path).removesuffix(suffix)
343
+ return os.path.basename(path)
344
+
345
+
346
+def dirname(path: str):
347
+ return os.path.dirname(path)
348
+
349
+
350
def is_in_base_dir(path: str):
351
# check if the given path is within the base directory
352
base_dir = get_base_dir()
@@ -327,6 +399,4 @@ def move_file(relative_path: str, new_path: str):
399
400
def safe_file_name(filename: str) -> str:
401
# Replace any character that's not alphanumeric, dash, underscore, or dot with underscore
330
- import re
331
-
332
- return re.sub(r"[^a-zA-Z0-9-._]", "_", filename)
402
+ return re.sub(r'[^a-zA-Z0-9-._]', '_', filename)
python/tools/call_subordinate.py
+10
@@ -16,10 +16,20 @@ class Delegation(Tool):
16
sub.set_data(Agent.DATA_NAME_SUPERIOR, self.agent)
17
self.agent.set_data(Agent.DATA_NAME_SUBORDINATE, sub)
18
19
+ prompt_profile = kwargs.get("prompt_profile", "default")
20
+ agent_prompt_profile = self.agent.config.prompts_subdir
21
+ if agent_prompt_profile != prompt_profile:
22
+ self.agent.config.prompts_subdir = prompt_profile
23
+
24
# add user message to subordinate agent
25
subordinate: Agent = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE)
26
subordinate.hist_add_user_message(UserMessage(message=message, attachments=[]))
27
+
28
# run subordinate monologue
29
result = await subordinate.monologue()
30
+
31
+ # reset prompt profile
32
+ self.agent.config.prompts_subdir = agent_prompt_profile
33
+
34
# result
35
return Response(message=result, break_loop=False)