mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-01 17:17:24 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user