diff --git a/agents/agent0/_context.md b/agents/agent0/_context.md deleted file mode 100644 index ef48e78e2..000000000 --- a/agents/agent0/_context.md +++ /dev/null @@ -1,4 +0,0 @@ -# Agent 0 -- main agent of the system -- communicates to user and delegates to subordinates -- general purpose assistant, communication skills, formatted output \ No newline at end of file diff --git a/agents/agent0/agent.json b/agents/agent0/agent.json deleted file mode 100644 index 4fa2cb2c1..000000000 --- a/agents/agent0/agent.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Agent 0", - "description": "Main agent of the system communicating directly with the user.", - "context": "" -} diff --git a/agents/agent0/agent.yaml b/agents/agent0/agent.yaml new file mode 100644 index 000000000..aa20770c2 --- /dev/null +++ b/agents/agent0/agent.yaml @@ -0,0 +1,3 @@ +title: Agent 0 +description: Main agent of the system communicating directly with the user. +context: '' diff --git a/agents/default/_context.md b/agents/default/_context.md deleted file mode 100644 index 24a619beb..000000000 --- a/agents/default/_context.md +++ /dev/null @@ -1,3 +0,0 @@ -# Default prompts -- default prompt file templates -- should be inherited and overriden by specialized prompt profiles \ No newline at end of file diff --git a/agents/default/agent.json b/agents/default/agent.json deleted file mode 100644 index c5c905f70..000000000 --- a/agents/default/agent.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Default", - "description": "Default prompt file templates. Should be inherited and overriden by specialized prompt profiles.", - "context": "" -} diff --git a/agents/default/agent.yaml b/agents/default/agent.yaml new file mode 100644 index 000000000..4d3140f2e --- /dev/null +++ b/agents/default/agent.yaml @@ -0,0 +1,4 @@ +title: Default +description: Default prompt file templates. Should be inherited and overriden by specialized + prompt profiles. +context: '' diff --git a/agents/developer/_context.md b/agents/developer/_context.md deleted file mode 100644 index d1ae067e7..000000000 --- a/agents/developer/_context.md +++ /dev/null @@ -1,2 +0,0 @@ -# Developer -- agent specialized in complex software development \ No newline at end of file diff --git a/agents/developer/agent.json b/agents/developer/agent.json deleted file mode 100644 index 8680176e3..000000000 --- a/agents/developer/agent.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Developer", - "description": "Agent specialized in complex software development.", - "context": "Use this agent for software development tasks, including writing code, debugging, refactoring, and architectural design." -} diff --git a/agents/developer/agent.yaml b/agents/developer/agent.yaml new file mode 100644 index 000000000..a5c7c1df6 --- /dev/null +++ b/agents/developer/agent.yaml @@ -0,0 +1,4 @@ +title: Developer +description: Agent specialized in complex software development. +context: Use this agent for software development tasks, including writing code, debugging, + refactoring, and architectural design. diff --git a/agents/hacker/_context.md b/agents/hacker/_context.md deleted file mode 100644 index 548236b72..000000000 --- a/agents/hacker/_context.md +++ /dev/null @@ -1,2 +0,0 @@ -# Hacker -- agent specialized in cyber security and penetration testing \ No newline at end of file diff --git a/agents/hacker/agent.json b/agents/hacker/agent.json deleted file mode 100644 index cde645d79..000000000 --- a/agents/hacker/agent.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Hacker", - "description": "Agent specialized in cyber security and penetration testing.", - "context": "Use this agent for cybersecurity tasks such as penetration testing, vulnerability analysis, and security auditing." -} diff --git a/agents/hacker/agent.yaml b/agents/hacker/agent.yaml new file mode 100644 index 000000000..943eaac11 --- /dev/null +++ b/agents/hacker/agent.yaml @@ -0,0 +1,4 @@ +title: Hacker +description: Agent specialized in cyber security and penetration testing. +context: Use this agent for cybersecurity tasks such as penetration testing, vulnerability + analysis, and security auditing. diff --git a/agents/researcher/_context.md b/agents/researcher/_context.md deleted file mode 100644 index 3953f6cf1..000000000 --- a/agents/researcher/_context.md +++ /dev/null @@ -1,2 +0,0 @@ -# Researcher -- agent specialized in research, data analysis and reporting \ No newline at end of file diff --git a/agents/researcher/agent.json b/agents/researcher/agent.json deleted file mode 100644 index e06a9639b..000000000 --- a/agents/researcher/agent.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Researcher", - "description": "Agent specialized in research, data analysis and reporting.", - "context": "Use this agent for information gathering, data analysis, topic research, and generating comprehensive reports." -} diff --git a/agents/researcher/agent.yaml b/agents/researcher/agent.yaml new file mode 100644 index 000000000..4b4f7df42 --- /dev/null +++ b/agents/researcher/agent.yaml @@ -0,0 +1,4 @@ +title: Researcher +description: Agent specialized in research, data analysis and reporting. +context: Use this agent for information gathering, data analysis, topic research, + and generating comprehensive reports. diff --git a/initialize.py b/initialize.py index 9b48a2684..ea8ea606d 100644 --- a/initialize.py +++ b/initialize.py @@ -142,7 +142,7 @@ def initialize_preload(): def initialize_migration(): from python.helpers import migration, dotenv # run migration - migration.migrate_user_data() + migration.startup_migration() # reload .env as it might have been moved dotenv.load_dotenv() # reload settings to ensure new paths are picked up diff --git a/plugins/example_agent/agents/plugin_example/agent.json b/plugins/example_agent/agents/plugin_example/agent.json deleted file mode 100644 index 4e299f268..000000000 --- a/plugins/example_agent/agents/plugin_example/agent.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Plugin Example Agent", - "description": "This is an example agent profile loaded from the 'example_agent' plugin.", - "context": "You are an example agent provided by the 'example_agent' plugin. Your purpose is to demonstrate how plugins can distribute agent profiles." -} diff --git a/plugins/example_agent/agents/plugin_example/agent.yaml b/plugins/example_agent/agents/plugin_example/agent.yaml new file mode 100644 index 000000000..c624990ce --- /dev/null +++ b/plugins/example_agent/agents/plugin_example/agent.yaml @@ -0,0 +1,4 @@ +title: Plugin Example Agent +description: This is an example agent profile loaded from the 'example_agent' plugin. +context: You are an example agent provided by the 'example_agent' plugin. Your purpose + is to demonstrate how plugins can distribute agent profiles. diff --git a/plugins/example_agent/plugin.json b/plugins/example_agent/plugin.json deleted file mode 100644 index c5c599690..000000000 --- a/plugins/example_agent/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Example Agent", - "description": "Example agent plugin demonstrating the Agent Zero plugin system.", - "version": "1.0.0", - "settings_sections": [], - "per_project_config": true, - "per_agent_config": false -} diff --git a/plugins/example_agent/plugin.yaml b/plugins/example_agent/plugin.yaml new file mode 100644 index 000000000..91e828957 --- /dev/null +++ b/plugins/example_agent/plugin.yaml @@ -0,0 +1,6 @@ +name: Example Agent +description: Example agent plugin demonstrating the Agent Zero plugin system. +version: 1.0.0 +settings_sections: [] +per_project_config: true +per_agent_config: false \ No newline at end of file diff --git a/plugins/memory/config.default.json b/plugins/memory/config.default.json deleted file mode 100644 index 13cc223b9..000000000 --- a/plugins/memory/config.default.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "project_memory_isolation": true, - "memory_recall_enabled": true, - "memory_recall_delayed": false, - "memory_recall_interval": 3, - "memory_recall_history_len": 10000, - "memory_recall_memories_max_search": 12, - "memory_recall_solutions_max_search": 8, - "memory_recall_memories_max_result": 5, - "memory_recall_solutions_max_result": 3, - "memory_recall_similarity_threshold": 0.7, - "memory_recall_query_prep": false, - "memory_recall_post_filter": false, - "memory_memorize_enabled": true, - "memory_memorize_consolidation": true, - "memory_memorize_replace_threshold": 0.9, - "agent_memory_subdir": "default" -} diff --git a/plugins/memory/default_config.yaml b/plugins/memory/default_config.yaml new file mode 100644 index 000000000..ff0562467 --- /dev/null +++ b/plugins/memory/default_config.yaml @@ -0,0 +1,16 @@ +project_memory_isolation: true +memory_recall_enabled: true +memory_recall_delayed: false +memory_recall_interval: 3 +memory_recall_history_len: 10000 +memory_recall_memories_max_search: 12 +memory_recall_solutions_max_search: 8 +memory_recall_memories_max_result: 5 +memory_recall_solutions_max_result: 3 +memory_recall_similarity_threshold: 0.7 +memory_recall_query_prep: false +memory_recall_post_filter: false +memory_memorize_enabled: true +memory_memorize_consolidation: true +memory_memorize_replace_threshold: 0.9 +agent_memory_subdir: default \ No newline at end of file diff --git a/plugins/memory/extensions/python/message_loop_prompts_after/_50_recall_memories.py b/plugins/memory/extensions/python/message_loop_prompts_after/_50_recall_memories.py index e52cf1c70..ab8ec6ac2 100644 --- a/plugins/memory/extensions/python/message_loop_prompts_after/_50_recall_memories.py +++ b/plugins/memory/extensions/python/message_loop_prompts_after/_50_recall_memories.py @@ -25,11 +25,13 @@ class RecallMemories(Extension): async def execute(self, loop_data: LoopData = LoopData(), **kwargs): - set = plugins.get_plugin_config("memory") + set = plugins.get_plugin_config("memory", self.agent) + if not set: + return None # turned off in settings? if not set["memory_recall_enabled"]: - return + return None # every X iterations (or the first one) recall memories if loop_data.iteration % set["memory_recall_interval"] == 0: @@ -63,7 +65,9 @@ class RecallMemories(Extension): del extras["solutions"] - set = plugins.get_plugin_config("memory") + set = plugins.get_plugin_config("memory", self.agent) + if not set: + return None # try: # get system message and chat history for util llm diff --git a/plugins/memory/extensions/python/message_loop_prompts_after/_91_recall_wait.py b/plugins/memory/extensions/python/message_loop_prompts_after/_91_recall_wait.py index 9039fefd0..45965a222 100644 --- a/plugins/memory/extensions/python/message_loop_prompts_after/_91_recall_wait.py +++ b/plugins/memory/extensions/python/message_loop_prompts_after/_91_recall_wait.py @@ -6,7 +6,9 @@ from python.helpers import plugins class RecallWait(Extension): async def execute(self, loop_data: LoopData = LoopData(), **kwargs): - set = plugins.get_plugin_config("memory") + set = plugins.get_plugin_config("memory", self.agent) + if not set: + return None task = self.agent.get_data(DATA_NAME_TASK_MEMORIES) iter = self.agent.get_data(DATA_NAME_ITER_MEMORIES) or 0 diff --git a/plugins/memory/extensions/python/monologue_end/_50_memorize_fragments.py b/plugins/memory/extensions/python/monologue_end/_50_memorize_fragments.py index 5fcf3227e..391cf4104 100644 --- a/plugins/memory/extensions/python/monologue_end/_50_memorize_fragments.py +++ b/plugins/memory/extensions/python/monologue_end/_50_memorize_fragments.py @@ -16,7 +16,9 @@ class MemorizeMemories(Extension): async def execute(self, loop_data: LoopData = LoopData(), **kwargs): # try: - set = plugins.get_plugin_config("memory") + set = plugins.get_plugin_config("memory", self.agent) + if not set: + return None if not set["memory_memorize_enabled"]: return @@ -36,7 +38,9 @@ class MemorizeMemories(Extension): async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs): try: - set = plugins.get_plugin_config("memory") + set = plugins.get_plugin_config("memory", self.agent) + if not set: + return None db = await Memory.get(self.agent) diff --git a/plugins/memory/extensions/python/monologue_end/_51_memorize_solutions.py b/plugins/memory/extensions/python/monologue_end/_51_memorize_solutions.py index 82877d3c9..aeef9ea1c 100644 --- a/plugins/memory/extensions/python/monologue_end/_51_memorize_solutions.py +++ b/plugins/memory/extensions/python/monologue_end/_51_memorize_solutions.py @@ -15,7 +15,9 @@ class MemorizeSolutions(Extension): async def execute(self, loop_data: LoopData = LoopData(), **kwargs): # try: - set = plugins.get_plugin_config("memory") + set = plugins.get_plugin_config("memory", self.agent) + if not set: + return None if not set["memory_memorize_enabled"]: return @@ -34,7 +36,10 @@ class MemorizeSolutions(Extension): async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs): try: - set = plugins.get_plugin_config("memory") + set = plugins.get_plugin_config("memory", self.agent) + if not set: + return None + db = await Memory.get(self.agent) diff --git a/plugins/memory/plugin.json b/plugins/memory/plugin.json deleted file mode 100644 index 4063b918d..000000000 --- a/plugins/memory/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Memory", - "description": "Provides persistent memory capabilities to Agent Zero agents.", - "version": "1.0.0", - "settings_sections": ["agent"], - "per_project_config": true, - "per_agent_config": false -} diff --git a/plugins/memory/plugin.yaml b/plugins/memory/plugin.yaml new file mode 100644 index 000000000..3c43c4a29 --- /dev/null +++ b/plugins/memory/plugin.yaml @@ -0,0 +1,7 @@ +name: Memory +description: Provides persistent memory capabilities to Agent Zero agents. +version: 1.0.0 +settings_sections: + - agent +per_project_config: true +per_agent_config: false \ No newline at end of file diff --git a/python/helpers/files.py b/python/helpers/files.py index 2a21032ba..b2508ca34 100644 --- a/python/helpers/files.py +++ b/python/helpers/files.py @@ -3,19 +3,16 @@ from fnmatch import fnmatch import json from ntpath import isabs import os -import sys import re import base64 import shutil import tempfile from typing import Any, Literal import zipfile -import importlib -import importlib.util -import inspect import glob import mimetypes from simpleeval import simple_eval +from python.helpers import yaml AGENTS_DIR = "agents" PLUGINS_DIR = "plugins" @@ -224,6 +221,12 @@ def read_file_json(relative_path: str, encoding="utf-8"): with open(absolute_path, "r", encoding=encoding) as f: return json.load(f) +def read_file_yaml(relative_path: str, encoding="utf-8"): + absolute_path = get_abs_path(relative_path) + + with open(absolute_path, "r", encoding=encoding) as f: + return yaml.loads(f.read()) + def read_file_bin(relative_path: str): # Try to get the absolute path for the file from the original directory or backup directories absolute_path = get_abs_path(relative_path) diff --git a/python/helpers/migration.py b/python/helpers/migration.py index 498398c15..d523411e7 100644 --- a/python/helpers/migration.py +++ b/python/helpers/migration.py @@ -1,7 +1,15 @@ import os +import json from python.helpers import files +from python.helpers import subagents +from python.helpers import yaml as yaml_helper from python.helpers.print_style import PrintStyle + +def startup_migration() -> None: + migrate_user_data() + convert_agents_json_yaml() + def migrate_user_data() -> None: """ Migrate user data from /tmp and other locations to /usr. @@ -44,6 +52,26 @@ def migrate_user_data() -> None: PrintStyle().print("Migration check complete.") + +def convert_agents_json_yaml() -> None: + for root in subagents.get_agents_roots(): + rel_root = files.deabsolute_path(root) + for subdir in files.get_subdirectories(rel_root): + agent_yaml = os.path.join(rel_root, subdir, "agent.yaml") + if files.exists(agent_yaml): + continue + + agent_json = os.path.join(rel_root, subdir, "agent.json") + if not files.exists(agent_json): + continue + + try: + agent_obj = json.loads(files.read_file(agent_json)) + files.write_file(agent_yaml, yaml_helper.dumps(agent_obj)) + except Exception as e: + PrintStyle.error(f"Failed to convert {agent_json} to YAML", e) + continue + # --- Helper Functions ---------------------------------------------------------- def _move_dir(src: str, dst: str, overwrite: bool = False) -> None: diff --git a/python/helpers/plugins.py b/python/helpers/plugins.py index 36d5e8e9a..7bef84a2f 100644 --- a/python/helpers/plugins.py +++ b/python/helpers/plugins.py @@ -2,9 +2,9 @@ from __future__ import annotations import re, json from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, TypedDict +from typing import Any, Dict, Iterator, List, Literal, Optional, TYPE_CHECKING, TypedDict -from python.helpers import files, print_style +from python.helpers import files, print_style, yaml as yaml_helper from pydantic import BaseModel, Field if TYPE_CHECKING: @@ -22,12 +22,12 @@ class PluginAssetFile(TypedDict): project_name: str agent_profile: str -META_FILE_NAME = "plugin.json" +META_FILE_NAME = "plugin.yaml" CONFIG_FILE_NAME = "config.json" -CONFIG_DEFAULT_FILE_NAME = "config.default.json" +CONFIG_DEFAULT_FILE_NAME = "default_config.yaml" DISABLED_FILE_NAME = ".toggle-0" ENABLED_FILE_NAME = ".toggle-1" -TOGGLE_FILE_PATTERN = "*.toggle-[01]" +TOGGLE_FILE_PATTERN = ".toggle-[01]" class PluginMetadata(BaseModel): @@ -55,11 +55,11 @@ class PluginListItem(BaseModel): toggle_state: ToggleState = "disabled" -def get_plugin_roots() -> List[str]: +def get_plugin_roots(plugin_name:str="") -> List[str]: """Plugin root directories, ordered by priority (user first).""" return [ - files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR), - files.get_abs_path(files.PLUGINS_DIR), + files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name), + files.get_abs_path(files.PLUGINS_DIR, plugin_name), ] @@ -90,8 +90,11 @@ def get_enhanced_plugins_list( try: if not d.is_dir() or d.name.startswith("."): continue + meta_file = str(d / META_FILE_NAME) + if not files.exists(meta_file): + continue meta = PluginMetadata.model_validate( - files.read_file_json(str(d / META_FILE_NAME)) + files.read_file_yaml(meta_file) ) has_main_screen = files.exists(str(d / "webui" / "main.html")) has_config_screen = files.exists(str(d / "webui" / "config.html")) @@ -112,8 +115,9 @@ def get_enhanced_plugins_list( toggle_state=toggle_state, ) ) - except: - pass + except Exception as e: + print_style.PrintStyle.error(f"Failed to load plugin {d.name}: {e}") + continue if custom: load_plugins(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR), True) @@ -127,7 +131,7 @@ def get_plugin_meta(plugin_name: str): if not plugin_dir: return None return PluginMetadata.model_validate( - files.read_file_json(files.get_abs_path(plugin_dir, META_FILE_NAME)) + files.read_file_yaml(files.get_abs_path(plugin_dir, META_FILE_NAME)) ) @@ -192,6 +196,10 @@ def get_enabled_plugins(agent: Agent | None): # every plugin is on by default, unless disabled in usr dir enabled = True + # root plugin paths + plugin_paths = get_plugin_roots(plugin) + + # + agent paths if agent: from python.helpers import subagents @@ -201,46 +209,51 @@ def get_enabled_plugins(agent: Agent | None): plugin, must_exist_completely=True, include_default=False, - include_user=True, + include_user=False, include_plugins=False, include_project=True, ) + plugin_paths = agent_paths + plugin_paths - # go through agent paths in reverse order and determine the state - for agent_path in reversed(agent_paths): - if enabled: - enabled = not files.exists( - files.get_abs_path(agent_path, DISABLED_FILE_NAME) - ) - else: - enabled = files.exists( - files.get_abs_path(agent_path, ENABLED_FILE_NAME) - ) + # go through paths in reverse order and determine the state + enabled = determined_toggle_from_paths(enabled, reversed(plugin_paths)) if enabled: active.append(plugin) return active +def determined_toggle_from_paths(default:bool, paths:Iterator[str]): + enabled = default + for plugin_path in paths: + if enabled: + enabled = not files.exists( + files.get_abs_path(plugin_path, DISABLED_FILE_NAME) + ) + else: + enabled = files.exists( + files.get_abs_path(plugin_path, ENABLED_FILE_NAME) + ) + return enabled def get_toggle_state(plugin_name: str) -> ToggleState: meta = get_plugin_meta(plugin_name) if not meta: return "disabled" if meta.always_enabled: - return "enabled" + return "enabled" - state = "enabled" + # root plugin paths + plugin_paths = get_plugin_roots(plugin_name) + state = "enabled" if determined_toggle_from_paths(True, reversed(plugin_paths)) else "disabled" - # toggles inside of user directory (there should be only one, but let's make it work in any case) - usr_toggles = files.find_existing_paths_by_pattern(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN)) - for toggle in usr_toggles: - if toggle.endswith(ENABLED_FILE_NAME): - state = "enabled" - if toggle.endswith(DISABLED_FILE_NAME): - state = "disabled" + # global toggles + usr_toggles = [ + files.find_existing_paths_by_pattern(files.get_abs_path(files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN)), + files.find_existing_paths_by_pattern(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN)) + ] - # if there are more toggles in project/agent directories, return advanced + # additional toggles in project/agent directories, return advanced if meta.per_agent_config or meta.per_project_config: configs = find_plugin_assets( TOGGLE_FILE_PATTERN, @@ -311,7 +324,9 @@ def get_plugin_config( find_plugin_dir(plugin_name), CONFIG_DEFAULT_FILE_NAME ) if file_path and files.exists(file_path): - return json.loads(files.read_file(file_path)) + return (json.loads if file_path.lower().endswith(".json") else yaml_helper.loads)( + files.read_file(file_path) + ) return None @@ -322,7 +337,7 @@ def save_plugin_config( plugin_name, project_name, agent_profile, CONFIG_FILE_NAME ) if file_path: - files.write_file(file_path, json.dumps(settings)) + files.write_file(file_path, yaml_helper.dumps(settings)) def find_plugin_asset( diff --git a/python/helpers/subagents.py b/python/helpers/subagents.py index 54b3b2cc1..6690d59ad 100644 --- a/python/helpers/subagents.py +++ b/python/helpers/subagents.py @@ -1,4 +1,5 @@ from python.helpers import files +from python.helpers import yaml as yaml_helper from typing import TypedDict, TYPE_CHECKING from pydantic import BaseModel, model_validator import json @@ -88,8 +89,13 @@ def _get_agents_list_from_dir(dir: str, origin: Origin) -> dict[str, SubAgentLis for subdir in subdirs: try: - agent_json = files.read_file(files.get_abs_path(dir, subdir, "agent.json")) - agent_data = SubAgentListItem.model_validate_json(agent_json) + agent_yaml_path = files.get_abs_path(dir, subdir, "agent.yaml") + if files.exists(agent_yaml_path): + agent_yaml = files.read_file(agent_yaml_path) + agent_data = SubAgentListItem.model_validate(yaml_helper.loads(agent_yaml) or {}) + else: + agent_json = files.read_file(files.get_abs_path(dir, subdir, "agent.json")) + agent_data = SubAgentListItem.model_validate_json(agent_json) name = agent_data.name or subdir agent_data.name = name agent_data.path = files.get_abs_path(dir, subdir) @@ -176,8 +182,13 @@ def delete_agent_data(name: str) -> None: def _load_agent_data_from_dir(dir: str, name: str, origin: Origin) -> SubAgent | None: try: - subagent_json = files.read_file(files.get_abs_path(dir, name, "agent.json")) - subagent = SubAgent.model_validate_json(subagent_json) + agent_yaml_path = files.get_abs_path(dir, name, "agent.yaml") + if files.exists(agent_yaml_path): + agent_yaml = files.read_file(agent_yaml_path) + subagent = SubAgent.model_validate(yaml_helper.loads(agent_yaml) or {}) + else: + subagent_json = files.read_file(files.get_abs_path(dir, name, "agent.json")) + subagent = SubAgent.model_validate_json(subagent_json) except Exception: # backward compatibility (before agent.json existed) try: diff --git a/python/helpers/yaml.py b/python/helpers/yaml.py new file mode 100644 index 000000000..13b1d8c55 --- /dev/null +++ b/python/helpers/yaml.py @@ -0,0 +1,24 @@ +import json +import yaml + + +def loads(text: str): + return yaml.safe_load(text) + + +def dumps(obj, **kwargs) -> str: + dump_kwargs = { + "allow_unicode": True, + "sort_keys": False, + **kwargs, + } + return yaml.safe_dump(obj, **dump_kwargs) + + +def from_json(text: str, **yaml_dump_kwargs) -> str: + return dumps(json.loads(text), **yaml_dump_kwargs) + + +def to_json(text: str, **json_dump_kwargs) -> str: + obj = loads(text) + return json.dumps(obj, ensure_ascii=False, **json_dump_kwargs)