diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ffe5b..1ca5426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.1] - 2025-11-05 ### Fixed -- Fixed Pydantic validation error for tool_calls when using DeepSeek and other AI providers that return `args` as JSON strings instead of dictionaries. Added `ToolCallArgsParsingWrapper` that monkey-patches ChatOpenAI's `_create_chat_result` method to parse string arguments before AIMessage construction. +- Fixed Pydantic validation errors for both `tool_calls` and `invalid_tool_calls` when using DeepSeek and other AI providers: + - `tool_calls.args`: Converts JSON strings to dictionaries (for successful tool calls) + - `invalid_tool_calls.args`: Converts dictionaries to JSON strings (for failed tool calls) +- Added `ToolCallArgsParsingWrapper` that monkey-patches ChatOpenAI's `_create_chat_result` method to normalize arguments before AIMessage construction. ## [0.4.0] - 2025-11-05 diff --git a/agent/chat_model_wrapper.py b/agent/chat_model_wrapper.py index d326e29..0be77de 100644 --- a/agent/chat_model_wrapper.py +++ b/agent/chat_model_wrapper.py @@ -50,19 +50,36 @@ class ToolCallArgsParsingWrapper: if 'choices' in response_dict: for choice in response_dict['choices']: - if 'message' in choice and 'tool_calls' in choice['message']: - tool_calls = choice['message']['tool_calls'] - if tool_calls: - for tool_call in tool_calls: - if 'function' in tool_call and 'arguments' in tool_call['function']: - args = tool_call['function']['arguments'] - # Parse string arguments to dict - if isinstance(args, str): - try: - tool_call['function']['arguments'] = json.loads(args) - except json.JSONDecodeError: - # Keep as string if parsing fails - pass + if 'message' not in choice: + continue + + message = choice['message'] + + # Fix regular tool_calls: string args -> dict + if 'tool_calls' in message and message['tool_calls']: + for tool_call in message['tool_calls']: + if 'function' in tool_call and 'arguments' in tool_call['function']: + args = tool_call['function']['arguments'] + # Parse string arguments to dict + if isinstance(args, str): + try: + tool_call['function']['arguments'] = json.loads(args) + except json.JSONDecodeError: + # Keep as string if parsing fails + pass + + # Fix invalid_tool_calls: dict args -> string + if 'invalid_tool_calls' in message and message['invalid_tool_calls']: + for invalid_call in message['invalid_tool_calls']: + if 'args' in invalid_call: + args = invalid_call['args'] + # Convert dict arguments to JSON string + if isinstance(args, dict): + try: + invalid_call['args'] = json.dumps(args) + except (TypeError, ValueError): + # Keep as-is if serialization fails + pass # Call original method with fixed response return original_create_chat_result(response_dict, generation_info)