fix: Use ast.literal_eval for Python-style single quotes in tool calls

- Models may return single quotes instead of double quotes
- Fall back to ast.literal_eval when JSON parsing fails
- Handle both JSON and Python-style literals in streaming responses
parent 3507a642
......@@ -1431,14 +1431,20 @@ class RotationHandler:
# Check for tool call patterns in the accumulated text
if accumulated_response_text:
import re as re_module
import ast as ast_module # For parsing Python-style literals with single quotes
# Pattern 0: "assistant: [...]" wrapping everything (nested format)
outer_assistant_pattern = r"^assistant:\s*(\[.*\])\s*$"
outer_assistant_match = re_module.match(outer_assistant_pattern, accumulated_response_text.strip(), re_module.DOTALL)
if outer_assistant_match:
try:
# Try JSON first, then fall back to ast.literal_eval for Python-style single quotes
try:
outer_content = json.loads(outer_assistant_match.group(1))
except json.JSONDecodeError:
# Model may have used single quotes instead of double quotes
outer_content = ast_module.literal_eval(outer_assistant_match.group(1))
if isinstance(outer_content, list) and len(outer_content) > 0:
for item in outer_content:
if isinstance(item, dict) and item.get('type') == 'text':
......@@ -1481,8 +1487,12 @@ class RotationHandler:
# Extract the final assistant text if present
if inner_tool_match.group(2):
try:
# Try JSON first, then fall back to ast.literal_eval
try:
final_assistant = json.loads(inner_tool_match.group(2))
except json.JSONDecodeError:
final_assistant = ast_module.literal_eval(inner_tool_match.group(2))
if isinstance(final_assistant, list) and len(final_assistant) > 0:
for final_item in final_assistant:
if isinstance(final_item, dict) and final_item.get('type') == 'text':
......@@ -1492,14 +1502,14 @@ class RotationHandler:
final_text = ""
else:
final_text = ""
except json.JSONDecodeError:
except (json.JSONDecodeError, ValueError, SyntaxError):
final_text = ""
else:
final_text = ""
except (json.JSONDecodeError, Exception) as e:
except (json.JSONDecodeError, ValueError, SyntaxError, Exception) as e:
logger.debug(f"Failed to parse streaming tool JSON: {e}")
break
except (json.JSONDecodeError, Exception) as e:
except (json.JSONDecodeError, ValueError, SyntaxError, Exception) as e:
logger.debug(f"Failed to parse outer assistant format in streaming: {e}")
# Pattern 1: Simple "tool: {...}" format (not nested)
......@@ -1539,8 +1549,12 @@ class RotationHandler:
# Extract the final assistant text if present
if tool_match.group(2):
try:
# Try JSON first, then fall back to ast.literal_eval
try:
final_assistant = json.loads(tool_match.group(2))
except json.JSONDecodeError:
final_assistant = ast_module.literal_eval(tool_match.group(2))
if isinstance(final_assistant, list) and len(final_assistant) > 0:
for final_item in final_assistant:
if isinstance(final_item, dict) and final_item.get('type') == 'text':
......@@ -1550,11 +1564,11 @@ class RotationHandler:
final_text = ""
else:
final_text = ""
except json.JSONDecodeError:
except (json.JSONDecodeError, ValueError, SyntaxError):
final_text = ""
else:
final_text = ""
except (json.JSONDecodeError, Exception) as e:
except (json.JSONDecodeError, ValueError, SyntaxError, Exception) as e:
logger.debug(f"Failed to parse simple streaming tool JSON: {e}")
# Now send the response chunks
......
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