rebase
This commit is contained in:
@@ -31,6 +31,59 @@ from app.models import LlmCallLog
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
# Patterns for parsing tool calls emitted as text (some local models don't
|
||||
# use the OpenAI function-calling format and instead emit calls as text).
|
||||
import re as _re
|
||||
|
||||
_TOOL_CALL_PATTERNS = [
|
||||
# call:name{json_args} or call:name(json_args)
|
||||
_re.compile(r"call:(\w+)\s*[\{\(]([^}\)]*)[\}\)]"),
|
||||
# <tool_call>name{args}</tool_call> or <tool_call>\n{"name": ..., "arguments": ...}\n</tool_call>
|
||||
_re.compile(r"<tool_call>\s*(\w+)\s*\{([^}]*)\}\s*</tool_call>"),
|
||||
# name{"key": "value", ...} (function call style)
|
||||
_re.compile(r"\b(\w+)\s*\(\s*(\{[^}]*\})\s*\)"),
|
||||
]
|
||||
|
||||
|
||||
def _parse_text_tool_calls(content: str) -> list[dict]:
|
||||
"""Parse tool calls emitted as text by some local models.
|
||||
|
||||
Handles patterns like:
|
||||
- call:calc{"expression": "2+2"}
|
||||
- <tool_call>calc{"expression": "2+2"}</tool_call>
|
||||
- calc({"expression": "2+2"})
|
||||
|
||||
Returns a list of OpenAI-format tool_call dicts.
|
||||
"""
|
||||
if not content:
|
||||
return []
|
||||
calls: list[dict] = []
|
||||
for pattern in _TOOL_CALL_PATTERNS:
|
||||
for match in pattern.finditer(content):
|
||||
name = match.group(1)
|
||||
args_str = match.group(2).strip()
|
||||
if not args_str:
|
||||
args = {}
|
||||
else:
|
||||
try:
|
||||
args = json.loads(args_str)
|
||||
except json.JSONDecodeError:
|
||||
# Try fixing common issues: single quotes, unquoted keys
|
||||
try:
|
||||
fixed = args_str.replace("'", '"')
|
||||
# Add quotes around bare keys
|
||||
fixed = _re.sub(r"(\w+)\s*:", r'"\1":', fixed)
|
||||
args = json.loads(fixed)
|
||||
except json.JSONDecodeError:
|
||||
args = {"_raw": args_str}
|
||||
calls.append({
|
||||
"id": f"parsed_{len(calls)}",
|
||||
"type": "function",
|
||||
"function": {"name": name, "arguments": json.dumps(args)},
|
||||
})
|
||||
return calls
|
||||
|
||||
|
||||
class LLMError(Exception):
|
||||
"""Base LLM error."""
|
||||
|
||||
@@ -218,6 +271,18 @@ class LlmClient:
|
||||
finish_reason = choice.get("finish_reason", "stop")
|
||||
usage = data.get("usage", {})
|
||||
|
||||
# If the model didn't return tool_calls in the OpenAI format but DID
|
||||
# emit them as text (some local models use "call:name{args}" or
|
||||
# "<tool_call>name{args}</tool_call>"), try to parse them out.
|
||||
if tools and not msg.get("tool_calls"):
|
||||
content = msg.get("content", "") or ""
|
||||
parsed = _parse_text_tool_calls(content)
|
||||
if parsed:
|
||||
msg = dict(msg) # don't mutate the original
|
||||
msg["tool_calls"] = parsed
|
||||
if finish_reason == "stop":
|
||||
finish_reason = "tool_calls"
|
||||
|
||||
log_id: uuid.UUID | None = None
|
||||
if session is not None:
|
||||
log_id = await self._write_log_safely(
|
||||
|
||||
Reference in New Issue
Block a user