WIP: Multiple fixes and improvements

Backend:
- Fixed auth issue where get_optional_user wasn't properly extracting tokens
- Added user_id to conversational agent for proper auth context
- Fixed DCA buy logic to support multiple buys on dips
- Fixed sell logic to use amount_percent
- Added comprehensive backtest engine tests
- Fixed kline data validation for bad price data
- Fixed chained tool calls handling

Frontend:
- BotCard now links to chat page instead of bot page
- Chat page handles direct bot loading from dashboard
- Various UI improvements and fixes

Tests:
- Added test_agent.py with mock client tests
- Added test_backtest_engine.py with 7 comprehensive tests
This commit is contained in:
shokollm
2026-04-15 15:20:42 +00:00
parent 1b8761d1f4
commit f46cad4379
33 changed files with 4642 additions and 1167 deletions

View File

@@ -0,0 +1,164 @@
"""Mock client for testing the ConversationalAgent without hitting real APIs."""
from typing import List, Dict, Any, Optional
class MockMiniMaxClient:
"""Mock client that returns predefined responses for testing.
Usage:
mock = MockMiniMaxClient()
mock.add_response({\"choices\": [...]}) # Add responses in order
mock.add_response({\"choices\": [...]}) # Second call gets this
agent = ConversationalAgent(client=mock)
result = agent.chat(\"hello\")
"""
def __init__(self, responses: List[Dict[str, Any]] = None):
self.responses = responses or []
self.call_count = 0
self.calls: List[Dict[str, Any]] = [] # Record all calls for assertions
def add_response(self, response: Dict[str, Any]):
"""Add a response to be returned on the next call."""
self.responses.append(response)
def chat(
self,
messages: List[Dict[str, str]],
system_prompt: str,
tools: Optional[List[Dict[str, Any]]] = None,
temperature: float = 0.7,
max_tokens: int = 2000,
thinking_budget: int = 1500,
) -> Dict[str, Any]:
"""Return the next predefined response."""
# Record the call for debugging/tests
self.calls.append({
"messages": messages,
"system_prompt": system_prompt[:100] if system_prompt else None,
"tool_calls_count": len(tools) if tools else 0,
})
if self.call_count < len(self.responses):
response = self.responses[self.call_count]
self.call_count += 1
return response
# Default response if no more predefined responses
return {
"choices": [{
"message": {
"content": "Mock response - no more responses configured",
"role": "assistant",
}
}]
}
def reset(self):
"""Reset call count and calls list."""
self.call_count = 0
self.calls = []
def verify_call(self, call_index: int, expected_messages: int = None, expected_tool_count: int = None):
"""Verify a specific call was made correctly."""
if call_index >= len(self.calls):
raise AssertionError(f"Call {call_index} was not made. Total calls: {len(self.calls)}")
call = self.calls[call_index]
if expected_messages is not None:
actual = len(call["messages"])
if actual != expected_messages:
raise AssertionError(
f"Call {call_index}: expected {expected_messages} messages, got {actual}"
)
if expected_tool_count is not None:
actual = call["tool_calls_count"]
if actual != expected_tool_count:
raise AssertionError(
f"Call {call_index}: expected {expected_tool_count} tools, got {actual}"
)
class MockMiniMaxClientWithToolCall(MockMiniMaxClient):
"""Mock client that generates tool call responses based on message content."""
def __init__(self, tool_handlers: Dict[str, Dict[str, Any]] = None):
"""tool_handlers: dict mapping tool names to their responses."""
super().__init__()
self.tool_handlers = tool_handlers or {}
def chat(
self,
messages: List[Dict[str, str]],
system_prompt: str,
tools: Optional[List[Dict[str, Any]]] = None,
temperature: float = 0.7,
max_tokens: int = 2000,
thinking_budget: int = 1500,
) -> Dict[str, Any]:
"""Check if last message contains a tool call request and return appropriate response."""
self.calls.append({
"messages": messages,
"system_prompt": system_prompt[:100] if system_prompt else None,
"tool_calls_count": len(tools) if tools else 0,
})
# Get the last message
if not messages:
return {"choices": [{"message": {"content": "No messages", "role": "assistant"}}]}
last_msg = messages[-1]
# If it's a tool result, look for the tool that was called
if last_msg.get("role") == "user" and "[TOOL RESULT]" in last_msg.get("content", ""):
# Extract tool name from the content
content = last_msg["content"]
# Format: [TOOL RESULT] tool_name: result
for tool_name in self.tool_handlers:
if f"[TOOL RESULT] {tool_name}:" in content:
return self.tool_handlers[tool_name]
# Check if we should generate a tool call
last_user_msg = ""
for msg in reversed(messages):
if msg.get("role") == "user" and "[TOOL RESULT]" not in msg.get("content", ""):
last_user_msg = msg.get("content", "")
break
# Check each tool's trigger
for tool_name, handler in self.tool_handlers.items():
trigger = handler.get("trigger", "")
if trigger and trigger.lower() in last_user_msg.lower():
return {
"choices": [{
"message": {
"content": handler.get("content", ""),
"role": "assistant",
"tool_calls": [{
"id": f"call_{tool_name}",
"type": "function",
"function": {
"name": tool_name,
"arguments": handler.get("arguments", "{}")
}
}]
}
}]
}
# Default: return first available response or empty
if self.responses:
response = self.responses[0]
self.call_count += 1
return response
return {
"choices": [{
"message": {
"content": "How can I help you with your trading bot?",
"role": "assistant",
}
}]
}