From 0641ce554aa3fb4f659278f44030b0295b945e67 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 5 Nov 2025 21:18:54 -0500 Subject: [PATCH] fix: remove incorrect tool_calls conversion logic Systematic debugging revealed the root cause of Pydantic validation errors: - DeepSeek correctly returns tool_calls.arguments as JSON strings - My wrapper was incorrectly converting strings to dicts - This caused LangChain's parse_tool_call() to fail (json.loads(dict) error) - Failure created invalid_tool_calls with dict args (should be string) - Result: Pydantic validation error on invalid_tool_calls Solution: Remove all conversion logic. DeepSeek format is already correct. ToolCallArgsParsingWrapper now acts as a simple passthrough proxy. Trading session completes successfully with no errors. Fixes the systematic-debugging investigation that identified the issue was in our fix attempt, not in the original API response. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 8 ++-- agent/chat_model_wrapper.py | 93 ++++++------------------------------- 2 files changed, 19 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca5426..bed2916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +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 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. +- Resolved Pydantic validation errors when using DeepSeek Chat v3.1 through systematic debugging +- Root cause: Initial implementation incorrectly converted tool_calls arguments from strings to dictionaries, causing LangChain's `parse_tool_call()` to fail and create invalid_tool_calls with wrong format +- Solution: Removed unnecessary conversion logic - DeepSeek already returns arguments in correct format (JSON strings) +- `ToolCallArgsParsingWrapper` now acts as a simple passthrough proxy (kept for backward compatibility) ## [0.4.0] - 2025-11-05 diff --git a/agent/chat_model_wrapper.py b/agent/chat_model_wrapper.py index 1291890..1c5c986 100644 --- a/agent/chat_model_wrapper.py +++ b/agent/chat_model_wrapper.py @@ -1,26 +1,24 @@ """ -Chat model wrapper to fix tool_calls args parsing issues. +Chat model wrapper - Passthrough wrapper for ChatOpenAI models. -Some AI providers (like DeepSeek) return tool_calls.args as JSON strings instead -of dictionaries, causing Pydantic validation errors. This wrapper monkey-patches -the model to fix args before AIMessage construction. +Originally created to fix DeepSeek tool_calls arg parsing issues, but investigation +revealed DeepSeek already returns the correct format (arguments as JSON strings). + +This wrapper is now a simple passthrough that proxies all calls to the underlying model. +Kept for backward compatibility and potential future use. """ -import json -from typing import Any, List, Optional, Dict -from functools import wraps -from langchain_core.messages import AIMessage, BaseMessage +from typing import Any class ToolCallArgsParsingWrapper: """ - Wrapper around ChatOpenAI that fixes tool_calls args parsing. + Passthrough wrapper around ChatOpenAI models. - This fixes the Pydantic validation error: - "Input should be a valid dictionary [type=dict_type, input_value='...', input_type=str]" + After systematic debugging, determined that DeepSeek returns tool_calls.arguments + as JSON strings (correct format), so no parsing/conversion is needed. - Works by monkey-patching _create_chat_result to parse string args before - AIMessage construction. + This wrapper simply proxies all calls to the wrapped model. """ def __init__(self, model: Any, **kwargs): @@ -28,63 +26,10 @@ class ToolCallArgsParsingWrapper: Initialize wrapper around a chat model. Args: - model: The chat model to wrap (should be ChatOpenAI instance) + model: The chat model to wrap **kwargs: Additional parameters (ignored, for compatibility) """ self.wrapped_model = model - self._patch_model() - - def _patch_model(self): - """Monkey-patch the model's _create_chat_result to fix tool_calls args""" - if not hasattr(self.wrapped_model, '_create_chat_result'): - # Model doesn't have this method (e.g., MockChatModel), skip patching - return - - original_create_chat_result = self.wrapped_model._create_chat_result - - @wraps(original_create_chat_result) - def patched_create_chat_result(response: Any, generation_info: Optional[Dict] = None): - """Patched version that fixes tool_calls args before AIMessage construction""" - # Fix tool_calls in the response dict before passing to original method - response_dict = response if isinstance(response, dict) else response.model_dump() - - # DIAGNOSTIC: Log response structure - print(f"\n[DEBUG] Response keys: {response_dict.keys()}") - if 'choices' in response_dict: - print(f"[DEBUG] Number of choices: {len(response_dict['choices'])}") - for i, choice in enumerate(response_dict['choices']): - print(f"[DEBUG] Choice {i} keys: {choice.keys()}") - if 'message' in choice: - message = choice['message'] - print(f"[DEBUG] Message keys: {message.keys()}") - - # Check tool_calls structure - if 'tool_calls' in message and message['tool_calls']: - print(f"[DEBUG] Found {len(message['tool_calls'])} tool_calls") - for j, tc in enumerate(message['tool_calls']): - print(f"[DEBUG] tool_calls[{j}] keys: {tc.keys()}") - if 'function' in tc: - print(f"[DEBUG] tool_calls[{j}].function keys: {tc['function'].keys()}") - if 'arguments' in tc['function']: - args = tc['function']['arguments'] - print(f"[DEBUG] tool_calls[{j}].function.arguments type: {type(args)}") - print(f"[DEBUG] tool_calls[{j}].function.arguments value: {repr(args)[:200]}") - - if 'invalid_tool_calls' in message: - print(f"[DEBUG] Found invalid_tool_calls: {len(message['invalid_tool_calls'])} items") - for j, inv in enumerate(message['invalid_tool_calls']): - print(f"[DEBUG] invalid_tool_calls[{j}] keys: {inv.keys()}") - if 'args' in inv: - print(f"[DEBUG] invalid_tool_calls[{j}].args type: {type(inv['args'])}") - print(f"[DEBUG] invalid_tool_calls[{j}].args value: {inv['args']}") - - # REMOVED: No conversion needed yet - gathering data first - - # Call original method with unmodified response - return original_create_chat_result(response_dict, generation_info) - - # Replace the method - self.wrapped_model._create_chat_result = patched_create_chat_result @property def _llm_type(self) -> str: @@ -94,21 +39,13 @@ class ToolCallArgsParsingWrapper: return "wrapped-chat-model" def __getattr__(self, name: str): - """Proxy all other attributes/methods to the wrapped model""" + """Proxy all attributes/methods to the wrapped model""" return getattr(self.wrapped_model, name) def bind_tools(self, tools: Any, **kwargs): - """ - Bind tools to the wrapped model. - - Since we patch the model in-place, we can just delegate to the wrapped model. - """ + """Bind tools to the wrapped model""" return self.wrapped_model.bind_tools(tools, **kwargs) def bind(self, **kwargs): - """ - Bind settings to the wrapped model. - - Since we patch the model in-place, we can just delegate to the wrapped model. - """ + """Bind settings to the wrapped model""" return self.wrapped_model.bind(**kwargs)