Spaces:
Paused
Paused
Rafael Uzarowski commited on
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":
|
|
|
|
| 10 |
if superior, orchestrate
|
| 11 |
-
respond to existing subordinates using call_subordinate tool with reset false
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
~~~json
|
| 15 |
{
|
| 16 |
"thoughts": [
|
| 17 |
"The result seems to be ok but...",
|
| 18 |
-
"I will ask a
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 4 |
-
import
|
| 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, **
|
| 20 |
obj = json.loads(content)
|
| 21 |
-
# obj = replace_placeholders_dict(obj, **
|
| 22 |
return obj
|
| 23 |
else:
|
| 24 |
-
content = replace_placeholders_text(content, **
|
| 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, **
|
| 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 |
-
|
| 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)
|