feat: Normalize alternative tool call formats (invoke/function_calls)
This commit is contained in:
@@ -314,13 +314,29 @@ CRITICAL RULES:
|
|||||||
4. Use ONLY the exact format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
|
4. Use ONLY the exact format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
|
||||||
5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. Literal "\n" instead of real line breaks will cause tools to fail.
|
5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. Literal "\n" instead of real line breaks will cause tools to fail.
|
||||||
6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
|
6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
|
||||||
- Correct: <function=think> ... </function>
|
|
||||||
- Incorrect: <thinking_tools.think> ... </function>
|
|
||||||
- Incorrect: <think> ... </think>
|
|
||||||
- Incorrect: {"think": {...}}
|
|
||||||
7. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
|
7. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
|
||||||
8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
|
8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
|
||||||
|
|
||||||
|
CORRECT format — use this EXACTLY:
|
||||||
|
<function=tool_name>
|
||||||
|
<parameter=param_name>value</parameter>
|
||||||
|
</function>
|
||||||
|
|
||||||
|
WRONG formats — NEVER use these:
|
||||||
|
- <invoke name="tool_name"><parameter name="param_name">value</parameter></invoke>
|
||||||
|
- <function_calls><invoke name="tool_name">...</invoke></function_calls>
|
||||||
|
- <tool_call><tool_name>...</tool_name></tool_call>
|
||||||
|
- {"tool_name": {"param_name": "value"}}
|
||||||
|
- ```<function=tool_name>...</function>```
|
||||||
|
|
||||||
|
Do NOT emit any extra XML tags in your output. In particular:
|
||||||
|
- NO <thinking>...</thinking> or <thought>...</thought> blocks
|
||||||
|
- NO <scratchpad>...</scratchpad> or <reasoning>...</reasoning> blocks
|
||||||
|
- NO <answer>...</answer> or <response>...</response> wrappers
|
||||||
|
If you need to reason, use the think tool. Your raw output must contain ONLY the tool call — no surrounding XML tags.
|
||||||
|
|
||||||
|
Notice: use <function=X> NOT <invoke name="X">, use <parameter=X> NOT <parameter name="X">, use </function> NOT </invoke>.
|
||||||
|
|
||||||
Example (agent creation tool):
|
Example (agent creation tool):
|
||||||
<function=create_agent>
|
<function=create_agent>
|
||||||
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
|
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
from strix.llm.utils import normalize_tool_format
|
||||||
|
|
||||||
|
|
||||||
_FUNCTION_TAG_PREFIX = "<function="
|
_FUNCTION_TAG_PREFIX = "<function="
|
||||||
|
_INVOKE_TAG_PREFIX = "<invoke "
|
||||||
|
|
||||||
_FUNC_PATTERN = re.compile(r"<function=([^>]+)>")
|
_FUNC_PATTERN = re.compile(r"<function=([^>]+)>")
|
||||||
_FUNC_END_PATTERN = re.compile(r"</function>")
|
_FUNC_END_PATTERN = re.compile(r"</function>")
|
||||||
@@ -21,9 +24,8 @@ def _get_safe_content(content: str) -> tuple[str, str]:
|
|||||||
return content, ""
|
return content, ""
|
||||||
|
|
||||||
suffix = content[last_lt:]
|
suffix = content[last_lt:]
|
||||||
target = _FUNCTION_TAG_PREFIX # "<function="
|
|
||||||
|
|
||||||
if target.startswith(suffix):
|
if _FUNCTION_TAG_PREFIX.startswith(suffix) or _INVOKE_TAG_PREFIX.startswith(suffix):
|
||||||
return content[:last_lt], suffix
|
return content[:last_lt], suffix
|
||||||
|
|
||||||
return content, ""
|
return content, ""
|
||||||
@@ -42,6 +44,8 @@ def parse_streaming_content(content: str) -> list[StreamSegment]:
|
|||||||
if not content:
|
if not content:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
content = normalize_tool_format(content)
|
||||||
|
|
||||||
segments: list[StreamSegment] = []
|
segments: list[StreamSegment] = []
|
||||||
|
|
||||||
func_matches = list(_FUNC_PATTERN.finditer(content))
|
func_matches = list(_FUNC_PATTERN.finditer(content))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from strix.llm.memory_compressor import MemoryCompressor
|
|||||||
from strix.llm.utils import (
|
from strix.llm.utils import (
|
||||||
_truncate_to_first_function,
|
_truncate_to_first_function,
|
||||||
fix_incomplete_tool_call,
|
fix_incomplete_tool_call,
|
||||||
|
normalize_tool_format,
|
||||||
parse_tool_invocations,
|
parse_tool_invocations,
|
||||||
)
|
)
|
||||||
from strix.skills import load_skills
|
from strix.skills import load_skills
|
||||||
@@ -143,10 +144,12 @@ class LLM:
|
|||||||
delta = self._get_chunk_content(chunk)
|
delta = self._get_chunk_content(chunk)
|
||||||
if delta:
|
if delta:
|
||||||
accumulated += delta
|
accumulated += delta
|
||||||
if "</function>" in accumulated:
|
if "</function>" in accumulated or "</invoke>" in accumulated:
|
||||||
accumulated = accumulated[
|
for end_tag in ("</function>", "</invoke>"):
|
||||||
: accumulated.find("</function>") + len("</function>")
|
pos = accumulated.find(end_tag)
|
||||||
]
|
if pos != -1:
|
||||||
|
accumulated = accumulated[: pos + len(end_tag)]
|
||||||
|
break
|
||||||
yield LLMResponse(content=accumulated)
|
yield LLMResponse(content=accumulated)
|
||||||
done_streaming = 1
|
done_streaming = 1
|
||||||
continue
|
continue
|
||||||
@@ -155,6 +158,7 @@ class LLM:
|
|||||||
if chunks:
|
if chunks:
|
||||||
self._update_usage_stats(stream_chunk_builder(chunks))
|
self._update_usage_stats(stream_chunk_builder(chunks))
|
||||||
|
|
||||||
|
accumulated = normalize_tool_format(accumulated)
|
||||||
accumulated = fix_incomplete_tool_call(_truncate_to_first_function(accumulated))
|
accumulated = fix_incomplete_tool_call(_truncate_to_first_function(accumulated))
|
||||||
yield LLMResponse(
|
yield LLMResponse(
|
||||||
content=accumulated,
|
content=accumulated,
|
||||||
|
|||||||
@@ -3,6 +3,29 @@ import re
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
_INVOKE_OPEN = re.compile(r'<invoke\s+name="([^"]+)"\s*>')
|
||||||
|
_PARAM_NAME_ATTR = re.compile(r'<parameter\s+name="([^"]+)"\s*>')
|
||||||
|
_FUNCTION_CALLS_TAG = re.compile(r"</?function_calls>")
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_tool_format(content: str) -> str:
|
||||||
|
"""Convert alternative tool-call XML format to the expected one.
|
||||||
|
|
||||||
|
Handles:
|
||||||
|
<function_calls>...</function_calls> → stripped
|
||||||
|
<invoke name="X"> → <function=X>
|
||||||
|
<parameter name="X"> → <parameter=X>
|
||||||
|
</invoke> → </function>
|
||||||
|
"""
|
||||||
|
if "<invoke" not in content and "<function_calls" not in content:
|
||||||
|
return content
|
||||||
|
|
||||||
|
content = _FUNCTION_CALLS_TAG.sub("", content)
|
||||||
|
content = _INVOKE_OPEN.sub(r"<function=\1>", content)
|
||||||
|
content = _PARAM_NAME_ATTR.sub(r"<parameter=\1>", content)
|
||||||
|
return content.replace("</invoke>", "</function>")
|
||||||
|
|
||||||
|
|
||||||
STRIX_MODEL_MAP: dict[str, str] = {
|
STRIX_MODEL_MAP: dict[str, str] = {
|
||||||
"claude-sonnet-4.6": "anthropic/claude-sonnet-4-6",
|
"claude-sonnet-4.6": "anthropic/claude-sonnet-4-6",
|
||||||
"claude-opus-4.6": "anthropic/claude-opus-4-6",
|
"claude-opus-4.6": "anthropic/claude-opus-4-6",
|
||||||
@@ -41,7 +64,9 @@ def _truncate_to_first_function(content: str) -> str:
|
|||||||
if not content:
|
if not content:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
function_starts = [match.start() for match in re.finditer(r"<function=", content)]
|
function_starts = [
|
||||||
|
match.start() for match in re.finditer(r"<function=|<invoke\s+name=", content)
|
||||||
|
]
|
||||||
|
|
||||||
if len(function_starts) >= 2:
|
if len(function_starts) >= 2:
|
||||||
second_function_start = function_starts[1]
|
second_function_start = function_starts[1]
|
||||||
@@ -52,6 +77,7 @@ def _truncate_to_first_function(content: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
||||||
|
content = normalize_tool_format(content)
|
||||||
content = fix_incomplete_tool_call(content)
|
content = fix_incomplete_tool_call(content)
|
||||||
|
|
||||||
tool_invocations: list[dict[str, Any]] = []
|
tool_invocations: list[dict[str, Any]] = []
|
||||||
@@ -81,12 +107,14 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
|||||||
|
|
||||||
|
|
||||||
def fix_incomplete_tool_call(content: str) -> str:
|
def fix_incomplete_tool_call(content: str) -> str:
|
||||||
"""Fix incomplete tool calls by adding missing </function> tag."""
|
"""Fix incomplete tool calls by adding missing closing tag.
|
||||||
if (
|
|
||||||
"<function=" in content
|
Handles both ``<function=…>`` and ``<invoke name="…">`` formats.
|
||||||
and content.count("<function=") == 1
|
"""
|
||||||
and "</function>" not in content
|
has_open = "<function=" in content or "<invoke " in content
|
||||||
):
|
count_open = content.count("<function=") + content.count("<invoke ")
|
||||||
|
has_close = "</function>" in content or "</invoke>" in content
|
||||||
|
if has_open and count_open == 1 and not has_close:
|
||||||
content = content.rstrip()
|
content = content.rstrip()
|
||||||
content = content + "function>" if content.endswith("</") else content + "\n</function>"
|
content = content + "function>" if content.endswith("</") else content + "\n</function>"
|
||||||
return content
|
return content
|
||||||
@@ -107,6 +135,7 @@ def clean_content(content: str) -> str:
|
|||||||
if not content:
|
if not content:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
content = normalize_tool_format(content)
|
||||||
content = fix_incomplete_tool_call(content)
|
content = fix_incomplete_tool_call(content)
|
||||||
|
|
||||||
tool_pattern = r"<function=[^>]+>.*?</function>"
|
tool_pattern = r"<function=[^>]+>.*?</function>"
|
||||||
|
|||||||
Reference in New Issue
Block a user