diff --git a/src/tools/__init__.py b/src/tools/__init__.py new file mode 100644 index 000000000..c431160ad --- /dev/null +++ b/src/tools/__init__.py @@ -0,0 +1,6 @@ +"""Tool implementation package, split by domain (slice 1, #4082/#4071). + +Public tool functions live in domain modules. ``src.tool_implementations`` +re-exports from here for backward compatibility. +""" +from src.tools._common import _parse_tool_args # noqa: F401 diff --git a/src/tools/_common.py b/src/tools/_common.py new file mode 100644 index 000000000..856d498f2 --- /dev/null +++ b/src/tools/_common.py @@ -0,0 +1,39 @@ +"""Shared helpers used across tool implementation domains. + +Extracted from tool_implementations.py as part of slice 1 (#4082/#4071). +Domain modules under src/tools/ import from here. +""" +import json + + +def _parse_tool_args(content): + """Parse a tool-call argument blob. + + Accepts either a JSON string or an already-decoded dict. Unwraps the + common `{"body": {...}}` envelope that smaller models emit when they + read tool descriptions like "Body is JSON: {...}" literally — they + pass `body` as a field name rather than treating it as a noun. + + Returns a dict on success, raises ValueError on bad JSON. + """ + if isinstance(content, str): + try: + args = json.loads(content) if content.strip() else {} + except (json.JSONDecodeError, TypeError) as e: + raise ValueError(str(e)) + elif isinstance(content, dict): + args = content + else: + args = {} + # Unwrap {"body": {...}} envelope — but only if `body` is the sole key + # and points at a dict. We don't want to clobber a legitimate `body` + # field on tools where it's a real arg (e.g. send_email body text). + if ( + isinstance(args, dict) + and len(args) == 1 + and "body" in args + and isinstance(args["body"], dict) + and "action" in args["body"] # extra safety: only unwrap if the inner dict looks like a tool call + ): + args = args["body"] + return args