mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-19 07:59:34 +00:00
Standardize multi-action tools around tool_args.action while keeping parser compatibility for older tool/args, tool_name:action, and method-shaped requests. This keeps new prompts clean without breaking agents that learned the previous dialect. Move A0 connector remote execution/file tools into stable standard prompts, make remote targeting independent of the active chat context, and skill-gate beta computer-use remote so it no longer weighs down the always-on tool list. Align text editor, scheduler, skills, office artifact, memory, notify, and browser prompts/tools around the canonical action contract. Add scheduler update/timezone handling, skills_tool read_file, text editor patch coverage, and fixes for memory_forget, behaviour_adjustment, and code execution progress warnings. Reduce default prompt pressure by compacting browser and scheduler prompts into skill-backed manifests, shortening skill catalog descriptions, and pruning noisy framework knowledge. Remove obsolete connector prompt stubs and root tool-call knowledge examples. Tests: conda run -n a0 pytest tests/test_a0_connector_prompt_gating.py tests/test_tool_action_contracts.py tests/test_task_scheduler_timezone.py tests/test_text_editor_context_patch.py tests/test_tool_request_normalization.py tests/test_office_document_store.py::test_odf_is_advertised_and_docx_remains_explicit_compatibility tests/test_office_document_store.py::test_document_artifact_accepts_method_alias_for_ods_create tests/test_skills_runtime.py tests/test_default_prompt_budget.py::test_a0_small_profile_removed_and_prompt_text_generic -q
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
|
|
from .dirty_json import DirtyJson
|
|
import regex, re
|
|
from helpers.modules import load_classes_from_file, load_classes_from_folder # keep here for backwards compatibility
|
|
from typing import Any
|
|
|
|
def json_parse_dirty(json: str) -> dict[str, Any] | None:
|
|
if not json or not isinstance(json, str):
|
|
return None
|
|
|
|
ext_json = extract_json_object_string(json.strip())
|
|
if ext_json:
|
|
try:
|
|
data = DirtyJson.parse_string(ext_json)
|
|
if isinstance(data, dict):
|
|
return data
|
|
except Exception:
|
|
# If parsing fails, return None instead of crashing
|
|
return None
|
|
return None
|
|
|
|
|
|
def normalize_tool_request(tool_request: Any) -> tuple[str, dict]:
|
|
if not isinstance(tool_request, dict):
|
|
raise ValueError("Tool request must be a dictionary")
|
|
tool_name = tool_request.get("tool_name")
|
|
if not tool_name or not isinstance(tool_name, str):
|
|
tool_name = tool_request.get("tool")
|
|
if not tool_name or not isinstance(tool_name, str):
|
|
raise ValueError("Tool request must have a tool_name (type string) field")
|
|
tool_args = tool_request.get("tool_args")
|
|
if not isinstance(tool_args, dict):
|
|
tool_args = tool_request.get("args")
|
|
if not isinstance(tool_args, dict):
|
|
raise ValueError("Tool request must have a tool_args (type dictionary) field")
|
|
tool_args = dict(tool_args)
|
|
if ":" in tool_name:
|
|
tool_name, action = tool_name.split(":", 1)
|
|
if not tool_name or not action:
|
|
raise ValueError("tool_name method suffix must include tool and action")
|
|
tool_args.setdefault("action", action)
|
|
method = tool_args.get("method")
|
|
if "action" not in tool_args and isinstance(method, str) and method:
|
|
tool_args["action"] = method
|
|
return tool_name, tool_args
|
|
|
|
|
|
def extract_json_root_string(content: str) -> str | None:
|
|
if not content or not isinstance(content, str):
|
|
return None
|
|
|
|
start = content.find("{")
|
|
if start == -1:
|
|
return None
|
|
first_array = content.find("[")
|
|
if first_array != -1 and first_array < start:
|
|
return None
|
|
|
|
parser = DirtyJson()
|
|
try:
|
|
parser.parse(content[start:])
|
|
except Exception:
|
|
return None
|
|
|
|
if not parser.completed:
|
|
return None
|
|
|
|
return content[start : start + parser.index]
|
|
|
|
|
|
def extract_json_object_string(content):
|
|
start = content.find("{")
|
|
if start == -1:
|
|
return ""
|
|
|
|
# Find the first '{'
|
|
end = content.rfind("}")
|
|
if end == -1:
|
|
# If there's no closing '}', return from start to the end
|
|
return content[start:]
|
|
else:
|
|
# If there's a closing '}', return the substring from start to end
|
|
return content[start : end + 1]
|
|
|
|
|
|
def extract_json_string(content):
|
|
# Regular expression pattern to match a JSON object
|
|
pattern = r'\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\]|"(?:\\.|[^"\\])*"|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?'
|
|
|
|
# Search for the pattern in the content
|
|
match = regex.search(pattern, content)
|
|
|
|
if match:
|
|
# Return the matched JSON string
|
|
return match.group(0)
|
|
else:
|
|
return ""
|
|
|
|
|
|
def fix_json_string(json_string):
|
|
# Function to replace unescaped line breaks within JSON string values
|
|
def replace_unescaped_newlines(match):
|
|
return match.group(0).replace("\n", "\\n")
|
|
|
|
# Use regex to find string values and apply the replacement function
|
|
fixed_string = re.sub(
|
|
r'(?<=: ")(.*?)(?=")', replace_unescaped_newlines, json_string, flags=re.DOTALL
|
|
)
|
|
return fixed_string
|