Compare commits

...

4 Commits

Author SHA1 Message Date
shokollm
fccdbb4cca docs: add README.md 2026-04-15 15:46:05 +00:00
958dc3bb1f Merge pull request 'feat: conversation-based chat system with anonymous support' (#65) from fix/issue-59 into main 2026-04-14 08:19:54 +02:00
shokollm
5ae8d76bde feat: implement conversation-based chat system with anonymous support
- Add Conversation model with user/anonymous_token/bot_id fields
- Add Message model linked to conversations
- Add AnonymousUser model for tracking anonymous chat limits
- Create /api/conversations endpoints (list, create, get, delete)
- Add POST /api/conversations/{id}/chat for messaging
- Add POST /api/conversations/{id}/set-bot for linking bot
- Implement rate limiter with system-wide (500/5hrs) and anonymous limits
- Anonymous users: max 50 chats, max 1 bot, max 1 backtest
- Add warning after 40 anonymous messages
- Register conversations router in main.py
- Add create_bot, list_bots, set_bot, get_bot_info tools to registry
2026-04-14 04:25:00 +00:00
a9679bbb5d Merge pull request 'refactor: Split conversational.py into modular structure' (#64) from fix/issue-63 into main 2026-04-14 05:57:16 +02:00
7 changed files with 714 additions and 5 deletions

230
README.md Normal file
View File

@@ -0,0 +1,230 @@
# Randebu
**Create Trading Bots Through Natural Conversation**
Randebu is a web-based platform that allows traders to create and manage automated trading bots through simple chat interactions. No coding required.
---
## The Problem
Trading bots like **OpenClaw** and similar platforms are powerful but come with a steep learning curve:
- Complex configuration files
- Requires understanding of trading strategies
- CLI-based interfaces
- Steep technical barrier for non-developers
## The Solution
Randebu lets you create trading bots by simply chatting:
> "Create a bot that buys PEPE when it drops 5% and sells when it profits 10%"
That's it. Randebu handles the rest.
---
## How It Works
1. **Chat** - Tell Randebu what you want in plain English
2. **Bot Created** - Randebu creates a configured trading bot
3. **Backtest** - Test your strategy with historical data
4. **Simulate** - Run a simulation with real-time data
5. **Deploy** - Activate your bot on the blockchain
---
## Built on AVE Cloud
Randebu is powered by **AVE Cloud Skills** - the same infrastructure used by professional trading teams.
### AVE Skills Currently Integrated
Randebu uses **AVE Cloud Skills** (the skill scripts from `ave-cloud-skill` repository) for data fetching:
| Skill Script | Command | Purpose | Line in agent.py |
|-------------|---------|---------|------------------|
| `ave_data_rest.py` | `trending` | Get trending tokens | 218 |
| `ave_data_rest.py` | `search` | Search tokens by keyword | 285 |
| `ave_data_rest.py` | `risk` | Honeypot/risk analysis | 367 |
| `ave_data_rest.py` | `token` | Get token details | 487 |
| `ave_data_rest.py` | `price` | Get token prices | 509 |
### AVE Integration Points
The AVE skills are called through `_call_ave_script()` in `agent.py`:
```python
# agent.py - Calling AVE skill scripts
def _call_ave_script(self, command: str, args: list) -> tuple[int, str]:
ave_skill_path = os.path.join(
repo_root, "ave-cloud-skill", "scripts", "ave_data_rest.py"
)
result = subprocess.run(
["python3", ave_skill_path, command] + args,
...
)
```
### Direct API Usage (Not Skills)
These components use the AVE API directly (not through skills):
- `backtest/engine.py` - Uses `AveCloudClient.get_klines()` for historical kline data
- `simulate/engine.py` - Uses `AveCloudClient.get_klines()` for real-time kline data
---
## Further AVE Integration Opportunities
### 1. Trading Execution (Priority: High)
- **AVE Skills**: `trade-chain-wallet`, `trade-proxy-wallet`
- **Use**: Execute trades directly from the bot (market orders, limit orders, TP/SL)
- **Status**: Not yet integrated - this is the next major feature
### 2. Real-Time Alerts (Priority: Medium)
- **AVE Skills**: WebSocket streams (`data-wss`)
- **Use**: Notify users when price hits targets
### 3. Portfolio Tracking (Priority: Medium)
- **AVE API**: `address/walletinfo` endpoint
- **Use**: Show user's complete portfolio across chains
### 4. Advanced Risk Analysis (Priority: Low)
- **AVE Skills**: Extended token analysis
- **Use**: More detailed honeypot detection, liquidity analysis
---
## Tech Stack
| Component | Technology |
|-----------|------------|
| Frontend | SvelteKit, TypeScript |
| Backend | FastAPI, Python |
| Database | PostgreSQL |
| AI | MiniMax (extended thinking) |
| Trading Data | AVE Cloud API |
---
## Future Development Plan
### Phase 1: Core MVP (Current)
- [x] Chat-based bot creation
- [x] Strategy configuration via conversation
- [x] Backtest historical data
- [x] Simulation with real-time data
- [x] Bot management (create, list, set)
### Phase 2: Trading Execution
- [ ] AVE Trading API integration
- [ ] Chain wallet support
- [ ] Proxy wallet (bot-managed) support
- [ ] TP/SL automation
### Phase 3: Advanced Features
- [ ] Portfolio dashboard
- [ ] Multi-chain support (Solana, Base, ETH)
- [ ] Copy trading (follow other traders)
- [ ] Strategy marketplace
### Phase 4: Platform Growth
- [ ] Strategy templates
- [ ] Community strategies
- [ ] Premium features (for fees)
---
## Business Opportunity
### Target Market
1. **Retail Traders** - People who want to automate trading but can't code
2. **Crypto Enthusiasts** - Active traders looking for easier tools
3. **Small Funds** - Need automation without expensive developers
### Revenue Model
| Tier | Price | Features |
|------|-------|----------|
| Free | $0 | 1 bot, 50 chats, basic features |
| Pro | $19/mo | Unlimited bots, backtests, simulations |
| Enterprise | Custom | API access, priority support, custom integrations |
### Competitive Advantage
- **No-code** - Unlike OpenClaw, 3Commas, Cryptohopper
- **Natural Language** - Describe strategy in plain English
- **AVE Integration** - Built on professional-grade infrastructure
- **Focused UX** - Simple, clean interface designed for beginners
### Market Size
- Crypto traders: 100M+ globally
- Trading bot market: $1.5B+ by 2027
- No-code platform market: Growing rapidly
---
## Getting Started
### Prerequisites
- Python 3.10+
- Node.js 18+
- PostgreSQL
### Installation
```bash
# Clone the repo
git clone https://github.com/shoko/randebu.git
cd randebu
# Setup backend
cd src/backend
pip install -r requirements.txt
# Setup frontend
cd ../frontend
npm install
# Configure environment
cp .env.example .env
# Edit .env with your API keys
# Run
# Backend: uvicorn main:app
# Frontend: npm run dev
```
### Configuration
Required environment variables:
- `MINIMAX_API_KEY` - For AI chat
- `AVE_API_KEY` - For trading data
- `DATABASE_URL` - PostgreSQL connection
---
## Contributing
Contributions welcome! Please read our contributing guidelines before submitting PRs.
---
## License
MIT
---
## Links
- Website: [randebu.com](https://randebu.com)
- AVE Cloud: [cloud.ave.ai](https://cloud.ave.ai)
- Hackathon: [clawhackathon.aveai.trade](https://clawhackathon.aveai.trade)
---
*Built with ❤️ for traders, by traders*

View 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,
}

View File

@@ -10,6 +10,7 @@ from sqlalchemy import (
ForeignKey, ForeignKey,
Index, Index,
JSON, JSON,
Integer,
) )
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from ..core.database import Base from ..core.database import Base
@@ -30,6 +31,9 @@ class User(Base):
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
bots = relationship("Bot", back_populates="user", cascade="all, delete-orphan") bots = relationship("Bot", back_populates="user", cascade="all, delete-orphan")
conversations = relationship(
"Conversation", back_populates="user", cascade="all, delete-orphan"
)
class Bot(Base): class Bot(Base):
@@ -47,7 +51,7 @@ class Bot(Base):
user = relationship("User", back_populates="bots") user = relationship("User", back_populates="bots")
conversations = relationship( conversations = relationship(
"BotConversation", back_populates="bot", cascade="all, delete-orphan" "Conversation", back_populates="bot", cascade="all, delete-orphan"
) )
backtests = relationship( backtests = relationship(
"Backtest", back_populates="bot", cascade="all, delete-orphan" "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") 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): class BotConversation(Base):
__tablename__ = "bot_conversations" __tablename__ = "bot_conversations"
@@ -118,6 +163,9 @@ class Signal(Base):
Index("idx_bots_user_id", Bot.user_id) 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_conversations_bot_id", BotConversation.bot_id)
Index("idx_backtests_bot_id", Backtest.bot_id) Index("idx_backtests_bot_id", Backtest.bot_id)
Index("idx_simulations_bot_id", Simulation.bot_id) Index("idx_simulations_bot_id", Simulation.bot_id)

View File

@@ -242,3 +242,57 @@ class AveChainSwapRequest(BaseModel):
class AveChainSwapResponse(BaseModel): class AveChainSwapResponse(BaseModel):
swap: Optional[dict] = None swap: Optional[dict] = None
upsell_message: Optional[str] = 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

View File

@@ -4,7 +4,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter from slowapi import Limiter
from slowapi.util import get_remote_address 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.limiter import limiter
from .core.database import engine, Base from .core.database import engine, Base
@@ -15,7 +15,17 @@ logger = logging.getLogger(__name__)
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Initialize database on startup.""" """Initialize database on startup."""
# Import all models to ensure they're registered # 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 # Create tables if they don't exist
Base.metadata.create_all(bind=engine) 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(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(bots.router, prefix="/api/bots", tags=["bots"]) 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(backtest.router, prefix="/api", tags=["backtest"])
app.include_router(simulate.router, prefix="/api", tags=["simulate"]) app.include_router(simulate.router, prefix="/api", tags=["simulate"])
app.include_router(config.router, prefix="/api/config", tags=["config"]) app.include_router(config.router, prefix="/api/config", tags=["config"])

View File

@@ -37,6 +37,50 @@ TOOL_REGISTRY: Dict[str, Any] = {
"example": "Buy PEPE when it drops 10% within 1 hour", "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": [ "ave": [
{ {

View 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()