Rafael Uzarowski commited on
Commit
e570a0d
·
unverified ·
1 Parent(s): 1a3a7dd

feat: Prompt Profiles for subordinate agent, prompt folder metadata, prompt plugins

Browse files
prompts/default/agent.system.tool.call_sub.md CHANGED
@@ -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
 
10
  if superior, orchestrate
11
- respond to existing subordinates using call_subordinate tool with reset false
12
 
13
- example usage
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  ~~~json
15
  {
16
  "thoughts": [
17
  "The result seems to be ok but...",
18
- "I will ask a coder subordinate to fix...",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ],
20
  "tool_name": "call_subordinate",
21
  "tool_args": {
22
  "message": "...",
23
- "reset": "true"
 
24
  }
25
  }
26
- ~~~
 
6
  delegate specific subtasks not entire task
7
  reset arg usage:
8
  "true": spawn new subordinate
9
+ "false": ask respond to subordinate
10
+ prompt_profile defines subordinate specialization
11
  if superior, orchestrate
12
+ respond to existing subordinates using call_subordinate tool with reset: "false"
13
 
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...",
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": "...",
52
+ "reset": "true",
53
+ "prompt_profile": "research_agent",
54
  }
55
  }
56
+ ~~~
prompts/default/agent.system.tool.call_sub.py ADDED
@@ -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 ADDED
@@ -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 CHANGED
@@ -1,12 +1,74 @@
 
1
  from fnmatch import fnmatch
2
  import json
3
- import os, re
4
- import base64
5
-
6
  import re
 
7
  import shutil
8
  import tempfile
 
9
  import zipfile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  from python.helpers.strings import sanitize_string
12
 
@@ -15,13 +77,15 @@ def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
15
  content = read_file(_relative_path, _backup_dirs, _encoding)
16
  is_json = is_full_json_template(content)
17
  content = remove_code_fences(content)
 
 
18
  if is_json:
19
- content = replace_placeholders_json(content, **kwargs)
20
  obj = json.loads(content)
21
- # obj = replace_placeholders_dict(obj, **kwargs)
22
  return obj
23
  else:
24
- content = replace_placeholders_text(content, **kwargs)
25
  return content
26
 
27
 
@@ -37,11 +101,15 @@ def read_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
37
  # content = remove_code_fences(f.read())
38
  content = f.read()
39
 
 
 
 
40
  # Replace placeholders with values from kwargs
41
- content = replace_placeholders_text(content, **kwargs)
42
 
43
  # Process include statements
44
  content = process_includes(
 
45
  content, os.path.dirname(_relative_path), _backup_dirs, **kwargs
46
  )
47
 
@@ -162,9 +230,6 @@ def find_file_in_dirs(file_path, backup_dirs):
162
  )
163
 
164
 
165
- import re
166
-
167
-
168
  def remove_code_fences(text):
169
  # Pattern to match code fences with optional language specifier
170
  pattern = r"(```|~~~)(.*?\n)(.*?)(\1)"
@@ -179,9 +244,6 @@ def remove_code_fences(text):
179
  return result
180
 
181
 
182
- import re
183
-
184
-
185
  def is_full_json_template(text):
186
  # Pattern to match the entire text enclosed in ```json or ~~~json fences
187
  pattern = r"^\s*(```|~~~)\s*json\s*\n(.*?)\n\1\s*$"
@@ -275,6 +337,16 @@ def get_base_dir():
275
  return base_dir
276
 
277
 
 
 
 
 
 
 
 
 
 
 
278
  def is_in_base_dir(path: str):
279
  # check if the given path is within the base directory
280
  base_dir = get_base_dir()
@@ -327,6 +399,4 @@ def move_file(relative_path: str, new_path: str):
327
 
328
  def safe_file_name(filename: str) -> str:
329
  # 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)
 
1
+ from abc import ABC, abstractmethod
2
  from fnmatch import fnmatch
3
  import json
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
 
 
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:
83
+ content = replace_placeholders_json(content, **variables)
84
  obj = json.loads(content)
85
+ # obj = replace_placeholders_dict(obj, **variables)
86
  return obj
87
  else:
88
+ content = replace_placeholders_text(content, **variables)
89
  return content
90
 
91
 
 
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
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
 
 
230
  )
231
 
232
 
 
 
 
233
  def remove_code_fences(text):
234
  # Pattern to match code fences with optional language specifier
235
  pattern = r"(```|~~~)(.*?\n)(.*?)(\1)"
 
244
  return result
245
 
246
 
 
 
 
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*$"
 
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()
 
399
 
400
  def safe_file_name(filename: str) -> str:
401
  # Replace any character that's not alphanumeric, dash, underscore, or dot with underscore
402
+ return re.sub(r'[^a-zA-Z0-9-._]', '_', filename)
 
 
python/tools/call_subordinate.py CHANGED
@@ -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
  # add user message to subordinate agent
20
  subordinate: Agent = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE)
21
  subordinate.hist_add_user_message(UserMessage(message=message, attachments=[]))
 
22
  # run subordinate monologue
23
  result = await subordinate.monologue()
 
 
 
 
24
  # result
25
  return Response(message=result, break_loop=False)
 
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)