# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Strict-mode (Auto-Heal disabled) tool-call parsing. With ``allow_incomplete=False`` the parser must accept a well-formed ``...`` call even when the model appends prose after the closing tag -- matching the JSON-style ``...`` path, which already tolerates trailing text -- while still rejecting genuinely truncated calls that never close. """ from __future__ import annotations import json import sys from pathlib import Path _BACKEND_DIR = str(Path(__file__).resolve().parent.parent) if _BACKEND_DIR not in sys.path: sys.path.insert(0, _BACKEND_DIR) from core.inference.tool_call_parser import parse_tool_calls_from_text def _only(text: str) -> dict: calls = parse_tool_calls_from_text(text, allow_incomplete = False) assert len(calls) == 1, f"expected exactly one call, got {len(calls)}: {calls!r}" fn = calls[0]["function"] return {"name": fn["name"], "arguments": json.loads(fn["arguments"])} class TestFunctionStyleTrailingText: def test_closed_function_with_trailing_prose_is_accepted(self): text = ( "weather london" " Let me check that for you." ) call = _only(text) assert call == {"name": "web_search", "arguments": {"query": "weather london"}} def test_closed_function_with_trailing_whitespace_is_accepted(self): text = "cats \n\n" call = _only(text) assert call == {"name": "web_search", "arguments": {"query": "cats"}} def test_closed_function_without_trailing_text_still_parses(self): text = "cats" call = _only(text) assert call == {"name": "web_search", "arguments": {"query": "cats"}} def test_multi_param_with_trailing_prose(self): text = ( "ls -la" "home running it now" ) call = _only(text) assert call == { "name": "terminal", "arguments": {"command": "ls -la", "workdir": "home"}, } def test_code_value_containing_literal_close_tag_is_preserved(self): # The real closing is the last one; the literal inside # the code argument must survive (rfind, not the first match). text = ( "" 'print("")' " all done" ) call = _only(text) assert call == {"name": "python", "arguments": {"code": 'print("")'}} def test_incomplete_function_without_close_is_still_rejected(self): text = "weather london" assert parse_tool_calls_from_text(text, allow_incomplete = False) == [] def test_param_without_close_tag_is_rejected_in_strict_mode(self): # Closing present, but the single parameter never closes. text = "weather london" assert parse_tool_calls_from_text(text, allow_incomplete = False) == [] class TestParityWithJsonStyle: def test_json_tool_call_with_trailing_prose_is_accepted(self): text = ( '{"name":"web_search","arguments":{"query":"weather london"}}' " Let me check that for you." ) calls = parse_tool_calls_from_text(text, allow_incomplete = False) assert len(calls) == 1 assert calls[0]["function"]["name"] == "web_search" def test_function_and_json_styles_agree_on_trailing_text(self): q = "weather london" func = parse_tool_calls_from_text( f"{q} trailing", allow_incomplete = False, ) js = parse_tool_calls_from_text( f'{{"name":"web_search","arguments":{{"query":"{q}"}}}} trailing', allow_incomplete = False, ) assert len(func) == len(js) == 1 assert json.loads(func[0]["function"]["arguments"]) == {"query": q} assert json.loads(js[0]["function"]["arguments"]) == {"query": q} class TestHealingPathUnaffected: def test_auto_heal_still_repairs_unclosed_function(self): text = "cats" calls = parse_tool_calls_from_text(text, allow_incomplete = True) assert len(calls) == 1 assert calls[0]["function"]["name"] == "web_search"