[Redesign] Backend - Conversation-based Chat System #59

Open
opened 2026-04-13 12:51:26 +02:00 by shoko · 0 comments
Owner

Detailed Implementation Guide for Backend Chat System

Summary

Redesign backend to support conversation-based chat system with anonymous and authenticated users.


Current State (Before)

  • Chat tied to single bot
  • No conversation model
  • No anonymous user support
  • No rate limiting

Target State (After)

  • Conversations exist independently of bots
  • Anonymous users can chat (with limits)
  • Bot linked to conversation, not chat session
  • System-wide rate limiting

Implementation Phases

Phase 1: Database Models

1.1 Conversation Model

Create src/backend/app/db/models/conversation.py:

from sqlalchemy import Column, String, DateTime, ForeignKey, Text
from sqlalchemy.orm import relationship
from datetime import datetime
import uuid

class Conversation(Base):
    __tablename__ = "conversations"
    
    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    user_id = Column(String(36), ForeignKey("users.id"), nullable=True)  # NULL for anonymous
    anonymous_token = Column(String(64), nullable=True)  # Cookie-based ID
    bot_id = Column(String(36), ForeignKey("bots.id"), nullable=True)  # Associated bot
    title = Column(String(255), default="New Conversation")
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relationships
    user = relationship("User", back_populates="conversations")
    bot = relationship("Bot", back_populates="conversations")
    messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")

Check: Verify UUID import works, no circular imports with User/Bot models


1.2 Update Message Model

Add to existing Message model:

# In existing Message model
conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=True)
conversation = relationship("Conversation", back_populates="messages")

Check: Run alembic migration to create new tables


1.3 Anonymous Tracking Model

Create src/backend/app/db/models/anonymous_user.py:

class AnonymousUser(Base):
    __tablename__ = "anonymous_users"
    
    id = Column(String(64), primary_key=True)  # Cookie token
    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)

Migration needed: alembic revision --autogenerate -m "add conversation and anonymous user tables"


Phase 2: API Endpoints

2.1 Conversation Endpoints

Create src/backend/app/api/conversations.py:

from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from typing import List, Optional
from ...db.database import get_db
from ...db.models import Conversation, Message, User, AnonymousUser
from ...services.auth import get_current_user

router = APIRouter(prefix="/api/conversations", tags=["conversations"])

@router.get("")
def list_conversations(
    db: Session = Depends(get_db),
    current_user: Optional[User] = Depends(get_current_user)
):
    """List all conversations for 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
):
    """Create a new conversation."""
    # Handle anonymous vs logged in
    anonymous_token = request.cookies.get("anonymous_token") if request else None
    
    conversation = Conversation(
        user_id=current_user.id if current_user else None,
        anonymous_token=anonymous_token
    )
    db.add(conversation)
    db.commit()
    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)
):
    """Get conversation with messages."""
    conversation = db.query(Conversation).filter(Conversation.id == conversation_id).first()
    if not conversation:
        raise HTTPException(status_code=404, detail="Conversation not found")
    
    # Verify access
    if conversation.user_id and conversation.user_id != current_user.id:
        raise HTTPException(status_code=403, detail="Access denied")
    
    return conversation

@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
):
    """Send a message in a conversation."""
    # 1. Check rate limits (see Phase 3)
    # 2. Check anonymous limits (see Phase 3)
    # 3. Process message via ConversationalAgent
    # 4. Save message to database
    # 5. Return response

2.2 Bot Management Tools (for Assistant)

Add to src/backend/app/services/ai_agent/agent.py:

# New tools for the agent

async def create_bot_tool(conversation_id: str, strategy: str, chain: str = "bsc") -> dict:
    """Create a new trading bot linked to conversation."""
    # Check anonymous limit: max 1 bot
    # Create bot
    # Link bot to conversation
    return {"bot_id": "...", "status": "created"}

async def list_bots_tool(user_id: str) -> dict:
    """List all bots for current user."""
    bots = db.query(Bot).filter(Bot.user_id == user_id).all()
    return {"bots": [{"id": b.id, "name": b.name, "strategy": b.strategy} for b in bots]}

async def set_bot_tool(conversation_id: str, bot_id: str) -> dict:
    """Associate a bot with a conversation."""
    conversation.bot_id = bot_id
    db.commit()
    return {"status": "updated", "bot_id": bot_id}

async def get_bot_info_tool(bot_id: str) -> dict:
    """Get bot details for display in right pane."""
    bot = db.query(Bot).filter(Bot.id == bot_id).first()
    return {
        "name": bot.name,
        "chain": bot.chain,
        "strategy": bot.strategy,
        "status": bot.status
    }

Phase 3: Rate Limiting & Anonymous Limits

3.1 Rate Limiting Service

Create src/backend/app/services/rate_limiter.py:

import os
from datetime import datetime, timedelta
from sqlalchemy import func
from ..db.database import get_db
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):
        """Check if system-wide 5-hour limit exceeded."""
        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):
        """Check anonymous user limits."""
        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 increment_chat_count(db, anonymous_token: str):
        """Increment chat count for anonymous user."""
        anon = db.query(AnonymousUser).filter(
            AnonymousUser.id == anonymous_token
        ).first()
        
        if anon:
            anon.chat_count += 1
            db.commit()

3.2 Anonymous Token Management

In src/backend/app/api/conversations.py:

def get_or_create_anonymous_token(response: Response) -> str:
    """Get existing anonymous token or create new one."""
    token = request.cookies.get("anonymous_token")
    if not token:
        import secrets
        token = secrets.token_urlsafe(32)
        response.set_cookie(
            key="anonymous_token",
            value=token,
            max_age=60*60*24*365,  # 1 year
            httponly=True
        )
        
        # Create anonymous user record
        anon = AnonymousUser(id=token)
        db.add(anon)
        db.commit()
    return token

3.3 Anonymous Chat Flow

@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
):
    # 1. If not logged in → handle anonymous
    if not current_user:
        # Check system limit
        RateLimiter.check_system_limit(db)
        
        # Get/create anonymous token
        anon_token = get_or_create_anonymous_token(response, db)
        
        # Check anonymous limits
        anon = RateLimiter.check_anonymous_limit(db, anon_token)
        
        # Increment count
        RateLimiter.increment_chat_count(db, anon_token)
        
        # Add warning to response if needed
        warning = "Your progress is not saved." if anon.chat_count > 40 else None
    
    # 2. Process message with ConversationalAgent
    # ... existing logic ...

Phase 4: Integration with Existing Code

4.1 Update Chat Endpoint

Update existing /api/chat or create new flow that uses Conversation model.

4.2 Update ConversationalAgent

Make sure agent can access conversation context:

class ConversationalAgent:
    def __init__(self, api_key: str, bot_id: str = None, conversation_id: str = None):
        self.bot_id = bot_id
        self.conversation_id = conversation_id  # New
        ...

4.3 Bot Tools Registration

Register the new tools with the agent:

TOOL_REGISTRY = {
    "randebu": [
        # Existing: backtest, simulate, strategy
        # New:
        {
            "name": "create_bot",
            "description": "Create a new trading bot",
            "category": "Randebu Built-in",
            "command": None,  # Not a slash command
        },
        {
            "name": "list_bots",
            "description": "List your trading bots",
            "category": "Randebu Built-in",
            "command": None,
        },
        {
            "name": "set_bot",
            "description": "Set bot for this conversation",
            "category": "Randebu Built-in",
            "command": None,
        },
        {
            "name": "get_bot_info",
            "description": "Get current bot details",
            "category": "Randebu Built-in",
            "command": None,
        },
    ],
    ...
}

What to Check / Verify

Database

Check How
Migration runs alembic upgrade head
Models import python -c "from app.db.models import Conversation, Message, AnonymousUser"
Foreign keys work Test relationship queries

API Endpoints

Check How
Create conversation POST /api/conversations returns 201
List conversations GET /api/conversations returns list
Get with messages GET /api/conversations/{id} includes messages
Delete conversation DELETE /api/conversations/{id} removes it
Chat in conversation POST /api/conversations/{id}/chat works

Rate Limiting

Check How
System limit Manually set MAX_CHATS_PER_5HOURS=1, test 2nd request
Anonymous 50 limit Create 51st message, should fail
Cookie set Response includes Set-Cookie header
Count increments Query anonymous_users table

Anonymous User Limits

Check How
Max 1 bot Try creating 2nd bot, should fail
Max 1 backtest Try 2nd backtest, should fail
No simulation Try simulation, should fail
Warning shown Check response after 40 messages

How to Test

Unit Tests

# tests/test_conversation.py
def test_create_conversation(db):
    conv = create_conversation(db, user_id=None, anonymous_token="test123")
    assert conv.id is not None
    assert conv.title == "New Conversation"

def test_anonymous_limit(db):
    anon = AnonymousUser(id="test", chat_count=50)
    db.add(anon)
    db.commit()
    
    with pytest.raises(HTTPException) as exc:
        RateLimiter.check_anonymous_limit(db, "test")
    assert "limit" in exc.value.detail.lower()

def test_system_rate_limit(db):
    # Set limit to 1
    with mock.patch('MAX_CHATS_PER_5HOURS', 1):
        msg = Message(created_at=datetime.utcnow())
        db.add(msg)
        db.commit()
        
        with pytest.raises(HTTPException) as exc:
            RateLimiter.check_system_limit(db)
        assert "rate limited" in exc.value.detail.lower()

Integration Tests

# tests/test_api/test_conversations.py
def test_list_conversations_authenticated(client, db, test_user):
    # Create conversation
    conv = Conversation(user_id=test_user.id)
    db.add(conv)
    db.commit()
    
    response = client.get(
        "/api/conversations",
        headers={"Authorization": f"Bearer {test_user.token}"}
    )
    assert response.status_code == 200
    assert len(response.json()) == 1

def test_anonymous_chat_limit(client, db):
    # Set up 50 chats
    anon = AnonymousUser(id="test_anon", chat_count=50)
    db.add(anon)
    db.commit()
    
    response = client.post(
        "/api/conversations/abc/chat",
        json={"message": "hello"},
        cookies={"anonymous_token": "test_anon"}
    )
    assert response.status_code == 403

How to Debug

If Conversation Not Found

# Debug: Check if conversation exists
from app.db.database import get_db
db = next(get_db())
conv = db.query(Conversation).filter(Conversation.id == "abc").first()
print(conv)  # None if not found

If Rate Limit Not Working

# Debug: Check message count
from datetime import datetime, timedelta
from sqlalchemy import func
from app.db.models import Message

db = next(get_db())
cutoff = datetime.utcnow() - timedelta(hours=5)
count = db.query(func.count(Message.id)).filter(
    Message.created_at >= cutoff
).scalar()
print(f"Messages in last 5 hours: {count}")

If Anonymous Limits Not Applied

# Debug: Check anonymous user record
anon = db.query(AnonymousUser).filter(
    AnonymousUser.id == "token_value"
).first()
print(f"Chat count: {anon.chat_count if anon else 'No record'}")

Invariants (What NOT to Change)

Item Must Stay Same
Message model Existing fields must remain
Bot model Existing fields must remain
ConversationalAgent.process_message() Signature unchanged
MiniMax API calls Same behavior
Backtest/Simulate tools Same functionality

Files to Create/Modify

Action File
CREATE db/models/conversation.py
CREATE db/models/anonymous_user.py
CREATE services/rate_limiter.py
CREATE api/conversations.py
UPDATE db/models/message.py (add conversation_id)
UPDATE db/models/__init__.py (export new models)
UPDATE main.py (register router)
UPDATE services/ai_agent/tools.py (add bot tools)

Migration Required

alembic revision --autogenerate -m "add conversation and anonymous user tables"
alembic upgrade head

Dependencies


Priority: HIGH


Acceptance Criteria

  • Conversation model created and migrated
  • Message model updated with conversation_id
  • AnonymousUser model created and migrated
  • GET /api/conversations returns user's conversations
  • POST /api/conversations creates new conversation
  • GET /api/conversations/{id} returns with messages
  • POST /api/conversations/{id}/chat processes message
  • DELETE /api/conversations/{id} removes conversation
  • POST /api/conversations/{id}/set-bot links bot
  • Non-logged users can chat max 50 times
  • Non-logged users can create max 1 bot
  • Non-logged users can run max 1 backtest
  • Non-logged users CANNOT run simulation
  • System-wide rate limit (500/5hrs) works
  • "Your progress is not saved" warning shown after 40 messages
  • Rate limit error returns proper message
  • Assistant can use create_bot, list_bots, set_bot, get_bot_info tools
  • All unit tests pass
  • All integration tests pass

Notes for Developer

  1. Start with models — Get database right first
  2. Test incrementally — Don't build everything then test
  3. Use the rate limiter — Don't duplicate limit logic
  4. Keep backward compatibility — Existing /api/chat should still work
  5. Cookie httponly — Security: anonymous_token cookie should be httponly
  6. Handle edge cases — What if bot_id doesn't exist? What if user deletes bot while chatting?

⚠️ Prerequisite

This issue assumes issue #63 (Refactor) is COMPLETED.

The code examples reference the refactored structure:

  • services/ai_agent/agent.py (not conversational.py)
  • services/ai_agent/tools.py (tool registry)
  • services/ai_agent/help.py (help formatters)
  • services/ai_agent/client.py (MiniMax client)

If #63 is not done yet, complete it first.

# Detailed Implementation Guide for Backend Chat System ## Summary Redesign backend to support conversation-based chat system with anonymous and authenticated users. --- ## Current State (Before) - Chat tied to single bot - No conversation model - No anonymous user support - No rate limiting ## Target State (After) - Conversations exist independently of bots - Anonymous users can chat (with limits) - Bot linked to conversation, not chat session - System-wide rate limiting --- # Implementation Phases ## Phase 1: Database Models ### 1.1 Conversation Model Create `src/backend/app/db/models/conversation.py`: ```python from sqlalchemy import Column, String, DateTime, ForeignKey, Text from sqlalchemy.orm import relationship from datetime import datetime import uuid class Conversation(Base): __tablename__ = "conversations" id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) user_id = Column(String(36), ForeignKey("users.id"), nullable=True) # NULL for anonymous anonymous_token = Column(String(64), nullable=True) # Cookie-based ID bot_id = Column(String(36), ForeignKey("bots.id"), nullable=True) # Associated bot title = Column(String(255), default="New Conversation") created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships user = relationship("User", back_populates="conversations") bot = relationship("Bot", back_populates="conversations") messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan") ``` **Check:** Verify UUID import works, no circular imports with User/Bot models --- ### 1.2 Update Message Model Add to existing `Message` model: ```python # In existing Message model conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=True) conversation = relationship("Conversation", back_populates="messages") ``` **Check:** Run alembic migration to create new tables --- ### 1.3 Anonymous Tracking Model Create `src/backend/app/db/models/anonymous_user.py`: ```python class AnonymousUser(Base): __tablename__ = "anonymous_users" id = Column(String(64), primary_key=True) # Cookie token 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) ``` **Migration needed:** `alembic revision --autogenerate -m "add conversation and anonymous user tables"` --- ## Phase 2: API Endpoints ### 2.1 Conversation Endpoints Create `src/backend/app/api/conversations.py`: ```python from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session from typing import List, Optional from ...db.database import get_db from ...db.models import Conversation, Message, User, AnonymousUser from ...services.auth import get_current_user router = APIRouter(prefix="/api/conversations", tags=["conversations"]) @router.get("") def list_conversations( db: Session = Depends(get_db), current_user: Optional[User] = Depends(get_current_user) ): """List all conversations for 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 ): """Create a new conversation.""" # Handle anonymous vs logged in anonymous_token = request.cookies.get("anonymous_token") if request else None conversation = Conversation( user_id=current_user.id if current_user else None, anonymous_token=anonymous_token ) db.add(conversation) db.commit() 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) ): """Get conversation with messages.""" conversation = db.query(Conversation).filter(Conversation.id == conversation_id).first() if not conversation: raise HTTPException(status_code=404, detail="Conversation not found") # Verify access if conversation.user_id and conversation.user_id != current_user.id: raise HTTPException(status_code=403, detail="Access denied") return conversation @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 ): """Send a message in a conversation.""" # 1. Check rate limits (see Phase 3) # 2. Check anonymous limits (see Phase 3) # 3. Process message via ConversationalAgent # 4. Save message to database # 5. Return response ``` --- ### 2.2 Bot Management Tools (for Assistant) Add to `src/backend/app/services/ai_agent/agent.py`: ```python # New tools for the agent async def create_bot_tool(conversation_id: str, strategy: str, chain: str = "bsc") -> dict: """Create a new trading bot linked to conversation.""" # Check anonymous limit: max 1 bot # Create bot # Link bot to conversation return {"bot_id": "...", "status": "created"} async def list_bots_tool(user_id: str) -> dict: """List all bots for current user.""" bots = db.query(Bot).filter(Bot.user_id == user_id).all() return {"bots": [{"id": b.id, "name": b.name, "strategy": b.strategy} for b in bots]} async def set_bot_tool(conversation_id: str, bot_id: str) -> dict: """Associate a bot with a conversation.""" conversation.bot_id = bot_id db.commit() return {"status": "updated", "bot_id": bot_id} async def get_bot_info_tool(bot_id: str) -> dict: """Get bot details for display in right pane.""" bot = db.query(Bot).filter(Bot.id == bot_id).first() return { "name": bot.name, "chain": bot.chain, "strategy": bot.strategy, "status": bot.status } ``` --- ## Phase 3: Rate Limiting & Anonymous Limits ### 3.1 Rate Limiting Service Create `src/backend/app/services/rate_limiter.py`: ```python import os from datetime import datetime, timedelta from sqlalchemy import func from ..db.database import get_db 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): """Check if system-wide 5-hour limit exceeded.""" 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): """Check anonymous user limits.""" 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 increment_chat_count(db, anonymous_token: str): """Increment chat count for anonymous user.""" anon = db.query(AnonymousUser).filter( AnonymousUser.id == anonymous_token ).first() if anon: anon.chat_count += 1 db.commit() ``` --- ### 3.2 Anonymous Token Management In `src/backend/app/api/conversations.py`: ```python def get_or_create_anonymous_token(response: Response) -> str: """Get existing anonymous token or create new one.""" token = request.cookies.get("anonymous_token") if not token: import secrets token = secrets.token_urlsafe(32) response.set_cookie( key="anonymous_token", value=token, max_age=60*60*24*365, # 1 year httponly=True ) # Create anonymous user record anon = AnonymousUser(id=token) db.add(anon) db.commit() return token ``` --- ### 3.3 Anonymous Chat Flow ```python @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 ): # 1. If not logged in → handle anonymous if not current_user: # Check system limit RateLimiter.check_system_limit(db) # Get/create anonymous token anon_token = get_or_create_anonymous_token(response, db) # Check anonymous limits anon = RateLimiter.check_anonymous_limit(db, anon_token) # Increment count RateLimiter.increment_chat_count(db, anon_token) # Add warning to response if needed warning = "Your progress is not saved." if anon.chat_count > 40 else None # 2. Process message with ConversationalAgent # ... existing logic ... ``` --- ## Phase 4: Integration with Existing Code ### 4.1 Update Chat Endpoint Update existing `/api/chat` or create new flow that uses Conversation model. ### 4.2 Update ConversationalAgent Make sure agent can access conversation context: ```python class ConversationalAgent: def __init__(self, api_key: str, bot_id: str = None, conversation_id: str = None): self.bot_id = bot_id self.conversation_id = conversation_id # New ... ``` ### 4.3 Bot Tools Registration Register the new tools with the agent: ```python TOOL_REGISTRY = { "randebu": [ # Existing: backtest, simulate, strategy # New: { "name": "create_bot", "description": "Create a new trading bot", "category": "Randebu Built-in", "command": None, # Not a slash command }, { "name": "list_bots", "description": "List your trading bots", "category": "Randebu Built-in", "command": None, }, { "name": "set_bot", "description": "Set bot for this conversation", "category": "Randebu Built-in", "command": None, }, { "name": "get_bot_info", "description": "Get current bot details", "category": "Randebu Built-in", "command": None, }, ], ... } ``` --- ## What to Check / Verify ### Database | Check | How | |-------|-----| | Migration runs | `alembic upgrade head` | | Models import | `python -c "from app.db.models import Conversation, Message, AnonymousUser"` | | Foreign keys work | Test relationship queries | ### API Endpoints | Check | How | |-------|-----| | Create conversation | POST /api/conversations returns 201 | | List conversations | GET /api/conversations returns list | | Get with messages | GET /api/conversations/{id} includes messages | | Delete conversation | DELETE /api/conversations/{id} removes it | | Chat in conversation | POST /api/conversations/{id}/chat works | ### Rate Limiting | Check | How | |-------|-----| | System limit | Manually set MAX_CHATS_PER_5HOURS=1, test 2nd request | | Anonymous 50 limit | Create 51st message, should fail | | Cookie set | Response includes Set-Cookie header | | Count increments | Query anonymous_users table | ### Anonymous User Limits | Check | How | |-------|-----| | Max 1 bot | Try creating 2nd bot, should fail | | Max 1 backtest | Try 2nd backtest, should fail | | No simulation | Try simulation, should fail | | Warning shown | Check response after 40 messages | --- ## How to Test ### Unit Tests ```python # tests/test_conversation.py def test_create_conversation(db): conv = create_conversation(db, user_id=None, anonymous_token="test123") assert conv.id is not None assert conv.title == "New Conversation" def test_anonymous_limit(db): anon = AnonymousUser(id="test", chat_count=50) db.add(anon) db.commit() with pytest.raises(HTTPException) as exc: RateLimiter.check_anonymous_limit(db, "test") assert "limit" in exc.value.detail.lower() def test_system_rate_limit(db): # Set limit to 1 with mock.patch('MAX_CHATS_PER_5HOURS', 1): msg = Message(created_at=datetime.utcnow()) db.add(msg) db.commit() with pytest.raises(HTTPException) as exc: RateLimiter.check_system_limit(db) assert "rate limited" in exc.value.detail.lower() ``` ### Integration Tests ```python # tests/test_api/test_conversations.py def test_list_conversations_authenticated(client, db, test_user): # Create conversation conv = Conversation(user_id=test_user.id) db.add(conv) db.commit() response = client.get( "/api/conversations", headers={"Authorization": f"Bearer {test_user.token}"} ) assert response.status_code == 200 assert len(response.json()) == 1 def test_anonymous_chat_limit(client, db): # Set up 50 chats anon = AnonymousUser(id="test_anon", chat_count=50) db.add(anon) db.commit() response = client.post( "/api/conversations/abc/chat", json={"message": "hello"}, cookies={"anonymous_token": "test_anon"} ) assert response.status_code == 403 ``` --- ## How to Debug ### If Conversation Not Found ```python # Debug: Check if conversation exists from app.db.database import get_db db = next(get_db()) conv = db.query(Conversation).filter(Conversation.id == "abc").first() print(conv) # None if not found ``` ### If Rate Limit Not Working ```python # Debug: Check message count from datetime import datetime, timedelta from sqlalchemy import func from app.db.models import Message db = next(get_db()) cutoff = datetime.utcnow() - timedelta(hours=5) count = db.query(func.count(Message.id)).filter( Message.created_at >= cutoff ).scalar() print(f"Messages in last 5 hours: {count}") ``` ### If Anonymous Limits Not Applied ```python # Debug: Check anonymous user record anon = db.query(AnonymousUser).filter( AnonymousUser.id == "token_value" ).first() print(f"Chat count: {anon.chat_count if anon else 'No record'}") ``` --- ## Invariants (What NOT to Change) | Item | Must Stay Same | |------|----------------| | Message model | Existing fields must remain | | Bot model | Existing fields must remain | | ConversationalAgent.process_message() | Signature unchanged | | MiniMax API calls | Same behavior | | Backtest/Simulate tools | Same functionality | --- ## Files to Create/Modify | Action | File | |--------|------| | CREATE | `db/models/conversation.py` | | CREATE | `db/models/anonymous_user.py` | | CREATE | `services/rate_limiter.py` | | CREATE | `api/conversations.py` | | UPDATE | `db/models/message.py` (add conversation_id) | | UPDATE | `db/models/__init__.py` (export new models) | | UPDATE | `main.py` (register router) | | UPDATE | `services/ai_agent/tools.py` (add bot tools) | --- ## Migration Required ```bash alembic revision --autogenerate -m "add conversation and anonymous user tables" alembic upgrade head ``` --- ## Dependencies - Frontend issue: #60 - Refactor completed: #63 (✅ done) --- ## Priority: HIGH --- ## Acceptance Criteria - [ ] Conversation model created and migrated - [ ] Message model updated with conversation_id - [ ] AnonymousUser model created and migrated - [ ] GET /api/conversations returns user's conversations - [ ] POST /api/conversations creates new conversation - [ ] GET /api/conversations/{id} returns with messages - [ ] POST /api/conversations/{id}/chat processes message - [ ] DELETE /api/conversations/{id} removes conversation - [ ] POST /api/conversations/{id}/set-bot links bot - [ ] Non-logged users can chat max 50 times - [ ] Non-logged users can create max 1 bot - [ ] Non-logged users can run max 1 backtest - [ ] Non-logged users CANNOT run simulation - [ ] System-wide rate limit (500/5hrs) works - [ ] "Your progress is not saved" warning shown after 40 messages - [ ] Rate limit error returns proper message - [ ] Assistant can use create_bot, list_bots, set_bot, get_bot_info tools - [ ] All unit tests pass - [ ] All integration tests pass --- ## Notes for Developer 1. **Start with models** — Get database right first 2. **Test incrementally** — Don't build everything then test 3. **Use the rate limiter** — Don't duplicate limit logic 4. **Keep backward compatibility** — Existing /api/chat should still work 5. **Cookie httponly** — Security: anonymous_token cookie should be httponly 6. **Handle edge cases** — What if bot_id doesn't exist? What if user deletes bot while chatting? --- ## ⚠️ Prerequisite **This issue assumes issue #63 (Refactor) is COMPLETED.** The code examples reference the refactored structure: - `services/ai_agent/agent.py` (not conversational.py) - `services/ai_agent/tools.py` (tool registry) - `services/ai_agent/help.py` (help formatters) - `services/ai_agent/client.py` (MiniMax client) If #63 is not done yet, complete it first.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shoko/randebu#59