feat: Add tool call parsing for 'tool: {...}' text format

- Detect 'tool: {...}' pattern in Google model text responses
- Parse and convert to OpenAI-compatible tool_calls format
- Extract assistant text from 'assistant: [...]' format if present
- Handle both 'action' and 'name' fields for tool identification
- Convert arguments to JSON string for OpenAI compatibility

This fixes issues where models return tool calls as text instead of
using proper function_call attributes.
parent e1e0092d
...@@ -451,46 +451,97 @@ class GoogleProviderHandler(BaseProviderHandler): ...@@ -451,46 +451,97 @@ class GoogleProviderHandler(BaseProviderHandler):
# Some models return tool calls as text content instead of using function_call attribute # Some models return tool calls as text content instead of using function_call attribute
if response_text and not openai_tool_calls: if response_text and not openai_tool_calls:
import json import json
try: import re
# Try to parse as JSON
parsed_json = json.loads(response_text.strip()) # First, check for "tool: {...}" format
if isinstance(parsed_json, dict): tool_pattern = r'tool:\s*(\{[^}]*\})'
# Check if it looks like a tool call tool_match = re.search(tool_pattern, response_text, re.DOTALL)
if 'action' in parsed_json or 'function' in parsed_json or 'name' in parsed_json:
# This appears to be a tool call in JSON format if tool_match:
# Convert to OpenAI tool_calls format try:
if 'action' in parsed_json: tool_json_str = tool_match.group(1)
# Google-style tool call parsed_json = json.loads(tool_json_str)
openai_tool_call = { logging.info(f"Detected 'tool:' format in text content: {parsed_json}")
"id": f"call_{call_id}",
"type": "function", # Convert to OpenAI tool_calls format
"function": { openai_tool_call = {
"name": parsed_json.get('action', 'unknown'), "id": f"call_{call_id}",
"arguments": {k: v for k, v in parsed_json.items() if k != 'action'} "type": "function",
} "function": {
} "name": parsed_json.get('action', parsed_json.get('name', 'unknown')),
openai_tool_calls.append(openai_tool_call) "arguments": json.dumps({k: v for k, v in parsed_json.items() if k not in ['action', 'name']})
call_id += 1 }
logging.info(f"Detected tool call in text content: {parsed_json}") }
# Clear response_text since we're using tool_calls instead openai_tool_calls.append(openai_tool_call)
call_id += 1
logging.info(f"Converted 'tool:' format to OpenAI tool_calls: {openai_tool_call}")
# Extract any assistant text after the tool call
assistant_pattern = r"assistant:\s*(\[.*\])"
assistant_match = re.search(assistant_pattern, response_text, re.DOTALL)
if assistant_match:
try:
assistant_content = json.loads(assistant_match.group(1))
# Extract text from the assistant content
if isinstance(assistant_content, list) and len(assistant_content) > 0:
for item in assistant_content:
if isinstance(item, dict) and item.get('type') == 'text':
response_text = item.get('text', '')
break
else:
response_text = ""
else:
response_text = ""
except json.JSONDecodeError:
response_text = "" response_text = ""
elif 'function' in parsed_json or 'name' in parsed_json: else:
# OpenAI-style tool call # Clear response_text since we're using tool_calls instead
openai_tool_call = { response_text = ""
"id": f"call_{call_id}", except (json.JSONDecodeError, Exception) as e:
"type": "function", logging.debug(f"Failed to parse 'tool:' format: {e}")
"function": {
"name": parsed_json.get('name', parsed_json.get('function', 'unknown')), # Fall back to original JSON parsing if no tool: format found
"arguments": parsed_json.get('arguments', parsed_json.get('parameters', {})) elif not openai_tool_calls:
try:
# Try to parse as JSON
parsed_json = json.loads(response_text.strip())
if isinstance(parsed_json, dict):
# Check if it looks like a tool call
if 'action' in parsed_json or 'function' in parsed_json or 'name' in parsed_json:
# This appears to be a tool call in JSON format
# Convert to OpenAI tool_calls format
if 'action' in parsed_json:
# Google-style tool call
openai_tool_call = {
"id": f"call_{call_id}",
"type": "function",
"function": {
"name": parsed_json.get('action', 'unknown'),
"arguments": json.dumps({k: v for k, v in parsed_json.items() if k != 'action'})
}
} }
} openai_tool_calls.append(openai_tool_call)
openai_tool_calls.append(openai_tool_call) call_id += 1
call_id += 1 logging.info(f"Detected tool call in text content: {parsed_json}")
logging.info(f"Detected tool call in text content: {parsed_json}") # Clear response_text since we're using tool_calls instead
# Clear response_text since we're using tool_calls instead response_text = ""
response_text = "" elif 'function' in parsed_json or 'name' in parsed_json:
except (json.JSONDecodeError, Exception) as e: # OpenAI-style tool call
logging.debug(f"Response text is not valid JSON: {e}") openai_tool_call = {
"id": f"call_{call_id}",
"type": "function",
"function": {
"name": parsed_json.get('name', parsed_json.get('function', 'unknown')),
"arguments": json.dumps(parsed_json.get('arguments', parsed_json.get('parameters', {})))
}
}
openai_tool_calls.append(openai_tool_call)
call_id += 1
logging.info(f"Detected tool call in text content: {parsed_json}")
# Clear response_text since we're using tool_calls instead
response_text = ""
except (json.JSONDecodeError, Exception) as e:
logging.debug(f"Response text is not valid JSON: {e}")
# Set tool_calls if we have any # Set tool_calls if we have any
if openai_tool_calls: if openai_tool_calls:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment