Compare commits
4 Commits
fix/issue-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fccdbb4cca | ||
| 958dc3bb1f | |||
|
|
5ae8d76bde | ||
| a9679bbb5d |
230
README.md
Normal file
230
README.md
Normal 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*
|
||||||
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,
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
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