Compare commits

...

5 Commits

Author SHA1 Message Date
32cd7184ea Merge pull request 'feat: implement conversational AI agent with tool-calling' (#52) from feat/conversational-agent-with-tools into main 2026-04-10 09:39:45 +02:00
shokollm
765e390b9b feat: implement conversational AI agent with tool-calling
Prototype implementation that allows:
1. Normal conversation with the AI
2. Tool-calling to update trading strategies

Created new ConversationalAgent that uses CrewAI with tools:
- get_current_strategy: Check current bot strategy
- update_trading_strategy: Update bot's trading configuration

The agent can now respond to questions like 'What is this?' without
forcing JSON output, and can update strategies when user provides
specific parameters.

Refs #51
2026-04-10 05:00:22 +00:00
21ce282cae Merge pull request 'fix: add fallback UUID generator for crypto.randomUUID compatibility' (#50) from fix/crypto-randomuuid-fallback into main 2026-04-10 06:26:49 +02:00
shokollm
4fa9b0456a fix: add fallback UUID generator for crypto.randomUUID compatibility
crypto.randomUUID() is not available in all environments (e.g., older browsers,
non-secure contexts). Added a fallback UUID v4 implementation.
2026-04-10 04:19:45 +00:00
af9900d0ba Merge pull request 'fix: add timeout for chat requests and improve error handling' (#49) from fix/chat-timeout-handling into main 2026-04-10 06:15:45 +02:00
3 changed files with 211 additions and 54 deletions

View File

@@ -16,6 +16,7 @@ from ..db.schemas import (
) )
from ..db.models import Bot, BotConversation, User from ..db.models import Bot, BotConversation, User
from ..services.ai_agent.crew import get_trading_crew from ..services.ai_agent.crew import get_trading_crew
from ..services.ai_agent.conversational import get_conversational_agent
router = APIRouter() router = APIRouter()
MAX_BOTS_PER_USER = 3 MAX_BOTS_PER_USER = 3
@@ -183,69 +184,45 @@ def chat(
.order_by(BotConversation.created_at) .order_by(BotConversation.created_at)
.all() .all()
) )
history_for_crew = [ history_for_agent = [
{"role": conv.role, "content": conv.content} {"role": conv.role, "content": conv.content}
for conv in conversation_history[-10:] for conv in conversation_history[-10:]
] ]
user_message = request.message user_message = request.message
if request.strategy_config:
crew = get_trading_crew() # Use ConversationalAgent for natural chat with tool-calling
result = crew.chat(user_message, history_for_crew) agent = get_conversational_agent(bot_id=bot_id)
result = agent.chat(user_message, history_for_agent)
assistant_content = result.get("response", "I couldn't process your request.") assistant_content = result.get("response", "I couldn't process your request.")
if result.get("success") and result.get("strategy_config"):
bot.strategy_config = result["strategy_config"]
db.commit()
db_conversation = BotConversation( # Save conversation
bot_id=bot_id, db_conversation = BotConversation(
role="user", bot_id=bot_id,
content=user_message, role="user",
) content=user_message,
db.add(db_conversation) )
db.add(db_conversation)
db_assistant = BotConversation( db_assistant = BotConversation(
bot_id=bot_id, bot_id=bot_id,
role="assistant", role="assistant",
content=assistant_content, content=assistant_content,
) )
db.add(db_assistant) db.add(db_assistant)
db.commit() db.commit()
db.refresh(db_assistant) db.refresh(db_assistant)
return BotChatResponse( # If strategy was updated via tool, refresh bot data
response=assistant_content, if result.get("strategy_updated"):
strategy_config=result.get("strategy_config"), db.refresh(bot)
success=result.get("success", False),
)
else:
crew = get_trading_crew()
result = crew.chat(user_message, history_for_crew)
assistant_content = result.get("response", "I couldn't process your request.") return BotChatResponse(
response=assistant_content,
db_conversation = BotConversation( strategy_config=bot.strategy_config if result.get("strategy_updated") else None,
bot_id=bot_id, success=result.get("success", False),
role="user", )
content=user_message,
)
db.add(db_conversation)
db_assistant = BotConversation(
bot_id=bot_id,
role="assistant",
content=assistant_content,
)
db.add(db_assistant)
db.commit()
db.refresh(db_assistant)
return BotChatResponse(
response=assistant_content,
strategy_config=result.get("strategy_config"),
success=result.get("success", False),
)
@router.get("/{bot_id}/history", response_model=List[BotConversationResponse]) @router.get("/{bot_id}/history", response_model=List[BotConversationResponse])

View File

@@ -0,0 +1,167 @@
"""
Conversational Trading Agent
This agent can:
1. Have normal conversations with users
2. Update trading strategies when user provides specific instructions
Uses CrewAI's tool-calling capabilities for structured updates.
"""
from typing import List, Optional, Dict, Any
from crewai import Agent, LLM
from crewai.tools import tool
from sqlalchemy.orm import Session
from ...core.config import get_settings
from ...db.models import Bot
# Tool definitions
@tool
def get_current_strategy(bot_id: str) -> str:
"""Get the current trading strategy configuration for a bot.
Use this tool to check the current strategy before making changes.
Args:
bot_id: The ID of the bot to get strategy for
Returns:
JSON string with current strategy configuration
"""
from ...core.database import get_db
from ...db.models import Bot
db = next(get_db())
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
return '{"error": "Bot not found"}'
return str(bot.strategy_config)
@tool
def update_trading_strategy(
bot_id: str,
conditions: List[Dict],
actions: List[Dict],
risk_management: Optional[Dict] = None
) -> str:
"""Update the trading strategy configuration for a bot.
Call this tool when the user provides specific trading parameters like:
- Buy/sell conditions (price drops, price rises, etc.)
- Take profit percentages
- Stop loss percentages
Args:
bot_id: The ID of the bot to update
conditions: List of trigger conditions (e.g., [{"type": "price_drop", "token": "PEPE", "threshold": 5}])
actions: List of actions to take (e.g., [{"type": "buy", "amount_percent": 50}])
risk_management: Optional risk settings (e.g., {"stop_loss_percent": 10, "take_profit_percent": 50})
Returns:
Confirmation message with updated strategy
"""
from ...core.database import get_db
from ...db.models import Bot
db = next(get_db())
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
return '{"error": "Bot not found"}'
new_config = {
"conditions": conditions,
"actions": actions,
}
if risk_management:
new_config["risk_management"] = risk_management
bot.strategy_config = new_config
db.commit()
return f'Successfully updated trading strategy. New config: {new_config}'
SYSTEM_PROMPT = """You are a helpful AI trading assistant. You can:
1. Have normal conversations - answer questions about trading, tokens, strategies, etc.
2. Help users configure their trading bots when they provide specific parameters
When a user asks general questions, just answer conversationally.
When a user provides specific trading parameters (like percentages, tokens, conditions),
use the update_trading_strategy tool to save their configuration.
Example conversations:
- User: "What is this?" → Answer conversationally about the trading bot platform
- User: "I want take profit at 200%" → Use update_trading_strategy with that parameter
- User: "Alert me when PEPE drops 5%" → Use update_trading_strategy with that condition
Be friendly, helpful, and clear in your responses."""
class ConversationalAgent:
def __init__(self, api_key: str, model: str = "MiniMax-M2.7", bot_id: str = None):
self.api_key = api_key
self.model = model
self.bot_id = bot_id
self.llm = LLM(
model=model,
api_key=api_key,
api_base="https://api.minimax.io/v1"
)
# Create agent with tools
self.agent = Agent(
role="Trading Assistant",
goal="Help users with trading strategies and general questions",
backstory=SYSTEM_PROMPT,
tools=[get_current_strategy, update_trading_strategy],
llm=self.llm,
verbose=True,
allow_delegation=False,
)
def chat(self, user_message: str, conversation_history: List[Dict] = None) -> Dict[str, Any]:
"""Process a user message and return a response.
Args:
user_message: The user's message
conversation_history: Optional list of previous messages
Returns:
Dict with 'response' (the assistant's reply) and 'strategy_updated' (bool)
"""
# Execute agent using kickoff
try:
result = self.agent.kickoff(user_message)
# Check if strategy was updated
result_str = str(result)
strategy_updated = "update_trading_strategy" in result_str or \
"Successfully updated" in result_str
return {
"response": result_str,
"strategy_updated": strategy_updated,
"success": True
}
except Exception as e:
return {
"response": f"I encountered an error: {str(e)}. Please try again.",
"strategy_updated": False,
"success": False
}
def get_conversational_agent(api_key: str = None, model: str = None, bot_id: str = None) -> ConversationalAgent:
"""Get or create a ConversationalAgent instance."""
if api_key is None:
settings = get_settings()
api_key = settings.MINIMAX_API_KEY
if model is None:
settings = get_settings()
model = settings.MINIMAX_MODEL
return ConversationalAgent(api_key=api_key, model=model, bot_id=bot_id)

View File

@@ -8,12 +8,25 @@ export interface ChatMessage {
timestamp: Date; timestamp: Date;
} }
// Fallback UUID generator for environments where crypto.randomUUID is not available
function generateId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
// Fallback: simple UUID v4 implementation
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
export const chatStore = writable<ChatMessage[]>([]); export const chatStore = writable<ChatMessage[]>([]);
export function addMessage(message: Omit<ChatMessage, 'id' | 'timestamp'>) { export function addMessage(message: Omit<ChatMessage, 'id' | 'timestamp'>) {
const newMessage: ChatMessage = { const newMessage: ChatMessage = {
...message, ...message,
id: crypto.randomUUID(), id: generateId(),
timestamp: new Date() timestamp: new Date()
}; };
chatStore.update(messages => [...messages, newMessage]); chatStore.update(messages => [...messages, newMessage]);