Parse local function_model tool wrappers

This commit is contained in:
pewdiepie-archdaemon
2026-07-03 03:54:59 +00:00
parent 79716d717a
commit cf85c42195
2 changed files with 72 additions and 0 deletions
+43
View File
@@ -172,6 +172,21 @@ _GEMMA_TOOL_CALL_RE = re.compile(
re.IGNORECASE,
)
# Pattern 4c: Open-function wrapper emitted by some local MLX/Exo models.
# Example:
# <function_model>
# <function_call>web_search</function_call>
# <parameters>{"query":"Sweden news today"}</parameters>
# </function_model>
_FUNCTION_MODEL_OPEN_RE = re.compile(r"<function_model>\s*", re.IGNORECASE)
_FUNCTION_MODEL_CLOSE_RE = re.compile(r"</function_model>", re.IGNORECASE)
_FUNCTION_MODEL_NAME_RE = re.compile(
r"<function_call>\s*([A-Za-z_][\w-]*)\s*</function_call>",
re.IGNORECASE,
)
_FUNCTION_MODEL_PARAMS_OPEN_RE = re.compile(r"<parameters>\s*", re.IGNORECASE)
_FUNCTION_MODEL_PARAMS_CLOSE_RE = re.compile(r"</parameters>", re.IGNORECASE)
# Pattern 5: DeepSeek DSML markup leaking into content. When deepseek
# models can't emit structured tool_calls (e.g. we sent no tool schemas
@@ -885,6 +900,24 @@ def _parse_gemma_tool_call(tool_name: str, body: str) -> Optional[ToolBlock]:
return function_call_to_tool_block(tool_name, json.dumps(params))
def _parse_function_model_call(body: str) -> Optional[ToolBlock]:
"""Parse <function_model><function_call>tool</...><parameters>...</...>."""
name_match = _FUNCTION_MODEL_NAME_RE.search(body or "")
if not name_match:
return None
tool_name = name_match.group(1).strip().lower().replace("-", "_")
params = "{}"
for _ms, inner_start, inner_end, _me in _iter_delimited(
body,
_FUNCTION_MODEL_PARAMS_OPEN_RE,
_FUNCTION_MODEL_PARAMS_CLOSE_RE,
):
params = body[inner_start:inner_end].strip() or "{}"
break
from src.tool_schemas import function_call_to_tool_block
return function_call_to_tool_block(tool_name, params)
def _iter_delimited(text, open_re, close_re):
"""Yield ``(match_start, inner_start, inner_end, match_end)`` for each
non-overlapping ``open_re ... close_re`` pair, scanning strictly forward.
@@ -1136,6 +1169,15 @@ def parse_tool_blocks(text: str, skip_fenced: bool = False) -> List[ToolBlock]:
if block:
blocks.append(block)
# Pattern 4c: <function_model> wrapper from local MLX/Exo models.
if not blocks:
for _ms, inner_start, inner_end, _me in _iter_delimited(
text, _FUNCTION_MODEL_OPEN_RE, _FUNCTION_MODEL_CLOSE_RE
):
block = _parse_function_model_call(text[inner_start:inner_end])
if block:
blocks.append(block)
# Pattern 6: local text-model web_search call leaked as prose + bare JSON.
if not blocks and not skip_fenced:
raw_web_json = _parse_raw_web_json_lookup(text)
@@ -1182,6 +1224,7 @@ def strip_tool_blocks(text: str, skip_fenced: bool = False) -> str:
cleaned = _XML_OPEN_TOOL_CALL_RE.sub('', cleaned)
cleaned = _strip_delimited(cleaned, _TOOL_CODE_OPEN_RE, _TOOL_CODE_CLOSE_RE)
cleaned = _GEMMA_TOOL_CALL_RE.sub('', cleaned)
cleaned = _strip_delimited(cleaned, _FUNCTION_MODEL_OPEN_RE, _FUNCTION_MODEL_CLOSE_RE)
if not skip_fenced:
raw_web_json = _parse_raw_web_json_lookup(cleaned)
if raw_web_json:
+29
View File
@@ -0,0 +1,29 @@
import src.agent_tools # noqa: F401 (break agent_tools<->tool_parsing import cycle)
from src.tool_parsing import parse_tool_blocks, strip_tool_blocks
def test_function_model_wrapper_runs_web_search_and_strips_markup():
raw = """Sure, let me check what's making headlines in Sweden today.
<function_model>
<function_call>web_search</function_call>
<parameters>{"query": "Sweden news today July 2026"}</parameters>
</function_model>"""
blocks = parse_tool_blocks(raw, skip_fenced=True)
assert len(blocks) == 1
assert blocks[0].tool_type == "web_search"
assert blocks[0].content == "Sweden news today July 2026"
assert strip_tool_blocks(raw, skip_fenced=True) == "Sure, let me check what's making headlines in Sweden today."
def test_function_model_wrapper_with_unknown_tool_is_stripped_but_not_executed():
raw = """Nope.
<function_model>
<function_call>launch_missiles</function_call>
<parameters>{"target": "moon"}</parameters>
</function_model>"""
assert parse_tool_blocks(raw, skip_fenced=True) == []
assert strip_tool_blocks(raw, skip_fenced=True) == "Nope."