fix: handle tool_calls as single object or array

- Parse tool_calls whether it's a single object {...} or array [...]
- Normalize to list for consistent processing
- Add debug logging to trace tool execution flow
- Fix variable name (value_str instead of array_str)
This commit is contained in:
2026-02-24 14:59:35 +01:00
parent f83e6fc711
commit aa137b685b
+27 -7
View File
@@ -139,19 +139,25 @@ def parse_tool_calls(text: str) -> tuple:
cleaned_text = re.sub(r'```(?:json)?\s*\n?(.+?)```', r'\1', cleaned_text, flags=re.DOTALL)
cleaned_text = cleaned_text.strip()
# Try to find JSON with tool_calls - look for { tool_calls: [...] } pattern
# Try to find JSON with tool_calls - look for { tool_calls: [...] } or { tool_calls: {...} } pattern
try:
# Look for tool_calls inside braces (handle both quoted and unquoted keys)
pattern = r'\{\s*"?tool_calls"?\s*:\s*(\[.*?\])\s*\}'
# Match either an array \[...\] or a single object {...}
pattern = r'\{\s*"?tool_calls"?\s*:\s*(\[.*?\]|\{.*?\})\s*\}'
match = re.search(pattern, cleaned_text, re.DOTALL)
if match:
array_str = match.group(1)
value_str = match.group(1)
# Try to parse as JSON first
try:
tool_calls = json.loads(array_str)
parsed = json.loads(value_str)
# Normalize to list: if it's a dict (single tool), wrap in list
if isinstance(parsed, dict):
tool_calls = [parsed]
else:
tool_calls = parsed
except json.JSONDecodeError:
# Fix common JSON issues in model output
fixed = array_str
fixed = value_str
# Step 1: Handle unquoted keys (JavaScript style)
fixed = re.sub(r'([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:', r'\1"\2":', fixed)
@@ -181,7 +187,12 @@ def parse_tool_calls(text: str) -> tuple:
fixed = fixed.replace("'", '"')
try:
tool_calls = json.loads(fixed)
parsed = json.loads(fixed)
# Normalize to list
if isinstance(parsed, dict):
tool_calls = [parsed]
else:
tool_calls = parsed
except json.JSONDecodeError as e2:
# If still fails, try one more approach - manual extraction
try:
@@ -190,7 +201,7 @@ def parse_tool_calls(text: str) -> tuple:
# Find all function blocks - need to handle nested braces
# Look for "function": {...} where ... can contain nested braces
func_pattern = r'"function":\s*(\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\})'
func_matches = list(re.finditer(func_pattern, array_str, re.DOTALL))
func_matches = list(re.finditer(func_pattern, value_str, re.DOTALL))
for i, func_match in enumerate(func_matches):
func_content = func_match.group(1)
@@ -367,6 +378,7 @@ async def chat_completions(request: ChatCompletionRequest):
# Format messages into prompt (with tools if provided)
prompt = format_messages_with_tools(request.messages, request.tools)
has_tools = request.tools is not None and len(request.tools) > 0
print(f"DEBUG: has_tools={has_tools}, tools_count={len(request.tools) if request.tools else 0}")
# Generate ID
completion_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
@@ -507,6 +519,8 @@ async def chat_completions(request: ChatCompletionRequest):
response_text = result.selected_response.text
tokens_generated = result.selected_response.tokens_generated
print(f"DEBUG: Generated response (tokens={tokens_generated})")
print(f"DEBUG: Response preview: {response_text[:200]}...")
# Parse tool calls if tools were provided
content = response_text
@@ -514,7 +528,9 @@ async def chat_completions(request: ChatCompletionRequest):
finish_reason = "stop"
if has_tools:
print(f"DEBUG: Parsing tool calls from response...")
content, tool_calls_parsed = parse_tool_calls(response_text)
print(f"DEBUG: parse_tool_calls returned: content_len={len(content)}, parsed={tool_calls_parsed is not None}")
if tool_calls_parsed:
print(f" 🔧 Model requesting {len(tool_calls_parsed)} tool(s)...")
executor = get_tool_executor()
@@ -543,6 +559,10 @@ async def chat_completions(request: ChatCompletionRequest):
finish_reason = "stop"
tool_calls = [] # Clear tool_calls since we executed them
print(f" ✅ All tools executed, returning results")
else:
print(f"DEBUG: No tool calls parsed from response")
else:
print(f"DEBUG: No tools requested, returning normal response")
# Estimate prompt tokens (rough approximation)
prompt_tokens = len(prompt) // 4