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)