Compare commits
4 Commits
8acce849f4
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ae8d76bde | ||
| a9679bbb5d | |||
|
|
b1ddad0808 | ||
|
|
f705269e34 |
@@ -16,7 +16,7 @@ from ..db.schemas import (
|
||||
)
|
||||
from ..db.models import Bot, BotConversation, User
|
||||
from ..services.ai_agent.crew import get_trading_crew
|
||||
from ..services.ai_agent.conversational import get_conversational_agent
|
||||
from ..services.ai_agent import get_conversational_agent
|
||||
|
||||
router = APIRouter()
|
||||
MAX_BOTS_PER_USER = 3
|
||||
@@ -224,7 +224,9 @@ def chat(
|
||||
strategy_config=bot.strategy_config if result.get("strategy_updated") else None,
|
||||
success=result.get("success", False),
|
||||
strategy_needs_confirmation=result.get("strategy_needs_confirmation", False),
|
||||
strategy_data=result.get("strategy_data") if result.get("strategy_needs_confirmation") else None,
|
||||
strategy_data=result.get("strategy_data")
|
||||
if result.get("strategy_needs_confirmation")
|
||||
else None,
|
||||
token_search_results=result.get("token_search_results"),
|
||||
)
|
||||
|
||||
|
||||
225
src/backend/app/api/conversations.py
Normal file
225
src/backend/app/api/conversations.py
Normal file
@@ -0,0 +1,225 @@
|
||||
import secrets
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Response
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Annotated
|
||||
|
||||
from ..core.database import get_db
|
||||
from ..db.models import Conversation, Message, User, AnonymousUser, Bot
|
||||
from ..services.auth import get_current_user
|
||||
from ..services.rate_limiter import RateLimiter
|
||||
from ..services.ai_agent import get_conversational_agent
|
||||
|
||||
router = APIRouter(prefix="/api/conversations", tags=["conversations"])
|
||||
|
||||
|
||||
def get_or_create_anonymous_token(
|
||||
request: Request, response: Response, db: Session
|
||||
) -> str:
|
||||
token = request.cookies.get("anonymous_token")
|
||||
if not token:
|
||||
token = secrets.token_urlsafe(32)
|
||||
response.set_cookie(
|
||||
key="anonymous_token",
|
||||
value=token,
|
||||
max_age=60 * 60 * 24 * 365,
|
||||
httponly=True,
|
||||
)
|
||||
anon = AnonymousUser(id=token)
|
||||
db.add(anon)
|
||||
db.commit()
|
||||
return token
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_conversations(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
):
|
||||
if current_user:
|
||||
return (
|
||||
db.query(Conversation)
|
||||
.filter(Conversation.user_id == current_user.id)
|
||||
.order_by(Conversation.updated_at.desc())
|
||||
.all()
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_conversation(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
request: Request = None,
|
||||
response: Response = None,
|
||||
):
|
||||
anonymous_token = None
|
||||
if not current_user and request:
|
||||
anonymous_token = get_or_create_anonymous_token(request, response, db)
|
||||
|
||||
conversation = Conversation(
|
||||
user_id=current_user.id if current_user else None,
|
||||
anonymous_token=anonymous_token,
|
||||
)
|
||||
db.add(conversation)
|
||||
db.commit()
|
||||
db.refresh(conversation)
|
||||
return conversation
|
||||
|
||||
|
||||
@router.get("/{conversation_id}")
|
||||
def get_conversation(
|
||||
conversation_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
):
|
||||
conversation = (
|
||||
db.query(Conversation).filter(Conversation.id == conversation_id).first()
|
||||
)
|
||||
if not conversation:
|
||||
raise HTTPException(status_code=404, detail="Conversation not found")
|
||||
|
||||
if conversation.user_id and conversation.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
return conversation
|
||||
|
||||
|
||||
@router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_conversation(
|
||||
conversation_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
):
|
||||
conversation = (
|
||||
db.query(Conversation).filter(Conversation.id == conversation_id).first()
|
||||
)
|
||||
if not conversation:
|
||||
raise HTTPException(status_code=404, detail="Conversation not found")
|
||||
|
||||
if conversation.user_id and conversation.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
db.delete(conversation)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.post("/{conversation_id}/set-bot")
|
||||
def set_bot_for_conversation(
|
||||
conversation_id: str,
|
||||
bot_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
request: Request = None,
|
||||
):
|
||||
conversation = (
|
||||
db.query(Conversation).filter(Conversation.id == conversation_id).first()
|
||||
)
|
||||
if not conversation:
|
||||
raise HTTPException(status_code=404, detail="Conversation not found")
|
||||
|
||||
if conversation.user_id and conversation.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
if not current_user:
|
||||
anonymous_token = request.cookies.get("anonymous_token") if request else None
|
||||
if anonymous_token:
|
||||
RateLimiter.check_anonymous_bot_limit(db, anonymous_token)
|
||||
|
||||
bot = db.query(Bot).filter(Bot.id == bot_id).first()
|
||||
if not bot:
|
||||
raise HTTPException(status_code=404, detail="Bot not found")
|
||||
|
||||
if current_user and bot.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized to use this bot")
|
||||
|
||||
conversation.bot_id = bot_id
|
||||
db.commit()
|
||||
|
||||
if not current_user and request:
|
||||
anonymous_token = request.cookies.get("anonymous_token")
|
||||
if anonymous_token:
|
||||
RateLimiter.set_bot_created(db, anonymous_token)
|
||||
|
||||
return {"status": "updated", "bot_id": bot_id}
|
||||
|
||||
|
||||
@router.post("/{conversation_id}/chat")
|
||||
def chat_in_conversation(
|
||||
conversation_id: str,
|
||||
message: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
request: Request = None,
|
||||
response: Response = None,
|
||||
):
|
||||
conversation = (
|
||||
db.query(Conversation).filter(Conversation.id == conversation_id).first()
|
||||
)
|
||||
if not conversation:
|
||||
raise HTTPException(status_code=404, detail="Conversation not found")
|
||||
|
||||
if conversation.user_id and conversation.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
warning = None
|
||||
|
||||
if not current_user:
|
||||
RateLimiter.check_system_limit(db)
|
||||
|
||||
anon_token = get_or_create_anonymous_token(request, response, db)
|
||||
|
||||
anon = RateLimiter.check_anonymous_limit(db, anon_token)
|
||||
|
||||
RateLimiter.increment_chat_count(db, anon_token)
|
||||
|
||||
if anon and anon.chat_count > 40:
|
||||
warning = "Your progress is not saved."
|
||||
|
||||
conversation_history = (
|
||||
db.query(Message)
|
||||
.filter(Message.conversation_id == conversation_id)
|
||||
.order_by(Message.created_at)
|
||||
.all()
|
||||
)
|
||||
history_for_agent = [
|
||||
{"role": msg.role, "content": msg.content} for msg in conversation_history[-10:]
|
||||
]
|
||||
|
||||
if not conversation.bot_id:
|
||||
return {
|
||||
"response": "No bot selected for this conversation. Please set a bot first.",
|
||||
"thinking": None,
|
||||
"strategy_config": None,
|
||||
"success": False,
|
||||
"warning": warning,
|
||||
}
|
||||
|
||||
agent = get_conversational_agent(bot_id=conversation.bot_id)
|
||||
result = agent.chat(message, history_for_agent)
|
||||
|
||||
assistant_content = result.get("response", "I couldn't process your request.")
|
||||
|
||||
user_msg = Message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=message,
|
||||
)
|
||||
db.add(user_msg)
|
||||
|
||||
assistant_msg = Message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=assistant_content,
|
||||
)
|
||||
db.add(assistant_msg)
|
||||
|
||||
conversation.updated_at = conversation.updated_at
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"response": assistant_content,
|
||||
"thinking": result.get("thinking"),
|
||||
"strategy_config": result.get("strategy_config"),
|
||||
"success": result.get("success", False),
|
||||
"warning": warning,
|
||||
}
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy import (
|
||||
ForeignKey,
|
||||
Index,
|
||||
JSON,
|
||||
Integer,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..core.database import Base
|
||||
@@ -30,6 +31,9 @@ class User(Base):
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
bots = relationship("Bot", back_populates="user", cascade="all, delete-orphan")
|
||||
conversations = relationship(
|
||||
"Conversation", back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class Bot(Base):
|
||||
@@ -47,7 +51,7 @@ class Bot(Base):
|
||||
|
||||
user = relationship("User", back_populates="bots")
|
||||
conversations = relationship(
|
||||
"BotConversation", back_populates="bot", cascade="all, delete-orphan"
|
||||
"Conversation", back_populates="bot", cascade="all, delete-orphan"
|
||||
)
|
||||
backtests = relationship(
|
||||
"Backtest", back_populates="bot", cascade="all, delete-orphan"
|
||||
@@ -58,6 +62,47 @@ class Bot(Base):
|
||||
signals = relationship("Signal", back_populates="bot", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
__tablename__ = "conversations"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
user_id = Column(String, ForeignKey("users.id"), nullable=True)
|
||||
anonymous_token = Column(String(64), nullable=True)
|
||||
bot_id = Column(String, ForeignKey("bots.id"), nullable=True)
|
||||
title = Column(String(255), default="New Conversation")
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
user = relationship("User", back_populates="conversations")
|
||||
bot = relationship("Bot", back_populates="conversations")
|
||||
messages = relationship(
|
||||
"Message", back_populates="conversation", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
conversation_id = Column(String, ForeignKey("conversations.id"), nullable=True)
|
||||
role = Column(String, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
conversation = relationship("Conversation", back_populates="messages")
|
||||
|
||||
|
||||
class AnonymousUser(Base):
|
||||
__tablename__ = "anonymous_users"
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
chat_count = Column(Integer, default=0)
|
||||
bot_created = Column(Boolean, default=False)
|
||||
backtest_count = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class BotConversation(Base):
|
||||
__tablename__ = "bot_conversations"
|
||||
|
||||
@@ -118,6 +163,9 @@ class Signal(Base):
|
||||
|
||||
|
||||
Index("idx_bots_user_id", Bot.user_id)
|
||||
Index("idx_conversations_user_id", Conversation.user_id)
|
||||
Index("idx_conversations_bot_id", Conversation.bot_id)
|
||||
Index("idx_messages_conversation_id", Message.conversation_id)
|
||||
Index("idx_conversations_bot_id", BotConversation.bot_id)
|
||||
Index("idx_backtests_bot_id", Backtest.bot_id)
|
||||
Index("idx_simulations_bot_id", Simulation.bot_id)
|
||||
|
||||
@@ -242,3 +242,57 @@ class AveChainSwapRequest(BaseModel):
|
||||
class AveChainSwapResponse(BaseModel):
|
||||
swap: Optional[dict] = None
|
||||
upsell_message: Optional[str] = None
|
||||
|
||||
|
||||
class ConversationResponse(BaseModel):
|
||||
id: str
|
||||
user_id: Optional[str]
|
||||
anonymous_token: Optional[str]
|
||||
bot_id: Optional[str]
|
||||
title: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
id: str
|
||||
conversation_id: Optional[str]
|
||||
role: str
|
||||
content: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ConversationWithMessagesResponse(BaseModel):
|
||||
id: str
|
||||
user_id: Optional[str]
|
||||
anonymous_token: Optional[str]
|
||||
bot_id: Optional[str]
|
||||
title: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
messages: List[MessageResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SetBotRequest(BaseModel):
|
||||
bot_id: str
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
response: str
|
||||
thinking: Optional[str] = None
|
||||
strategy_config: Optional[dict] = None
|
||||
success: bool = False
|
||||
warning: Optional[str] = None
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from .api import auth, bots, backtest, simulate, config, ave
|
||||
from .api import auth, bots, backtest, simulate, config, ave, conversations
|
||||
from .core.limiter import limiter
|
||||
from .core.database import engine, Base
|
||||
|
||||
@@ -15,7 +15,17 @@ logger = logging.getLogger(__name__)
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Initialize database on startup."""
|
||||
# Import all models to ensure they're registered
|
||||
from .db.models import User, Bot, BotConversation, Backtest, Simulation, Signal
|
||||
from .db.models import (
|
||||
User,
|
||||
Bot,
|
||||
BotConversation,
|
||||
Backtest,
|
||||
Simulation,
|
||||
Signal,
|
||||
Conversation,
|
||||
Message,
|
||||
AnonymousUser,
|
||||
)
|
||||
|
||||
# Create tables if they don't exist
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@@ -44,6 +54,9 @@ app.add_middleware(
|
||||
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||||
app.include_router(bots.router, prefix="/api/bots", tags=["bots"])
|
||||
app.include_router(
|
||||
conversations.router, prefix="/api/conversations", tags=["conversations"]
|
||||
)
|
||||
app.include_router(backtest.router, prefix="/api", tags=["backtest"])
|
||||
app.include_router(simulate.router, prefix="/api", tags=["simulate"])
|
||||
app.include_router(config.router, prefix="/api/config", tags=["config"])
|
||||
|
||||
@@ -1,4 +1,29 @@
|
||||
"""AI Agent module for conversational trading."""
|
||||
|
||||
from .agent import ConversationalAgent, get_conversational_agent
|
||||
from .client import MiniMaxClient
|
||||
from .tools import get_tool_registry, TOOL_REGISTRY
|
||||
from .help import (
|
||||
format_tools_list,
|
||||
format_general_help,
|
||||
format_tool_help,
|
||||
format_skill_acknowledgment,
|
||||
)
|
||||
from .crew import TradingCrew, get_trading_crew
|
||||
from .llm_connector import MiniMaxLLM, MiniMaxConnector
|
||||
|
||||
__all__ = ["TradingCrew", "get_trading_crew", "MiniMaxLLM", "MiniMaxConnector"]
|
||||
__all__ = [
|
||||
"ConversationalAgent",
|
||||
"get_conversational_agent",
|
||||
"MiniMaxClient",
|
||||
"get_tool_registry",
|
||||
"TOOL_REGISTRY",
|
||||
"format_tools_list",
|
||||
"format_general_help",
|
||||
"format_tool_help",
|
||||
"format_skill_acknowledgment",
|
||||
"TradingCrew",
|
||||
"get_trading_crew",
|
||||
"MiniMaxLLM",
|
||||
"MiniMaxConnector",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
308
src/backend/app/services/ai_agent/client.py
Normal file
308
src/backend/app/services/ai_agent/client.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""MiniMax API client for the conversational agent."""
|
||||
|
||||
import requests
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """You are a helpful AI trading assistant named Randebu. You help users manage their trading bots.
|
||||
|
||||
IMPORTANT CHAIN LIMITATION:
|
||||
- We ONLY support BSC (Binance Smart Chain) blockchain
|
||||
- If user asks about any other chain (Solana, ETH, Base, etc.), respond with: "Currently we only support BSC (Binance Smart Chain). All trading strategies and token searches are performed on BSC."
|
||||
- Never search or recommend tokens on other chains
|
||||
- The search_tokens tool defaults to BSC, never change this
|
||||
|
||||
Your response must be valid JSON with exactly this structure:
|
||||
{
|
||||
"thinking": "Your internal reasoning and analysis (what you're thinking about)",
|
||||
"response": "Your actual response to the user (be concise and helpful)",
|
||||
"strategy_update": null or {
|
||||
"conditions": [{"type": "price_drop" | "price_rise" | "volume_spike" | "price_level", "token": "TOKEN_SYMBOL", "token_address": null, "threshold": number, ...}],
|
||||
"actions": [{"type": "buy" | "sell" | "hold", "amount_percent": number, ...}],
|
||||
"risk_management": {"stop_loss_percent": number, "take_profit_percent": number}
|
||||
}
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- "thinking" should be detailed reasoning about the user's request
|
||||
- "response" should be conversational and clear
|
||||
- "strategy_update" should be populated ONLY when the user provides specific trading parameters (percentages, tokens, conditions, etc.)
|
||||
- IMPORTANT: When a token is mentioned, set "token_address": null and ask user to confirm the token address before saving. Your response should say something like: "I need to confirm the token address. Could you provide the contract address for [TOKEN]?"
|
||||
- If no strategy parameters are provided, set "strategy_update" to null
|
||||
- Be friendly, concise, and helpful in your response
|
||||
|
||||
Example 1 (no strategy update):
|
||||
User: "What can this bot do?"
|
||||
{
|
||||
"thinking": "The user is asking about the bot's capabilities. I should explain the main features.",
|
||||
"response": "Randebu is your AI trading assistant! It can monitor cryptocurrency prices and execute trades based on your configured strategies. Tell me your trading parameters and I'll set them up for you.",
|
||||
"strategy_update": null
|
||||
}
|
||||
|
||||
Example 2 (token needs confirmation):
|
||||
User: "I want to buy PEPE when it drops 10%"
|
||||
{
|
||||
"thinking": "User wants to buy PEPE. I need the token contract address to proceed. I should ask for confirmation.",
|
||||
"response": "I'd be happy to set up a buy order for PEPE! However, I need to confirm the token contract address. Could you provide the BSC contract address for PEPE? (It usually starts with 0x...)",
|
||||
"strategy_update": {
|
||||
"conditions": [{"type": "price_drop", "token": "PEPE", "token_address": null, "threshold": 10}],
|
||||
"actions": [{"type": "buy", "amount_percent": 100}],
|
||||
"risk_management": null
|
||||
}
|
||||
}
|
||||
|
||||
Example 3 (with token address provided by user):
|
||||
User: "Buy 0x6982508145454Ce125dDE157d8d64a26D53f60a2 when it drops 10%"
|
||||
{
|
||||
"thinking": "User provided a contract address, I can use it directly.",
|
||||
"response": "Perfect! I've configured your strategy to buy the token when it drops 10%.",
|
||||
"strategy_update": {
|
||||
"conditions": [{"type": "price_drop", "token": "TOKEN", "token_address": "0x6982508145454Ce125dDE157d8d64a26D53f60a2", "threshold": 10}],
|
||||
"actions": [{"type": "buy", "amount_percent": 100}],
|
||||
"risk_management": null
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_tokens",
|
||||
"description": "Search for tokens by keyword on BSC blockchain. Use this when user asks to search for a specific token or find tokens by name/symbol.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"description": "Token symbol or name to search for (e.g., 'PEPE', 'BTC')",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of tokens to return (default: 10)",
|
||||
"default": 10,
|
||||
},
|
||||
},
|
||||
"required": ["keyword"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_token",
|
||||
"description": "Get detailed information about a specific token including price, market cap, and pairs. Use when user asks for token details or wants to find a specific token by contract address.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "Token contract address (e.g., '0x6982508145454Ce125dDE157d8d64a26D53f60a2')",
|
||||
},
|
||||
"chain": {
|
||||
"type": "string",
|
||||
"description": "Blockchain chain (default: bsc)",
|
||||
"default": "bsc",
|
||||
},
|
||||
},
|
||||
"required": ["address"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_price",
|
||||
"description": "Get current price(s) for tokens. Use when user asks for token price or wants to compare prices of multiple tokens.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token_ids": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of token IDs with chain suffix (e.g., 'PEPE-bsc,TRUMP-bsc')",
|
||||
}
|
||||
},
|
||||
"required": ["token_ids"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_risk",
|
||||
"description": "Get risk analysis for a token contract. Use when user asks about token risk, honeypot analysis, or safety assessment before trading.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "Token contract address (e.g., '0x6982508145454Ce125dDE157d8d64a26D53f60a2')",
|
||||
},
|
||||
"chain": {
|
||||
"type": "string",
|
||||
"description": "Blockchain chain (default: bsc)",
|
||||
"default": "bsc",
|
||||
},
|
||||
},
|
||||
"required": ["address"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_trending",
|
||||
"description": "Get trending tokens on a blockchain. Use when user asks what's trending, top tokens, or popular tokens right now.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chain": {
|
||||
"type": "string",
|
||||
"description": "Blockchain chain (default: bsc)",
|
||||
"default": "bsc",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of trending tokens to return (default: 10, max: 50)",
|
||||
"default": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_backtest",
|
||||
"description": "Run a backtest to evaluate how the current trading strategy would have performed historically. Returns key metrics like ROI, win rate, max drawdown, etc. Use this when user asks to backtest, test strategy, or check historical performance.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token_address": {
|
||||
"type": "string",
|
||||
"description": "The BSC contract address of the token to backtest (required)",
|
||||
},
|
||||
"timeframe": {
|
||||
"type": "string",
|
||||
"description": "Timeframe for klines: '1d' (1 day), '4h' (4 hours), '1h' (1 hour), '15m' (15 minutes)",
|
||||
"default": "1d",
|
||||
},
|
||||
"start_date": {
|
||||
"type": "string",
|
||||
"description": "Start date for backtest in YYYY-MM-DD format (e.g., '2024-01-01')",
|
||||
},
|
||||
"end_date": {
|
||||
"type": "string",
|
||||
"description": "End date for backtest in YYYY-MM-DD format (e.g., '2024-12-01')",
|
||||
},
|
||||
},
|
||||
"required": ["token_address"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "manage_simulation",
|
||||
"description": "Manage trading simulations: start, stop, or check status. Simulations run on real-time klines and show live portfolio updates. Use when user asks to run simulation, check simulation status, or stop simulation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["start", "stop", "status", "results"],
|
||||
"description": "Action to perform: 'start' (begin new simulation), 'stop' (stop running simulation), 'status' (check if simulation is running), 'results' (get results from current or latest simulation)",
|
||||
},
|
||||
"token_address": {
|
||||
"type": "string",
|
||||
"description": "Token contract address for simulation (required for 'start' action)",
|
||||
},
|
||||
"kline_interval": {
|
||||
"type": "string",
|
||||
"description": "Kline interval: '1m', '5m', '15m', '1h' (default: '1m')",
|
||||
"default": "1m",
|
||||
},
|
||||
},
|
||||
"required": ["action"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
SYSTEM_PROMPT_WITH_TOOLS = (
|
||||
SYSTEM_PROMPT
|
||||
+ """
|
||||
|
||||
You have access to tools:
|
||||
- search_tokens(keyword, limit): Search for tokens by keyword. Use it when user asks to search for a token or find tokens by name/symbol.
|
||||
- get_token(address, chain): Get detailed information about a specific token. Use when user asks for token details.
|
||||
- get_price(token_ids): Get current price(s) for tokens. Use when user asks for token price.
|
||||
- get_risk(address, chain): Get risk analysis for a token. Use when user asks about token safety or honeypot analysis.
|
||||
- get_trending(chain, limit): Get trending tokens on a blockchain. Use when user asks what's trending, top tokens, or popular tokens.
|
||||
- run_backtest(token_address, timeframe, start_date, end_date): Run a backtest on historical data. Returns performance metrics. Use when user asks to backtest or check historical performance.
|
||||
- manage_simulation(action, token_address, kline_interval): Manage trading simulations. Actions: 'start' (begin new), 'stop' (stop running), 'status' (check if running), 'results' (get current/latest results).
|
||||
|
||||
When you want to use a tool, respond with:
|
||||
{
|
||||
"thinking": "...",
|
||||
"response": "Running backtest...",
|
||||
"tool_call": {"name": "run_backtest", "arguments": {"token_address": "0x...", "timeframe": "1d", "start_date": "2024-01-01", "end_date": "2024-12-01"}}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class MiniMaxClient:
|
||||
"""Client for MiniMax extended thinking API."""
|
||||
|
||||
def __init__(self, api_key: str, model: str = "MiniMax-M2.7"):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.endpoint = "https://api.minimax.io/v1/text/chatcompletion_v2"
|
||||
|
||||
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]:
|
||||
"""Send a chat request to MiniMax API."""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
all_messages = [{"role": "system", "content": system_prompt}] + messages
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": all_messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"thinking": {"type": "human", "budget_tokens": thinking_budget},
|
||||
}
|
||||
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
|
||||
resp = requests.post(self.endpoint, headers=headers, json=payload)
|
||||
return resp.json() or {}
|
||||
|
||||
def check_connection(self) -> bool:
|
||||
"""Check if API is reachable."""
|
||||
try:
|
||||
resp = requests.post(
|
||||
self.endpoint,
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": [{"role": "user", "content": "ping"}],
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
return resp.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
83
src/backend/app/services/ai_agent/help.py
Normal file
83
src/backend/app/services/ai_agent/help.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Help formatters for slash commands and tool documentation."""
|
||||
|
||||
from typing import Optional
|
||||
from .tools import get_tool_registry, SKILL_EMOJIS
|
||||
|
||||
|
||||
def format_tools_list() -> str:
|
||||
"""Format the tool registry as a help message."""
|
||||
message = "📋 Available Tools\n\n"
|
||||
|
||||
for category in ["randebu", "ave"]:
|
||||
tools = get_tool_registry().get(category, [])
|
||||
if category == "randebu":
|
||||
message += "🤖 Randebu Built-in:\n"
|
||||
else:
|
||||
message += "☁️ AVE Cloud Skills:\n"
|
||||
|
||||
for tool in tools:
|
||||
message += f" • {tool['command']} - {tool['description']}\n"
|
||||
message += "\n"
|
||||
|
||||
message = (
|
||||
message.rstrip() + "\n\nType /<tool-name> for detailed help on a specific tool."
|
||||
)
|
||||
return message
|
||||
|
||||
|
||||
def format_skill_acknowledgment(tool_name: str, description: str) -> str:
|
||||
"""Format a brief acknowledgment when a skill is activated."""
|
||||
emoji = SKILL_EMOJIS.get(tool_name.lower(), "✨")
|
||||
return f"{emoji} **{tool_name}** loaded. Ready for *{description}*, ask me away!"
|
||||
|
||||
|
||||
def format_tool_help(tool_name: str) -> str:
|
||||
"""Format detailed help for a specific tool."""
|
||||
tool_name = tool_name.lstrip("/")
|
||||
|
||||
for category in ["randebu", "ave"]:
|
||||
for tool in get_tool_registry().get(category, []):
|
||||
if tool["name"].lower() == tool_name.lower():
|
||||
cat_label = (
|
||||
"Randebu Built-in" if category == "randebu" else "AVE Cloud Skill"
|
||||
)
|
||||
details = tool["details"]
|
||||
message = (
|
||||
f"🔍 {tool['command']} - {details['description']} ({cat_label})\n\n"
|
||||
)
|
||||
message += f"**Description:** {details['description']}\n"
|
||||
message += f"**Commands:**\n {details['usage']}\n\n"
|
||||
message += f"**Example:**\n```\n{details['example']}\n```"
|
||||
return message
|
||||
|
||||
return f"Tool '{tool_name}' not found. Type / to see all available tools."
|
||||
|
||||
|
||||
def format_general_help() -> str:
|
||||
"""Format general help about Randebu."""
|
||||
return """🤖 **Randebu - AI Trading Assistant**
|
||||
|
||||
Randebu is your AI trading assistant that helps you manage your trading bots on BSC (Binance Smart Chain).
|
||||
|
||||
**Getting Started:**
|
||||
1. Create a bot on the dashboard
|
||||
2. Describe your trading strategy in plain English
|
||||
3. Run backtests to validate your strategy
|
||||
4. Start simulations to see live trading
|
||||
|
||||
**Example Strategies:**
|
||||
- "Buy PEPE when it drops 5%"
|
||||
- "Sell if price rises 10% within 1 hour"
|
||||
- "Buy when volume spikes by 200%"
|
||||
|
||||
**Slash Commands:**
|
||||
- `/` - Show all available tools
|
||||
- `/help` - Show this help message
|
||||
- `/<tool-name>` - Get help on a specific tool
|
||||
|
||||
**Natural Language:**
|
||||
You can also just describe what you want in natural language. For example:
|
||||
- "What's the price of PEPE?"
|
||||
- "Run a backtest on 0x... token"
|
||||
- "Start a simulation on TRUMP"
|
||||
"""
|
||||
172
src/backend/app/services/ai_agent/tools.py
Normal file
172
src/backend/app/services/ai_agent/tools.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Tool registry and definitions for the conversational agent."""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
|
||||
TOOL_REGISTRY: Dict[str, Any] = {
|
||||
"randebu": [
|
||||
{
|
||||
"name": "backtest",
|
||||
"description": "Run strategy backtest",
|
||||
"category": "Randebu Built-in",
|
||||
"command": "/backtest",
|
||||
"details": {
|
||||
"description": "Run a backtest to evaluate how the current trading strategy would have performed historically.",
|
||||
"usage": "/backtest [token_address] [--timeframe 1d|4h|1h|15m] [--start YYYY-MM-DD] [--end YYYY-MM-DD]",
|
||||
"example": "Run a backtest on PEPE for the last 30 days",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "simulate",
|
||||
"description": "Start/stop simulation",
|
||||
"category": "Randebu Built-in",
|
||||
"command": "/simulate",
|
||||
"details": {
|
||||
"description": "Start or stop trading simulations that run on real-time klines.",
|
||||
"usage": "/simulate start|stop|status|results [token_address]",
|
||||
"example": "Start a simulation on PEPE",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "strategy",
|
||||
"description": "View/update strategy",
|
||||
"category": "Randebu Built-in",
|
||||
"command": "/strategy",
|
||||
"details": {
|
||||
"description": "View your current trading strategy or update it with new parameters.",
|
||||
"usage": "Describe your strategy in plain English, e.g., 'Buy PEPE when price drops 5%'",
|
||||
"example": "Buy PEPE when it drops 10% within 1 hour",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "create_bot",
|
||||
"description": "Create a new trading bot",
|
||||
"category": "Randebu Built-in",
|
||||
"command": None,
|
||||
"details": {
|
||||
"description": "Create a new trading bot linked to the current conversation.",
|
||||
"usage": "create_bot <name> [--strategy <strategy_desc>]",
|
||||
"example": "create_bot MyBot --strategy Buy PEPE when it drops 5%",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "list_bots",
|
||||
"description": "List your trading bots",
|
||||
"category": "Randebu Built-in",
|
||||
"command": None,
|
||||
"details": {
|
||||
"description": "List all trading bots you own.",
|
||||
"usage": "list_bots",
|
||||
"example": "list_bots",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "set_bot",
|
||||
"description": "Set bot for this conversation",
|
||||
"category": "Randebu Built-in",
|
||||
"command": None,
|
||||
"details": {
|
||||
"description": "Associate a bot with the current conversation.",
|
||||
"usage": "set_bot <bot_id>",
|
||||
"example": "set_bot abc-123-def",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "get_bot_info",
|
||||
"description": "Get current bot details",
|
||||
"category": "Randebu Built-in",
|
||||
"command": None,
|
||||
"details": {
|
||||
"description": "Get details of the current bot for display in the right pane.",
|
||||
"usage": "get_bot_info [bot_id]",
|
||||
"example": "get_bot_info abc-123-def",
|
||||
},
|
||||
},
|
||||
],
|
||||
"ave": [
|
||||
{
|
||||
"name": "search",
|
||||
"description": "Token search",
|
||||
"category": "AVE Cloud Skills",
|
||||
"command": "/search",
|
||||
"details": {
|
||||
"description": "Find tokens by keyword, symbol, or contract address on BSC.",
|
||||
"usage": "search <keyword> [--chain bsc] [--limit 20]",
|
||||
"example": "search PEPE\nsearch 0x1234... --chain bsc",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "trending",
|
||||
"description": "Popular tokens",
|
||||
"category": "AVE Cloud Skills",
|
||||
"command": "/trending",
|
||||
"details": {
|
||||
"description": "Get list of trending/popular tokens on BSC.",
|
||||
"usage": "trending [--chain bsc] [--limit 20]",
|
||||
"example": "trending --chain bsc\ntrending --limit 10",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "risk",
|
||||
"description": "Honeypot detection",
|
||||
"category": "AVE Cloud Skills",
|
||||
"command": "/risk",
|
||||
"details": {
|
||||
"description": "Get risk analysis for a token contract including honeypot assessment.",
|
||||
"usage": "risk <token_address> [--chain bsc]",
|
||||
"example": "risk 0x6982508145454Ce125dDE157d8d64a26D53f60a2",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"description": "Token details",
|
||||
"category": "AVE Cloud Skills",
|
||||
"command": "/token",
|
||||
"details": {
|
||||
"description": "Get detailed information about a specific token including price, market cap, and pairs.",
|
||||
"usage": "token <address> [--chain bsc]",
|
||||
"example": "token 0x6982508145454Ce125dDE157d8d64a26D53f60a2",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"description": "Batch prices",
|
||||
"category": "AVE Cloud Skills",
|
||||
"command": "/price",
|
||||
"details": {
|
||||
"description": "Get current price(s) for multiple tokens.",
|
||||
"usage": "price <token_id>,<token_id>,... (e.g., PEPE-bsc,TRUMP-bsc)",
|
||||
"example": "price PEPE-bsc,TRUMP-bsc",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
SKILL_EMOJIS: Dict[str, str] = {
|
||||
"backtest": "📊",
|
||||
"simulate": "🎮",
|
||||
"strategy": "📝",
|
||||
"search": "🔍",
|
||||
"trending": "📈",
|
||||
"risk": "📉",
|
||||
"token": "🪙",
|
||||
"price": "💰",
|
||||
}
|
||||
|
||||
|
||||
def get_tool_registry() -> Dict[str, Any]:
|
||||
"""Return the tool registry for slash command help."""
|
||||
return TOOL_REGISTRY
|
||||
|
||||
|
||||
def get_tools_by_category(category: str) -> List[Dict[str, Any]]:
|
||||
"""Get tools filtered by category."""
|
||||
return TOOL_REGISTRY.get(category, [])
|
||||
|
||||
|
||||
def get_tool_by_name(tool_name: str) -> Dict[str, Any]:
|
||||
"""Get a tool by its name."""
|
||||
for category in ["randebu", "ave"]:
|
||||
for tool in TOOL_REGISTRY.get(category, []):
|
||||
if tool["name"].lower() == tool_name.lower():
|
||||
return tool
|
||||
return None
|
||||
95
src/backend/app/services/rate_limiter.py
Normal file
95
src/backend/app/services/rate_limiter.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func
|
||||
from fastapi import HTTPException
|
||||
from ..db.models import Message, AnonymousUser
|
||||
|
||||
MAX_CHATS_PER_5HOURS = int(os.getenv("MAX_CHATS_PER_5HOURS", "500"))
|
||||
MAX_ANONYMOUS_CHATS = 50
|
||||
MAX_ANONYMOUS_BOTS = 1
|
||||
MAX_ANONYMOUS_BACKTESTS = 1
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
@staticmethod
|
||||
def check_system_limit(db):
|
||||
cutoff = datetime.utcnow() - timedelta(hours=5)
|
||||
count = (
|
||||
db.query(func.count(Message.id))
|
||||
.filter(Message.created_at >= cutoff)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
if count >= MAX_CHATS_PER_5HOURS:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="Rate limited from the agent service. Please come back later.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_anonymous_limit(db, anonymous_token: str):
|
||||
anon = (
|
||||
db.query(AnonymousUser).filter(AnonymousUser.id == anonymous_token).first()
|
||||
)
|
||||
|
||||
if anon and anon.chat_count >= MAX_ANONYMOUS_CHATS:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You've reached the limit. Please create an account to continue.",
|
||||
)
|
||||
|
||||
return anon
|
||||
|
||||
@staticmethod
|
||||
def check_anonymous_bot_limit(db, anonymous_token: str):
|
||||
anon = (
|
||||
db.query(AnonymousUser).filter(AnonymousUser.id == anonymous_token).first()
|
||||
)
|
||||
|
||||
if anon and anon.bot_created:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You've reached the limit. Please create an account to continue.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_anonymous_backtest_limit(db, anonymous_token: str):
|
||||
anon = (
|
||||
db.query(AnonymousUser).filter(AnonymousUser.id == anonymous_token).first()
|
||||
)
|
||||
|
||||
if anon and anon.backtest_count >= MAX_ANONYMOUS_BACKTESTS:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You've reached the limit. Please create an account to continue.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def increment_chat_count(db, anonymous_token: str):
|
||||
anon = (
|
||||
db.query(AnonymousUser).filter(AnonymousUser.id == anonymous_token).first()
|
||||
)
|
||||
|
||||
if anon:
|
||||
anon.chat_count += 1
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def set_bot_created(db, anonymous_token: str):
|
||||
anon = (
|
||||
db.query(AnonymousUser).filter(AnonymousUser.id == anonymous_token).first()
|
||||
)
|
||||
|
||||
if anon:
|
||||
anon.bot_created = True
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def increment_backtest_count(db, anonymous_token: str):
|
||||
anon = (
|
||||
db.query(AnonymousUser).filter(AnonymousUser.id == anonymous_token).first()
|
||||
)
|
||||
|
||||
if anon:
|
||||
anon.backtest_count += 1
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user