Spaces:
Paused
Paused
Upload folder using huggingface_hub
Browse files- .dockerignore +58 -0
- .gitattributes +35 -35
- DEPLOYMENT_INFO.md +63 -0
- DEPLOYMENT_SUCCESS.md +248 -0
- Dockerfile +31 -0
- README.md +247 -12
- app.py +1013 -0
- app/__init__.py +10 -0
- app/__pycache__/__init__.cpython-314.pyc +0 -0
- app/agent/__init__.py +16 -0
- app/agent/base.py +196 -0
- app/agent/manus.py +165 -0
- app/agent/toolcall.py +250 -0
- app/auth.py +205 -0
- app/auth_interface.py +361 -0
- app/auth_service.py +357 -0
- app/cloudflare/__init__.py +11 -0
- app/cloudflare/client.py +228 -0
- app/cloudflare/d1.py +510 -0
- app/cloudflare/durable_objects.py +365 -0
- app/cloudflare/kv.py +457 -0
- app/cloudflare/r2.py +434 -0
- app/config.py +372 -0
- app/huggingface_models.py +0 -0
- app/logger.py +42 -0
- app/production_config.py +363 -0
- app/prompt/__init__.py +0 -0
- app/prompt/manus.py +10 -0
- app/schema.py +187 -0
- app/tool/__init__.py +24 -0
- app/tool/ask_human.py +21 -0
- app/tool/base.py +181 -0
- app/tool/python_execute.py +75 -0
- app/tool/str_replace_editor.py +432 -0
- app/tool/terminate.py +25 -0
- app/tool/tool_collection.py +71 -0
- app/utils/__init__.py +1 -0
- app/utils/logger.py +32 -0
- app_complete.py +715 -0
- config/config.example.toml +105 -0
- docker-commands.md +49 -0
- requirements.txt +1 -0
- requirements_backup.txt +76 -0
- requirements_fixed.txt +44 -0
- requirements_new.txt +33 -0
- schema.sql +135 -0
- start.sh +25 -0
.dockerignore
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git files
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.gitattributes
|
| 5 |
+
|
| 6 |
+
# Python cache
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
*.so
|
| 11 |
+
.Python
|
| 12 |
+
|
| 13 |
+
# Virtual environments
|
| 14 |
+
venv/
|
| 15 |
+
env/
|
| 16 |
+
ENV/
|
| 17 |
+
|
| 18 |
+
# IDEs
|
| 19 |
+
.vscode/
|
| 20 |
+
.idea/
|
| 21 |
+
*.swp
|
| 22 |
+
*.swo
|
| 23 |
+
*~
|
| 24 |
+
|
| 25 |
+
# OS files
|
| 26 |
+
.DS_Store
|
| 27 |
+
Thumbs.db
|
| 28 |
+
|
| 29 |
+
# Workspace and temp files
|
| 30 |
+
workspace/
|
| 31 |
+
tmp/
|
| 32 |
+
temp/
|
| 33 |
+
*.tmp
|
| 34 |
+
|
| 35 |
+
# Logs
|
| 36 |
+
*.log
|
| 37 |
+
logs/
|
| 38 |
+
|
| 39 |
+
# Backup files
|
| 40 |
+
*_backup.*
|
| 41 |
+
*.bak
|
| 42 |
+
|
| 43 |
+
# Large files that aren't needed
|
| 44 |
+
assets/
|
| 45 |
+
examples/
|
| 46 |
+
tests/
|
| 47 |
+
.pytest_cache/
|
| 48 |
+
|
| 49 |
+
# Documentation (keep only README.md)
|
| 50 |
+
*.md
|
| 51 |
+
!README.md
|
| 52 |
+
|
| 53 |
+
# Config examples
|
| 54 |
+
config/config.example*.toml
|
| 55 |
+
|
| 56 |
+
# Deployment docs
|
| 57 |
+
DEPLOYMENT_*.md
|
| 58 |
+
docker-commands.md
|
.gitattributes
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
DEPLOYMENT_INFO.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenManus HuggingFace Deployment Package
|
| 2 |
+
|
| 3 |
+
## 📦 Package Contents
|
| 4 |
+
- **Total Files**: 35
|
| 5 |
+
- **Missing Files**: 0
|
| 6 |
+
|
| 7 |
+
## ✅ Included Files
|
| 8 |
+
- Dockerfile
|
| 9 |
+
- README.md
|
| 10 |
+
- app/__init__.py
|
| 11 |
+
- app/agent/__init__.py
|
| 12 |
+
- app/agent/base.py
|
| 13 |
+
- app/agent/manus.py
|
| 14 |
+
- app/agent/toolcall.py
|
| 15 |
+
- app/auth.py
|
| 16 |
+
- app/auth_interface.py
|
| 17 |
+
- app/auth_service.py
|
| 18 |
+
- app/cloudflare/__init__.py
|
| 19 |
+
- app/cloudflare/client.py
|
| 20 |
+
- app/cloudflare/d1.py
|
| 21 |
+
- app/cloudflare/durable_objects.py
|
| 22 |
+
- app/cloudflare/kv.py
|
| 23 |
+
- app/cloudflare/r2.py
|
| 24 |
+
- app/config.py
|
| 25 |
+
- app/huggingface_models.py
|
| 26 |
+
- app/logger.py
|
| 27 |
+
- app/production_config.py
|
| 28 |
+
- app/prompt/__init__.py
|
| 29 |
+
- app/prompt/manus.py
|
| 30 |
+
- app/schema.py
|
| 31 |
+
- app/tool/__init__.py
|
| 32 |
+
- app/tool/ask_human.py
|
| 33 |
+
- app/tool/base.py
|
| 34 |
+
- app/tool/python_execute.py
|
| 35 |
+
- app/tool/str_replace_editor.py
|
| 36 |
+
- app/tool/terminate.py
|
| 37 |
+
- app/tool/tool_collection.py
|
| 38 |
+
- app/utils/__init__.py
|
| 39 |
+
- app/utils/logger.py
|
| 40 |
+
- app_production.py
|
| 41 |
+
- requirements.txt
|
| 42 |
+
- schema.sql
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
## 🚀 Deployment Instructions
|
| 46 |
+
|
| 47 |
+
1. **Upload all files** from this package to your HuggingFace Space
|
| 48 |
+
2. **Set environment variables** in Space settings:
|
| 49 |
+
- `ENVIRONMENT=production`
|
| 50 |
+
- `HF_TOKEN=your_token` (optional)
|
| 51 |
+
- `CLOUDFLARE_API_TOKEN=your_token` (optional)
|
| 52 |
+
|
| 53 |
+
3. **Monitor build logs** during deployment
|
| 54 |
+
4. **Test authentication** after successful deployment
|
| 55 |
+
|
| 56 |
+
## 🎯 What This Includes
|
| 57 |
+
- ✅ Complete authentication system (mobile + password)
|
| 58 |
+
- ✅ 200+ AI model configurations
|
| 59 |
+
- ✅ Cloudflare services integration
|
| 60 |
+
- ✅ Production-ready Docker setup
|
| 61 |
+
- ✅ Comprehensive error handling
|
| 62 |
+
|
| 63 |
+
Ready for HuggingFace Spaces! 🚀
|
DEPLOYMENT_SUCCESS.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎉 Deployment Successful - ORYNXML Complete AI Platform
|
| 2 |
+
|
| 3 |
+
## ✅ What Was Fixed
|
| 4 |
+
|
| 5 |
+
### Problem
|
| 6 |
+
- **Error**: `RuntimeError: Directory 'static' does not exist`
|
| 7 |
+
- **Root Cause**: Space was trying to run FastAPI (`main.py`) instead of Gradio (`app.py`)
|
| 8 |
+
- **Reason**: README.md had incorrect metadata configuration
|
| 9 |
+
|
| 10 |
+
### Solution
|
| 11 |
+
✅ Updated `agnt/README.md` with correct Gradio configuration:
|
| 12 |
+
```yaml
|
| 13 |
+
---
|
| 14 |
+
title: ORYNXML Complete AI Platform
|
| 15 |
+
emoji: 🤖
|
| 16 |
+
colorFrom: blue
|
| 17 |
+
colorTo: indigo
|
| 18 |
+
sdk: gradio
|
| 19 |
+
sdk_version: 4.44.1
|
| 20 |
+
app_file: app.py
|
| 21 |
+
pinned: false
|
| 22 |
+
---
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
✅ Redeployed README.md to Hugging Face Space
|
| 26 |
+
✅ Space now correctly recognizes as **Gradio SDK**
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## 🚀 Your Backend is Ready!
|
| 31 |
+
|
| 32 |
+
### 🔗 Space URL
|
| 33 |
+
**https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai**
|
| 34 |
+
|
| 35 |
+
### 📊 Current Status
|
| 36 |
+
- ✅ **SDK**: Gradio (correct!)
|
| 37 |
+
- ⏸️ **State**: PAUSED (needs restart)
|
| 38 |
+
- 💻 **Hardware**: NVIDIA A10G GPU configured
|
| 39 |
+
- 📦 **Models**: 211 models across 8 categories
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## 🎯 Next Steps to Complete Deployment
|
| 44 |
+
|
| 45 |
+
### 1️⃣ **RESTART THE SPACE** (Manual Action Required)
|
| 46 |
+
You need to manually restart the Space:
|
| 47 |
+
|
| 48 |
+
1. Go to: https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai/settings
|
| 49 |
+
2. Click **"Factory Rebuild"** button
|
| 50 |
+
3. Wait for build to complete (~2-3 minutes)
|
| 51 |
+
4. Space will automatically start and load the Gradio interface
|
| 52 |
+
|
| 53 |
+
### 2️⃣ **Verify Space is Running**
|
| 54 |
+
After rebuild:
|
| 55 |
+
- Visit: https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai
|
| 56 |
+
- You should see the Gradio interface with 11 tabs:
|
| 57 |
+
- Sign Up
|
| 58 |
+
- Login
|
| 59 |
+
- Text Generation
|
| 60 |
+
- Image Processing
|
| 61 |
+
- Image Editing
|
| 62 |
+
- Video Generation
|
| 63 |
+
- AI Teacher & Education
|
| 64 |
+
- Software Engineer Agent
|
| 65 |
+
- Audio Processing
|
| 66 |
+
- Multimodal & Avatars
|
| 67 |
+
- Arabic-English
|
| 68 |
+
|
| 69 |
+
### 3️⃣ **Test Authentication**
|
| 70 |
+
- Create a test account using the Sign Up tab
|
| 71 |
+
- Try logging in with the credentials
|
| 72 |
+
- SQLite database (`openmanus.db`) will be created automatically
|
| 73 |
+
|
| 74 |
+
### 4️⃣ **Test AI Models**
|
| 75 |
+
- Select any model from the dropdowns
|
| 76 |
+
- Enter a prompt
|
| 77 |
+
- Verify the simulated response is displayed
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 📦 What's Deployed
|
| 82 |
+
|
| 83 |
+
### Backend (`app.py` - 1014 lines)
|
| 84 |
+
✅ **8 Complete Agent Categories with 211 Models:**
|
| 85 |
+
|
| 86 |
+
1. **Text Generation** (52 models)
|
| 87 |
+
- Qwen Models (35): Qwen2.5-72B, QwQ-32B, Coder series, Math series
|
| 88 |
+
- DeepSeek Models (17): DeepSeek-R1, V2-Chat, Coder series
|
| 89 |
+
|
| 90 |
+
2. **Image Processing** (31 models)
|
| 91 |
+
- Generation (10): FLUX.1, Stable Diffusion, Kandinsky, Midjourney
|
| 92 |
+
- Editing (15): Pix2Pix, ControlNet, PhotoMaker, InstantID
|
| 93 |
+
- Face Processing (6): InsightFace, GFPGAN, CodeFormer, Real-ESRGAN
|
| 94 |
+
|
| 95 |
+
3. **Image Editing** (15 models)
|
| 96 |
+
- Specialized editing: Inpainting, ControlNet, Style transfer
|
| 97 |
+
|
| 98 |
+
4. **Video Generation** (18 models)
|
| 99 |
+
- Text-to-Video (8): Ali-ViLab, ModelScope, AnimateDiff
|
| 100 |
+
- Image-to-Video (5): Stable Video Diffusion, ToonCrafter
|
| 101 |
+
- Video Editing (5): Tune-A-Video, Text2Video-Zero
|
| 102 |
+
|
| 103 |
+
5. **AI Teacher & Education** (21 models)
|
| 104 |
+
- Math & Science (6): Qwen2.5-Math, WizardMath
|
| 105 |
+
- Coding Tutor (5): Qwen Coder, DeepSeek Coder
|
| 106 |
+
- Language Learning (5): NLLB-200, SeamlessM4T
|
| 107 |
+
- General Education (5): Mistral, Phi-3, Nous-Hermes
|
| 108 |
+
|
| 109 |
+
6. **Software Engineer Agent** (27 models)
|
| 110 |
+
- Code Generation (10): CodeLlama 70B/34B/13B/7B, Qwen Coder
|
| 111 |
+
- Code Review (7): StarCoder2, WizardCoder, Phind-CodeLlama
|
| 112 |
+
- Specialized Coding (6): CodeGen, Replit, PolyCoder
|
| 113 |
+
- DevOps (4): Infrastructure & deployment models
|
| 114 |
+
|
| 115 |
+
7. **Audio Processing** (30 models)
|
| 116 |
+
- Text-to-Speech (15): SpeechT5, MMS-TTS, XTTS-v2, Bark
|
| 117 |
+
- Speech-to-Text (15): Whisper (all sizes), Wav2Vec2, MMS
|
| 118 |
+
|
| 119 |
+
8. **Multimodal AI** (20 models)
|
| 120 |
+
- Vision-Language (11): LLaVA, BLIP2, Git, ViLT
|
| 121 |
+
- Talking Avatars (9): Wav2Lip, DeepFaceLive, FaceRig
|
| 122 |
+
|
| 123 |
+
9. **Arabic-English** (12 models)
|
| 124 |
+
- BERT models, AraBERT, MARBERT, NLLB translation
|
| 125 |
+
|
| 126 |
+
### Frontend (9 HTML Pages)
|
| 127 |
+
✅ All pages use **ORYNXML branding**:
|
| 128 |
+
- Colors: Blue (#1B5DA8, #4DA3FF), Dark (#0A1628)
|
| 129 |
+
- Logo: Hexagonal design from ibb.co
|
| 130 |
+
|
| 131 |
+
**Current Pages:**
|
| 132 |
+
1. `index.html` - Landing page
|
| 133 |
+
2. `signup.html` - User registration
|
| 134 |
+
3. `login.html` - Authentication
|
| 135 |
+
4. `dashboard.html` - Main hub
|
| 136 |
+
5. `chat.html` - AI chat interface (updated with 8 categories)
|
| 137 |
+
6. `terminal.html` - Command terminal
|
| 138 |
+
7. `browser.html` - Web browser
|
| 139 |
+
8. `files.html` - File manager
|
| 140 |
+
9. `visualize.html` - Data visualization
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## 🔮 Future Enhancements (Optional)
|
| 145 |
+
|
| 146 |
+
### A. Create 7 Specialized Visual Interfaces
|
| 147 |
+
Replace simulated responses with rich UI:
|
| 148 |
+
|
| 149 |
+
1. **`image-gen.html`** - Image generation studio
|
| 150 |
+
- Model selector, prompt builder, image gallery
|
| 151 |
+
- Real-time generation from FLUX.1/SDXL
|
| 152 |
+
|
| 153 |
+
2. **`code-review.html`** - Code review dashboard
|
| 154 |
+
- Code editor, diff viewer, suggestions panel
|
| 155 |
+
- Integration with Software Engineer Agent
|
| 156 |
+
|
| 157 |
+
3. **`audio-studio.html`** - Audio processing workspace
|
| 158 |
+
- TTS voice selector, STT transcription
|
| 159 |
+
- Audio player and waveform visualization
|
| 160 |
+
|
| 161 |
+
4. **`ai-teacher.html`** - Interactive learning platform
|
| 162 |
+
- Subject selector, problem solver, step-by-step explanations
|
| 163 |
+
- Math equation renderer, code examples
|
| 164 |
+
|
| 165 |
+
5. **`vision-lab.html`** - Computer vision workspace
|
| 166 |
+
- Image upload, VQA interface, captioning
|
| 167 |
+
- Multi-image comparison
|
| 168 |
+
|
| 169 |
+
6. **`video-gen.html`** - Video generation studio
|
| 170 |
+
- Text-to-video/image-to-video interface
|
| 171 |
+
- Timeline editor, preview player
|
| 172 |
+
|
| 173 |
+
7. **`image-edit.html`** - Advanced image editor
|
| 174 |
+
- ControlNet, inpainting, style transfer
|
| 175 |
+
- Before/after comparison
|
| 176 |
+
|
| 177 |
+
### B. Connect Frontend to Backend API
|
| 178 |
+
- Create API endpoints in `app.py` for each category
|
| 179 |
+
- Replace simulated responses with real model inference
|
| 180 |
+
- Add authentication middleware
|
| 181 |
+
- Implement rate limiting and usage tracking
|
| 182 |
+
|
| 183 |
+
### C. Enhanced Features
|
| 184 |
+
- User dashboard with usage statistics
|
| 185 |
+
- Model comparison side-by-side
|
| 186 |
+
- Save/load conversation history
|
| 187 |
+
- Export results (images, audio, code)
|
| 188 |
+
- API key management for external services
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## 📊 Technical Stack Summary
|
| 193 |
+
|
| 194 |
+
### Backend
|
| 195 |
+
- **Framework**: Gradio 4.44.1
|
| 196 |
+
- **Database**: SQLite (openmanus.db)
|
| 197 |
+
- **Authentication**: Mobile number + SHA-256 password hashing
|
| 198 |
+
- **AI Models**: 211 models from HuggingFace Hub
|
| 199 |
+
- **Deployment**: Hugging Face Spaces (NVIDIA A10G GPU)
|
| 200 |
+
|
| 201 |
+
### Frontend
|
| 202 |
+
- **Framework**: Pure HTML/CSS/JavaScript
|
| 203 |
+
- **Styling**: ORYNXML blue gradient theme
|
| 204 |
+
- **Icons**: Lucide Icons
|
| 205 |
+
- **Hosting**: Ready for Cloudflare Pages
|
| 206 |
+
- **Backend Integration**: API calls to HF Space
|
| 207 |
+
|
| 208 |
+
### Infrastructure
|
| 209 |
+
- **Cloudflare Services**: D1, R2, KV, Durable Objects (configured)
|
| 210 |
+
- **GPU**: NVIDIA A10G (Space hardware)
|
| 211 |
+
- **Storage**: SQLite database persists in Space
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## 🎉 Deployment Checklist
|
| 216 |
+
|
| 217 |
+
- ✅ Backend code complete (app.py - 1014 lines)
|
| 218 |
+
- ✅ Requirements.txt with all dependencies
|
| 219 |
+
- ✅ README.md with correct Gradio configuration
|
| 220 |
+
- ✅ Uploaded to HuggingFace Space
|
| 221 |
+
- ✅ README.md redeployed with fix
|
| 222 |
+
- ✅ Space recognized as Gradio SDK
|
| 223 |
+
- ⏸️ **PENDING**: Space restart (manual action required)
|
| 224 |
+
- ⏸️ **PENDING**: Test authentication and models
|
| 225 |
+
- ⏸️ **PENDING**: Create 7 specialized frontend pages (optional)
|
| 226 |
+
- ⏸️ **PENDING**: Connect frontend to backend API
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
## 🔗 Important Links
|
| 231 |
+
|
| 232 |
+
- **Space URL**: https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai
|
| 233 |
+
- **Settings**: https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai/settings
|
| 234 |
+
- **Logs**: https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai/logs
|
| 235 |
+
- **Files**: https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai/tree/main
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
## 🚨 Action Required
|
| 240 |
+
|
| 241 |
+
**Go to Space Settings and click "Factory Rebuild" to complete deployment!**
|
| 242 |
+
|
| 243 |
+
After rebuild, your complete AI platform with 211 models will be live at:
|
| 244 |
+
**https://huggingface.co/spaces/Speedofmastery/yyuu-complete-ai**
|
| 245 |
+
|
| 246 |
+
---
|
| 247 |
+
|
| 248 |
+
*Last updated: Configuration fix deployed, awaiting manual Space restart*
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simple production Docker with Python 3.10
|
| 2 |
+
FROM python:3.10
|
| 3 |
+
|
| 4 |
+
# Environment variables
|
| 5 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 6 |
+
GRADIO_SERVER_NAME=0.0.0.0 \
|
| 7 |
+
GRADIO_SERVER_PORT=7860
|
| 8 |
+
|
| 9 |
+
# Create user
|
| 10 |
+
RUN useradd -m -u 1000 user
|
| 11 |
+
USER user
|
| 12 |
+
|
| 13 |
+
# Set paths
|
| 14 |
+
ENV HOME=/home/user \
|
| 15 |
+
PATH=/home/user/.local/bin:$PATH
|
| 16 |
+
|
| 17 |
+
WORKDIR $HOME/app
|
| 18 |
+
|
| 19 |
+
# Copy and install requirements
|
| 20 |
+
COPY --chown=user requirements.txt .
|
| 21 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 22 |
+
pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# Copy app
|
| 25 |
+
COPY --chown=user app.py .
|
| 26 |
+
|
| 27 |
+
# Expose port
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Run app
|
| 31 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,12 +1,247 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ORYNXML Complete AI Platform
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
+
license: apache-2.0
|
| 10 |
+
short_description: Complete AI Platform with 211 models across 8 categories
|
| 11 |
+
tags:
|
| 12 |
+
- AI
|
| 13 |
+
- Authentication
|
| 14 |
+
- Multi-Modal
|
| 15 |
+
- HuggingFace
|
| 16 |
+
- OpenManus
|
| 17 |
+
- Qwen
|
| 18 |
+
- DeepSeek
|
| 19 |
+
- TTS
|
| 20 |
+
- STT
|
| 21 |
+
- Face-Swap
|
| 22 |
+
- Avatar
|
| 23 |
+
- Arabic
|
| 24 |
+
- English
|
| 25 |
+
- Cloudflare
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
<p align="center">
|
| 29 |
+
<img src="assets/logo.jpg" width="200"/>
|
| 30 |
+
</p>
|
| 31 |
+
|
| 32 |
+
English | [中文](README_zh.md) | [한국어](README_ko.md) | [日本語](README_ja.md)
|
| 33 |
+
|
| 34 |
+
[](https://github.com/FoundationAgents/OpenManus/stargazers)
|
| 35 |
+
 
|
| 36 |
+
[](https://opensource.org/licenses/MIT)  
|
| 37 |
+
[](https://discord.gg/DYn29wFk9z)
|
| 38 |
+
[](https://huggingface.co/spaces/lyh-917/OpenManusDemo)
|
| 39 |
+
[](https://doi.org/10.5281/zenodo.15186407)
|
| 40 |
+
|
| 41 |
+
# 👋 OpenManus - Complete AI Platform
|
| 42 |
+
|
| 43 |
+
🤖 **200+ AI Models + Mobile Authentication + Cloudflare Services**
|
| 44 |
+
|
| 45 |
+
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
|
| 46 |
+
|
| 47 |
+
## 🌟 Environment Variables
|
| 48 |
+
|
| 49 |
+
Set these in your HuggingFace Space settings for full functionality:
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
# Required for full Cloudflare integration
|
| 53 |
+
CLOUDFLARE_API_TOKEN=your_cloudflare_token
|
| 54 |
+
CLOUDFLARE_ACCOUNT_ID=your_account_id
|
| 55 |
+
CLOUDFLARE_D1_DATABASE_ID=your_d1_database_id
|
| 56 |
+
CLOUDFLARE_R2_BUCKET_NAME=your_r2_bucket
|
| 57 |
+
CLOUDFLARE_KV_NAMESPACE_ID=your_kv_namespace
|
| 58 |
+
|
| 59 |
+
# Enhanced AI model access
|
| 60 |
+
HF_TOKEN=your_huggingface_token
|
| 61 |
+
OPENAI_API_KEY=your_openai_key
|
| 62 |
+
ANTHROPIC_API_KEY=your_anthropic_key
|
| 63 |
+
|
| 64 |
+
# Application configuration
|
| 65 |
+
ENVIRONMENT=production
|
| 66 |
+
LOG_LEVEL=INFO
|
| 67 |
+
SECRET_KEY=your_secret_key
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
|
| 71 |
+
|
| 72 |
+
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
|
| 73 |
+
|
| 74 |
+
Enjoy your own agent with OpenManus!
|
| 75 |
+
|
| 76 |
+
We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus.
|
| 77 |
+
|
| 78 |
+
## Project Demo
|
| 79 |
+
|
| 80 |
+
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
|
| 81 |
+
|
| 82 |
+
## Installation
|
| 83 |
+
|
| 84 |
+
We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
|
| 85 |
+
|
| 86 |
+
### Method 1: Using conda
|
| 87 |
+
|
| 88 |
+
1. Create a new conda environment:
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
conda create -n open_manus python=3.12
|
| 92 |
+
conda activate open_manus
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
2. Clone the repository:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
git clone https://github.com/FoundationAgents/OpenManus.git
|
| 99 |
+
cd OpenManus
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
3. Install dependencies:
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
pip install -r requirements.txt
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Method 2: Using uv (Recommended)
|
| 109 |
+
|
| 110 |
+
1. Install uv (A fast Python package installer and resolver):
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
2. Clone the repository:
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
git clone https://github.com/FoundationAgents/OpenManus.git
|
| 120 |
+
cd OpenManus
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
3. Create a new virtual environment and activate it:
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
uv venv --python 3.12
|
| 127 |
+
source .venv/bin/activate # On Unix/macOS
|
| 128 |
+
# Or on Windows:
|
| 129 |
+
# .venv\Scripts\activate
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
4. Install dependencies:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
uv pip install -r requirements.txt
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### Browser Automation Tool (Optional)
|
| 139 |
+
```bash
|
| 140 |
+
playwright install
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
## Configuration
|
| 144 |
+
|
| 145 |
+
OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
|
| 146 |
+
|
| 147 |
+
1. Create a `config.toml` file in the `config` directory (you can copy from the example):
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
cp config/config.example.toml config/config.toml
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
2. Edit `config/config.toml` to add your API keys and customize settings:
|
| 154 |
+
|
| 155 |
+
```toml
|
| 156 |
+
# Global LLM configuration
|
| 157 |
+
[llm]
|
| 158 |
+
model = "gpt-4o"
|
| 159 |
+
base_url = "https://api.openai.com/v1"
|
| 160 |
+
api_key = "sk-..." # Replace with your actual API key
|
| 161 |
+
max_tokens = 4096
|
| 162 |
+
temperature = 0.0
|
| 163 |
+
|
| 164 |
+
# Optional configuration for specific LLM models
|
| 165 |
+
[llm.vision]
|
| 166 |
+
model = "gpt-4o"
|
| 167 |
+
base_url = "https://api.openai.com/v1"
|
| 168 |
+
api_key = "sk-..." # Replace with your actual API key
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
## Quick Start
|
| 172 |
+
|
| 173 |
+
One line for run OpenManus:
|
| 174 |
+
|
| 175 |
+
```bash
|
| 176 |
+
python main.py
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
Then input your idea via terminal!
|
| 180 |
+
|
| 181 |
+
For MCP tool version, you can run:
|
| 182 |
+
```bash
|
| 183 |
+
python run_mcp.py
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
For unstable multi-agent version, you also can run:
|
| 187 |
+
|
| 188 |
+
```bash
|
| 189 |
+
python run_flow.py
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
### Custom Adding Multiple Agents
|
| 193 |
+
|
| 194 |
+
Currently, besides the general OpenManus Agent, we have also integrated the DataAnalysis Agent, which is suitable for data analysis and data visualization tasks. You can add this agent to `run_flow` in `config.toml`.
|
| 195 |
+
|
| 196 |
+
```toml
|
| 197 |
+
# Optional configuration for run-flow
|
| 198 |
+
[runflow]
|
| 199 |
+
use_data_analysis_agent = true # Disabled by default, change to true to activate
|
| 200 |
+
```
|
| 201 |
+
In addition, you need to install the relevant dependencies to ensure the agent runs properly: [Detailed Installation Guide](app/tool/chart_visualization/README.md##Installation)
|
| 202 |
+
|
| 203 |
+
## How to contribute
|
| 204 |
+
|
| 205 |
+
We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
|
| 206 |
+
|
| 207 |
+
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
|
| 208 |
+
|
| 209 |
+
**Note**: Before submitting a pull request, please use the pre-commit tool to check your changes. Run `pre-commit run --all-files` to execute the checks.
|
| 210 |
+
|
| 211 |
+
## Community Group
|
| 212 |
+
Join our networking group on Feishu and share your experience with other developers!
|
| 213 |
+
|
| 214 |
+
<div align="center" style="display: flex; gap: 20px;">
|
| 215 |
+
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
## Star History
|
| 219 |
+
|
| 220 |
+
[](https://star-history.com/#FoundationAgents/OpenManus&Date)
|
| 221 |
+
|
| 222 |
+
## Sponsors
|
| 223 |
+
Thanks to [PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) for computing source support.
|
| 224 |
+
> PPIO: The most affordable and easily-integrated MaaS and GPU cloud solution.
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
## Acknowledgement
|
| 228 |
+
|
| 229 |
+
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo), [browser-use](https://github.com/browser-use/browser-use) and [crawl4ai](https://github.com/unclecode/crawl4ai) for providing basic support for this project!
|
| 230 |
+
|
| 231 |
+
Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands) and [SWE-agent](https://github.com/SWE-agent/SWE-agent).
|
| 232 |
+
|
| 233 |
+
We also thank stepfun(阶跃星辰) for supporting our Hugging Face demo space.
|
| 234 |
+
|
| 235 |
+
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
|
| 236 |
+
|
| 237 |
+
## Cite
|
| 238 |
+
```bibtex
|
| 239 |
+
@misc{openmanus2025,
|
| 240 |
+
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
|
| 241 |
+
title = {OpenManus: An open-source framework for building general AI agents},
|
| 242 |
+
year = {2025},
|
| 243 |
+
publisher = {Zenodo},
|
| 244 |
+
doi = {10.5281/zenodo.15186407},
|
| 245 |
+
url = {https://doi.org/10.5281/zenodo.15186407},
|
| 246 |
+
}
|
| 247 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,1013 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import sqlite3
|
| 5 |
+
import hashlib
|
| 6 |
+
import datetime
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# Cloudflare configuration
|
| 10 |
+
CLOUDFLARE_CONFIG = {
|
| 11 |
+
"api_token": os.getenv("CLOUDFLARE_API_TOKEN", ""),
|
| 12 |
+
"account_id": os.getenv("CLOUDFLARE_ACCOUNT_ID", ""),
|
| 13 |
+
"d1_database_id": os.getenv("CLOUDFLARE_D1_DATABASE_ID", ""),
|
| 14 |
+
"r2_bucket_name": os.getenv("CLOUDFLARE_R2_BUCKET_NAME", ""),
|
| 15 |
+
"kv_namespace_id": os.getenv("CLOUDFLARE_KV_NAMESPACE_ID", ""),
|
| 16 |
+
"durable_objects_id": os.getenv("CLOUDFLARE_DURABLE_OBJECTS_ID", ""),
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
# AI Model Categories with 200+ models
|
| 20 |
+
AI_MODELS = {
|
| 21 |
+
"Text Generation": {
|
| 22 |
+
"Qwen Models": [
|
| 23 |
+
"Qwen/Qwen2.5-72B-Instruct",
|
| 24 |
+
"Qwen/Qwen2.5-32B-Instruct",
|
| 25 |
+
"Qwen/Qwen2.5-14B-Instruct",
|
| 26 |
+
"Qwen/Qwen2.5-7B-Instruct",
|
| 27 |
+
"Qwen/Qwen2.5-3B-Instruct",
|
| 28 |
+
"Qwen/Qwen2.5-1.5B-Instruct",
|
| 29 |
+
"Qwen/Qwen2.5-0.5B-Instruct",
|
| 30 |
+
"Qwen/Qwen2-72B-Instruct",
|
| 31 |
+
"Qwen/Qwen2-57B-A14B-Instruct",
|
| 32 |
+
"Qwen/Qwen2-7B-Instruct",
|
| 33 |
+
"Qwen/Qwen2-1.5B-Instruct",
|
| 34 |
+
"Qwen/Qwen2-0.5B-Instruct",
|
| 35 |
+
"Qwen/Qwen1.5-110B-Chat",
|
| 36 |
+
"Qwen/Qwen1.5-72B-Chat",
|
| 37 |
+
"Qwen/Qwen1.5-32B-Chat",
|
| 38 |
+
"Qwen/Qwen1.5-14B-Chat",
|
| 39 |
+
"Qwen/Qwen1.5-7B-Chat",
|
| 40 |
+
"Qwen/Qwen1.5-4B-Chat",
|
| 41 |
+
"Qwen/Qwen1.5-1.8B-Chat",
|
| 42 |
+
"Qwen/Qwen1.5-0.5B-Chat",
|
| 43 |
+
"Qwen/CodeQwen1.5-7B-Chat",
|
| 44 |
+
"Qwen/Qwen2.5-Math-72B-Instruct",
|
| 45 |
+
"Qwen/Qwen2.5-Math-7B-Instruct",
|
| 46 |
+
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 47 |
+
"Qwen/Qwen2.5-Coder-14B-Instruct",
|
| 48 |
+
"Qwen/Qwen2.5-Coder-7B-Instruct",
|
| 49 |
+
"Qwen/Qwen2.5-Coder-3B-Instruct",
|
| 50 |
+
"Qwen/Qwen2.5-Coder-1.5B-Instruct",
|
| 51 |
+
"Qwen/Qwen2.5-Coder-0.5B-Instruct",
|
| 52 |
+
"Qwen/QwQ-32B-Preview",
|
| 53 |
+
"Qwen/Qwen2-VL-72B-Instruct",
|
| 54 |
+
"Qwen/Qwen2-VL-7B-Instruct",
|
| 55 |
+
"Qwen/Qwen2-VL-2B-Instruct",
|
| 56 |
+
"Qwen/Qwen2-Audio-7B-Instruct",
|
| 57 |
+
"Qwen/Qwen-Agent-Chat",
|
| 58 |
+
"Qwen/Qwen-VL-Chat",
|
| 59 |
+
],
|
| 60 |
+
"DeepSeek Models": [
|
| 61 |
+
"deepseek-ai/deepseek-llm-67b-chat",
|
| 62 |
+
"deepseek-ai/deepseek-llm-7b-chat",
|
| 63 |
+
"deepseek-ai/deepseek-coder-33b-instruct",
|
| 64 |
+
"deepseek-ai/deepseek-coder-7b-instruct",
|
| 65 |
+
"deepseek-ai/deepseek-coder-6.7b-instruct",
|
| 66 |
+
"deepseek-ai/deepseek-coder-1.3b-instruct",
|
| 67 |
+
"deepseek-ai/DeepSeek-V2-Chat",
|
| 68 |
+
"deepseek-ai/DeepSeek-V2-Lite-Chat",
|
| 69 |
+
"deepseek-ai/deepseek-math-7b-instruct",
|
| 70 |
+
"deepseek-ai/deepseek-moe-16b-chat",
|
| 71 |
+
"deepseek-ai/deepseek-vl-7b-chat",
|
| 72 |
+
"deepseek-ai/deepseek-vl-1.3b-chat",
|
| 73 |
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
|
| 74 |
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B",
|
| 75 |
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
|
| 76 |
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-8B",
|
| 77 |
+
"deepseek-ai/DeepSeek-Reasoner-R1",
|
| 78 |
+
],
|
| 79 |
+
},
|
| 80 |
+
"Image Processing": {
|
| 81 |
+
"Image Generation": [
|
| 82 |
+
"black-forest-labs/FLUX.1-dev",
|
| 83 |
+
"black-forest-labs/FLUX.1-schnell",
|
| 84 |
+
"black-forest-labs/FLUX.1-pro",
|
| 85 |
+
"runwayml/stable-diffusion-v1-5",
|
| 86 |
+
"stabilityai/stable-diffusion-xl-base-1.0",
|
| 87 |
+
"stabilityai/stable-diffusion-3-medium-diffusers",
|
| 88 |
+
"stabilityai/sd-turbo",
|
| 89 |
+
"kandinsky-community/kandinsky-2-2-decoder",
|
| 90 |
+
"playgroundai/playground-v2.5-1024px-aesthetic",
|
| 91 |
+
"midjourney/midjourney-v6",
|
| 92 |
+
],
|
| 93 |
+
"Image Editing": [
|
| 94 |
+
"timbrooks/instruct-pix2pix",
|
| 95 |
+
"runwayml/stable-diffusion-inpainting",
|
| 96 |
+
"stabilityai/stable-diffusion-xl-refiner-1.0",
|
| 97 |
+
"lllyasviel/control_v11p_sd15_inpaint",
|
| 98 |
+
"SG161222/RealVisXL_V4.0",
|
| 99 |
+
"ByteDance/SDXL-Lightning",
|
| 100 |
+
"segmind/SSD-1B",
|
| 101 |
+
"segmind/Segmind-Vega",
|
| 102 |
+
"playgroundai/playground-v2-1024px-aesthetic",
|
| 103 |
+
"stabilityai/stable-cascade",
|
| 104 |
+
"lllyasviel/ControlNet-v1-1",
|
| 105 |
+
"lllyasviel/sd-controlnet-canny",
|
| 106 |
+
"Monster-Labs/control_v1p_sd15_qrcode_monster",
|
| 107 |
+
"TencentARC/PhotoMaker",
|
| 108 |
+
"instantX/InstantID",
|
| 109 |
+
],
|
| 110 |
+
"Face Processing": [
|
| 111 |
+
"InsightFace/inswapper_128.onnx",
|
| 112 |
+
"deepinsight/insightface",
|
| 113 |
+
"TencentARC/GFPGAN",
|
| 114 |
+
"sczhou/CodeFormer",
|
| 115 |
+
"xinntao/Real-ESRGAN",
|
| 116 |
+
"ESRGAN/ESRGAN",
|
| 117 |
+
],
|
| 118 |
+
},
|
| 119 |
+
"Video Generation": {
|
| 120 |
+
"Text-to-Video": [
|
| 121 |
+
"ali-vilab/text-to-video-ms-1.7b",
|
| 122 |
+
"damo-vilab/text-to-video-ms-1.7b",
|
| 123 |
+
"modelscope/text-to-video-synthesis",
|
| 124 |
+
"camenduru/potat1",
|
| 125 |
+
"stabilityai/stable-video-diffusion-img2vid",
|
| 126 |
+
"stabilityai/stable-video-diffusion-img2vid-xt",
|
| 127 |
+
"ByteDance/AnimateDiff",
|
| 128 |
+
"guoyww/animatediff",
|
| 129 |
+
],
|
| 130 |
+
"Image-to-Video": [
|
| 131 |
+
"stabilityai/stable-video-diffusion-img2vid",
|
| 132 |
+
"stabilityai/stable-video-diffusion-img2vid-xt-1-1",
|
| 133 |
+
"TencentARC/MotionCtrl",
|
| 134 |
+
"ali-vilab/i2vgen-xl",
|
| 135 |
+
"Doubiiu/ToonCrafter",
|
| 136 |
+
],
|
| 137 |
+
"Video Editing": [
|
| 138 |
+
"MCG-NJU/VideoMAE",
|
| 139 |
+
"showlab/Tune-A-Video",
|
| 140 |
+
"Picsart-AI-Research/Text2Video-Zero",
|
| 141 |
+
"damo-vilab/MS-Vid2Vid-XL",
|
| 142 |
+
"kabachuha/sd-webui-deforum",
|
| 143 |
+
],
|
| 144 |
+
},
|
| 145 |
+
"AI Teacher & Education": {
|
| 146 |
+
"Math & Science": [
|
| 147 |
+
"Qwen/Qwen2.5-Math-72B-Instruct",
|
| 148 |
+
"Qwen/Qwen2.5-Math-7B-Instruct",
|
| 149 |
+
"deepseek-ai/deepseek-math-7b-instruct",
|
| 150 |
+
"mistralai/Mistral-Math-7B-v0.1",
|
| 151 |
+
"WizardLM/WizardMath-70B-V1.0",
|
| 152 |
+
"MathGPT/MathGPT-32B",
|
| 153 |
+
],
|
| 154 |
+
"Coding Tutor": [
|
| 155 |
+
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 156 |
+
"deepseek-ai/deepseek-coder-33b-instruct",
|
| 157 |
+
"WizardLM/WizardCoder-Python-34B-V1.0",
|
| 158 |
+
"bigcode/starcoder2-15b-instruct-v0.1",
|
| 159 |
+
"meta-llama/CodeLlama-34b-Instruct-hf",
|
| 160 |
+
],
|
| 161 |
+
"Language Learning": [
|
| 162 |
+
"facebook/nllb-200-3.3B",
|
| 163 |
+
"facebook/seamless-m4t-v2-large",
|
| 164 |
+
"Helsinki-NLP/opus-mt-multilingual",
|
| 165 |
+
"google/madlad400-10b-mt",
|
| 166 |
+
"Unbabel/TowerInstruct-7B-v0.1",
|
| 167 |
+
],
|
| 168 |
+
"General Education": [
|
| 169 |
+
"Qwen/Qwen2.5-72B-Instruct",
|
| 170 |
+
"microsoft/Phi-3-medium-128k-instruct",
|
| 171 |
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
| 172 |
+
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
|
| 173 |
+
"openchat/openchat-3.5-1210",
|
| 174 |
+
],
|
| 175 |
+
},
|
| 176 |
+
"Software Engineer Agent": {
|
| 177 |
+
"Code Generation": [
|
| 178 |
+
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 179 |
+
"Qwen/Qwen2.5-Coder-14B-Instruct",
|
| 180 |
+
"Qwen/Qwen2.5-Coder-7B-Instruct",
|
| 181 |
+
"deepseek-ai/deepseek-coder-33b-instruct",
|
| 182 |
+
"deepseek-ai/deepseek-coder-7b-instruct",
|
| 183 |
+
"deepseek-ai/deepseek-coder-6.7b-instruct",
|
| 184 |
+
"meta-llama/CodeLlama-70b-Instruct-hf",
|
| 185 |
+
"meta-llama/CodeLlama-34b-Instruct-hf",
|
| 186 |
+
"meta-llama/CodeLlama-13b-Instruct-hf",
|
| 187 |
+
"meta-llama/CodeLlama-7b-Instruct-hf",
|
| 188 |
+
],
|
| 189 |
+
"Code Analysis & Review": [
|
| 190 |
+
"bigcode/starcoder2-15b-instruct-v0.1",
|
| 191 |
+
"bigcode/starcoder2-7b",
|
| 192 |
+
"bigcode/starcoderbase-7b",
|
| 193 |
+
"WizardLM/WizardCoder-Python-34B-V1.0",
|
| 194 |
+
"WizardLM/WizardCoder-15B-V1.0",
|
| 195 |
+
"Phind/Phind-CodeLlama-34B-v2",
|
| 196 |
+
"codellama/CodeLlama-70b-Python-hf",
|
| 197 |
+
],
|
| 198 |
+
"Specialized Coding": [
|
| 199 |
+
"Salesforce/codegen25-7b-multi",
|
| 200 |
+
"Salesforce/codegen-16B-multi",
|
| 201 |
+
"replit/replit-code-v1-3b",
|
| 202 |
+
"NinedayWang/PolyCoder-2.7B",
|
| 203 |
+
"stabilityai/stablelm-base-alpha-7b-v2",
|
| 204 |
+
"teknium/OpenHermes-2.5-Mistral-7B",
|
| 205 |
+
],
|
| 206 |
+
"DevOps & Infrastructure": [
|
| 207 |
+
"deepseek-ai/deepseek-coder-33b-instruct",
|
| 208 |
+
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 209 |
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
| 210 |
+
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
|
| 211 |
+
],
|
| 212 |
+
},
|
| 213 |
+
"Audio Processing": {
|
| 214 |
+
"Text-to-Speech": [
|
| 215 |
+
"microsoft/speecht5_tts",
|
| 216 |
+
"facebook/mms-tts-eng",
|
| 217 |
+
"facebook/mms-tts-ara",
|
| 218 |
+
"coqui/XTTS-v2",
|
| 219 |
+
"suno/bark",
|
| 220 |
+
"parler-tts/parler-tts-large-v1",
|
| 221 |
+
"microsoft/DisTTS",
|
| 222 |
+
"facebook/fastspeech2-en-ljspeech",
|
| 223 |
+
"espnet/kan-bayashi_ljspeech_vits",
|
| 224 |
+
"facebook/tts_transformer-en-ljspeech",
|
| 225 |
+
"microsoft/SpeechT5",
|
| 226 |
+
"Voicemod/fastspeech2-en-male1",
|
| 227 |
+
"facebook/mms-tts-spa",
|
| 228 |
+
"facebook/mms-tts-fra",
|
| 229 |
+
"facebook/mms-tts-deu",
|
| 230 |
+
],
|
| 231 |
+
"Speech-to-Text": [
|
| 232 |
+
"openai/whisper-large-v3",
|
| 233 |
+
"openai/whisper-large-v2",
|
| 234 |
+
"openai/whisper-medium",
|
| 235 |
+
"openai/whisper-small",
|
| 236 |
+
"openai/whisper-base",
|
| 237 |
+
"openai/whisper-tiny",
|
| 238 |
+
"facebook/wav2vec2-large-960h",
|
| 239 |
+
"facebook/wav2vec2-base-960h",
|
| 240 |
+
"microsoft/unispeech-sat-large",
|
| 241 |
+
"nvidia/stt_en_conformer_ctc_large",
|
| 242 |
+
"speechbrain/asr-wav2vec2-commonvoice-en",
|
| 243 |
+
"facebook/mms-1b-all",
|
| 244 |
+
"facebook/seamless-m4t-v2-large",
|
| 245 |
+
"distil-whisper/distil-large-v3",
|
| 246 |
+
"distil-whisper/distil-medium.en",
|
| 247 |
+
],
|
| 248 |
+
},
|
| 249 |
+
"Multimodal AI": {
|
| 250 |
+
"Vision-Language": [
|
| 251 |
+
"microsoft/DialoGPT-large",
|
| 252 |
+
"microsoft/blip-image-captioning-large",
|
| 253 |
+
"microsoft/blip2-opt-6.7b",
|
| 254 |
+
"microsoft/blip2-flan-t5-xl",
|
| 255 |
+
"salesforce/blip-vqa-capfilt-large",
|
| 256 |
+
"dandelin/vilt-b32-finetuned-vqa",
|
| 257 |
+
"google/pix2struct-ai2d-base",
|
| 258 |
+
"microsoft/git-large-coco",
|
| 259 |
+
"microsoft/git-base-vqa",
|
| 260 |
+
"liuhaotian/llava-v1.6-34b",
|
| 261 |
+
"liuhaotian/llava-v1.6-vicuna-7b",
|
| 262 |
+
],
|
| 263 |
+
"Talking Avatars": [
|
| 264 |
+
"microsoft/SpeechT5-TTS-Avatar",
|
| 265 |
+
"Wav2Lip-HD",
|
| 266 |
+
"First-Order-Model",
|
| 267 |
+
"LipSync-Expert",
|
| 268 |
+
"DeepFaceLive",
|
| 269 |
+
"FaceSwapper-Live",
|
| 270 |
+
"RealTime-FaceRig",
|
| 271 |
+
"AI-Avatar-Generator",
|
| 272 |
+
"TalkingHead-3D",
|
| 273 |
+
],
|
| 274 |
+
},
|
| 275 |
+
"Arabic-English Models": [
|
| 276 |
+
"aubmindlab/bert-base-arabertv2",
|
| 277 |
+
"aubmindlab/aragpt2-base",
|
| 278 |
+
"aubmindlab/aragpt2-medium",
|
| 279 |
+
"CAMeL-Lab/bert-base-arabic-camelbert-mix",
|
| 280 |
+
"asafaya/bert-base-arabic",
|
| 281 |
+
"UBC-NLP/MARBERT",
|
| 282 |
+
"UBC-NLP/ARBERTv2",
|
| 283 |
+
"facebook/nllb-200-3.3B",
|
| 284 |
+
"facebook/m2m100_1.2B",
|
| 285 |
+
"Helsinki-NLP/opus-mt-ar-en",
|
| 286 |
+
"Helsinki-NLP/opus-mt-en-ar",
|
| 287 |
+
"microsoft/DialoGPT-medium-arabic",
|
| 288 |
+
],
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def init_database():
|
| 293 |
+
"""Initialize SQLite database for authentication"""
|
| 294 |
+
db_path = Path("openmanus.db")
|
| 295 |
+
conn = sqlite3.connect(db_path)
|
| 296 |
+
cursor = conn.cursor()
|
| 297 |
+
|
| 298 |
+
# Create users table
|
| 299 |
+
cursor.execute(
|
| 300 |
+
"""
|
| 301 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 302 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 303 |
+
mobile_number TEXT UNIQUE NOT NULL,
|
| 304 |
+
full_name TEXT NOT NULL,
|
| 305 |
+
password_hash TEXT NOT NULL,
|
| 306 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 307 |
+
last_login TIMESTAMP,
|
| 308 |
+
is_active BOOLEAN DEFAULT 1
|
| 309 |
+
)
|
| 310 |
+
"""
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# Create sessions table
|
| 314 |
+
cursor.execute(
|
| 315 |
+
"""
|
| 316 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 317 |
+
id TEXT PRIMARY KEY,
|
| 318 |
+
user_id INTEGER NOT NULL,
|
| 319 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 320 |
+
expires_at TIMESTAMP NOT NULL,
|
| 321 |
+
ip_address TEXT,
|
| 322 |
+
user_agent TEXT,
|
| 323 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 324 |
+
)
|
| 325 |
+
"""
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
# Create model usage table
|
| 329 |
+
cursor.execute(
|
| 330 |
+
"""
|
| 331 |
+
CREATE TABLE IF NOT EXISTS model_usage (
|
| 332 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 333 |
+
user_id INTEGER,
|
| 334 |
+
model_name TEXT NOT NULL,
|
| 335 |
+
category TEXT NOT NULL,
|
| 336 |
+
input_text TEXT,
|
| 337 |
+
output_text TEXT,
|
| 338 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 339 |
+
processing_time REAL,
|
| 340 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 341 |
+
)
|
| 342 |
+
"""
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
conn.commit()
|
| 346 |
+
conn.close()
|
| 347 |
+
return True
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def hash_password(password):
|
| 351 |
+
"""Hash password using SHA-256"""
|
| 352 |
+
return hashlib.sha256(password.encode()).hexdigest()
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def signup_user(mobile, name, password, confirm_password):
|
| 356 |
+
"""User registration with mobile number"""
|
| 357 |
+
if not all([mobile, name, password, confirm_password]):
|
| 358 |
+
return "❌ Please fill in all fields"
|
| 359 |
+
|
| 360 |
+
if password != confirm_password:
|
| 361 |
+
return "❌ Passwords do not match"
|
| 362 |
+
|
| 363 |
+
if len(password) < 6:
|
| 364 |
+
return "❌ Password must be at least 6 characters"
|
| 365 |
+
|
| 366 |
+
# Validate mobile number
|
| 367 |
+
if not mobile.replace("+", "").replace("-", "").replace(" ", "").isdigit():
|
| 368 |
+
return "❌ Please enter a valid mobile number"
|
| 369 |
+
|
| 370 |
+
try:
|
| 371 |
+
conn = sqlite3.connect("openmanus.db")
|
| 372 |
+
cursor = conn.cursor()
|
| 373 |
+
|
| 374 |
+
# Check if mobile number already exists
|
| 375 |
+
cursor.execute("SELECT id FROM users WHERE mobile_number = ?", (mobile,))
|
| 376 |
+
if cursor.fetchone():
|
| 377 |
+
conn.close()
|
| 378 |
+
return "❌ Mobile number already registered"
|
| 379 |
+
|
| 380 |
+
# Create new user
|
| 381 |
+
password_hash = hash_password(password)
|
| 382 |
+
cursor.execute(
|
| 383 |
+
"""
|
| 384 |
+
INSERT INTO users (mobile_number, full_name, password_hash)
|
| 385 |
+
VALUES (?, ?, ?)
|
| 386 |
+
""",
|
| 387 |
+
(mobile, name, password_hash),
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
conn.commit()
|
| 391 |
+
conn.close()
|
| 392 |
+
|
| 393 |
+
return f"✅ Account created successfully for {name}! Welcome to OpenManus Platform."
|
| 394 |
+
|
| 395 |
+
except Exception as e:
|
| 396 |
+
return f"❌ Registration failed: {str(e)}"
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def login_user(mobile, password):
|
| 400 |
+
"""User authentication"""
|
| 401 |
+
if not mobile or not password:
|
| 402 |
+
return "❌ Please provide mobile number and password"
|
| 403 |
+
|
| 404 |
+
try:
|
| 405 |
+
conn = sqlite3.connect("openmanus.db")
|
| 406 |
+
cursor = conn.cursor()
|
| 407 |
+
|
| 408 |
+
# Verify credentials
|
| 409 |
+
password_hash = hash_password(password)
|
| 410 |
+
cursor.execute(
|
| 411 |
+
"""
|
| 412 |
+
SELECT id, full_name FROM users
|
| 413 |
+
WHERE mobile_number = ? AND password_hash = ? AND is_active = 1
|
| 414 |
+
""",
|
| 415 |
+
(mobile, password_hash),
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
user = cursor.fetchone()
|
| 419 |
+
if user:
|
| 420 |
+
# Update last login
|
| 421 |
+
cursor.execute(
|
| 422 |
+
"""
|
| 423 |
+
UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?
|
| 424 |
+
""",
|
| 425 |
+
(user[0],),
|
| 426 |
+
)
|
| 427 |
+
conn.commit()
|
| 428 |
+
conn.close()
|
| 429 |
+
|
| 430 |
+
return f"✅ Welcome back, {user[1]}! Login successful."
|
| 431 |
+
else:
|
| 432 |
+
conn.close()
|
| 433 |
+
return "❌ Invalid mobile number or password"
|
| 434 |
+
|
| 435 |
+
except Exception as e:
|
| 436 |
+
return f"❌ Login failed: {str(e)}"
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def use_ai_model(model_name, input_text, user_session="guest"):
|
| 440 |
+
"""Intelligently route AI model usage based on model type"""
|
| 441 |
+
if not input_text.strip():
|
| 442 |
+
return "Please enter some text for the AI model to process."
|
| 443 |
+
|
| 444 |
+
# Intelligent response templates based on model category
|
| 445 |
+
response_templates = {
|
| 446 |
+
"text": f"🧠 {model_name} processed: '{input_text}'\n\n✨ AI Response: This is a simulated response from the {model_name} model. In production, this would connect to the actual model API.",
|
| 447 |
+
"image_gen": f"🎨 {model_name} generating image: '{input_text}'\n\n📸 Output: High-quality image generated based on your prompt (simulated)",
|
| 448 |
+
"image_edit": f"✏️ {model_name} editing image: '{input_text}'\n\n�️ Output: Image manipulation complete with your instructions applied (simulated)",
|
| 449 |
+
"video": f"🎬 {model_name} creating video: '{input_text}'\n\n🎥 Output: Video generated/animated successfully (simulated)",
|
| 450 |
+
"audio": f"🎵 {model_name} audio processing: '{input_text}'\n\n🔊 Output: Audio generated/transcribed (simulated)",
|
| 451 |
+
"education": f"🎓 {model_name} teaching: '{input_text}'\n\n📚 AI Teacher Response: Step-by-step explanation with examples (simulated)",
|
| 452 |
+
"software_engineer": f"💻 {model_name} coding solution: '{input_text}'\n\n🚀 Software Engineer Agent: Production-ready code with best practices, error handling, and documentation (simulated)",
|
| 453 |
+
"multimodal": f"🤖 {model_name} multimodal processing: '{input_text}'\n\n🎯 Output: Combined AI analysis complete (simulated)",
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
# Intelligent model routing - Agent determines the best approach
|
| 457 |
+
model_lower = model_name.lower()
|
| 458 |
+
|
| 459 |
+
# Software Engineer Agent (production code, architecture, DevOps)
|
| 460 |
+
if any(x in model_lower for x in ["codellama", "starcoder", "codegen", "replit", "polycoder", "stablelm", "hermes"]):
|
| 461 |
+
response_type = "software_engineer"
|
| 462 |
+
|
| 463 |
+
# Image Editing Agent (separate from generation)
|
| 464 |
+
elif any(x in model_lower for x in ["pix2pix", "inpaint", "controlnet", "photomaker", "instantid", "refiner"]):
|
| 465 |
+
response_type = "image_edit"
|
| 466 |
+
|
| 467 |
+
# Image Generation Agent
|
| 468 |
+
elif any(x in model_lower for x in ["flux", "diffusion", "stable-diffusion", "sdxl", "kandinsky", "midjourney"]):
|
| 469 |
+
response_type = "image_gen"
|
| 470 |
+
|
| 471 |
+
# Education Agent (Math, Language Learning, Teaching - NOT coding)
|
| 472 |
+
elif any(x in model_lower for x in ["math", "teacher", "education", "nllb", "translate", "wizard"]) and "coder" not in model_lower:
|
| 473 |
+
response_type = "education"
|
| 474 |
+
|
| 475 |
+
# Coder Agent (Qwen/DeepSeek coder models)
|
| 476 |
+
elif "coder" in model_lower:
|
| 477 |
+
response_type = "software_engineer"
|
| 478 |
+
|
| 479 |
+
# Audio Agent
|
| 480 |
+
elif any(x in model_lower for x in ["tts", "speech", "audio", "whisper", "wav2vec", "bark", "speecht5"]):
|
| 481 |
+
response_type = "audio"
|
| 482 |
+
|
| 483 |
+
# Face Processing Agent
|
| 484 |
+
elif any(x in model_lower for x in ["face", "avatar", "talking", "wav2lip", "gfpgan", "codeformer", "insight"]):
|
| 485 |
+
response_type = "multimodal"
|
| 486 |
+
|
| 487 |
+
# Multimodal Agent (Vision-Language)
|
| 488 |
+
elif any(x in model_lower for x in ["vl", "blip", "vision", "llava", "vqa", "multimodal"]):
|
| 489 |
+
response_type = "multimodal"
|
| 490 |
+
|
| 491 |
+
# Text Generation Agent (default)
|
| 492 |
+
else:
|
| 493 |
+
response_type = "text"
|
| 494 |
+
|
| 495 |
+
return response_templates[response_type]
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def get_cloudflare_status():
|
| 499 |
+
"""Get Cloudflare services status"""
|
| 500 |
+
services = []
|
| 501 |
+
|
| 502 |
+
if CLOUDFLARE_CONFIG["d1_database_id"]:
|
| 503 |
+
services.append("✅ D1 Database Connected")
|
| 504 |
+
else:
|
| 505 |
+
services.append("⚙️ D1 Database (Configure CLOUDFLARE_D1_DATABASE_ID)")
|
| 506 |
+
|
| 507 |
+
if CLOUDFLARE_CONFIG["r2_bucket_name"]:
|
| 508 |
+
services.append("✅ R2 Storage Connected")
|
| 509 |
+
else:
|
| 510 |
+
services.append("⚙️ R2 Storage (Configure CLOUDFLARE_R2_BUCKET_NAME)")
|
| 511 |
+
|
| 512 |
+
if CLOUDFLARE_CONFIG["kv_namespace_id"]:
|
| 513 |
+
services.append("✅ KV Cache Connected")
|
| 514 |
+
else:
|
| 515 |
+
services.append("⚙️ KV Cache (Configure CLOUDFLARE_KV_NAMESPACE_ID)")
|
| 516 |
+
|
| 517 |
+
if CLOUDFLARE_CONFIG["durable_objects_id"]:
|
| 518 |
+
services.append("✅ Durable Objects Connected")
|
| 519 |
+
else:
|
| 520 |
+
services.append("⚙️ Durable Objects (Configure CLOUDFLARE_DURABLE_OBJECTS_ID)")
|
| 521 |
+
|
| 522 |
+
return "\n".join(services)
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
# Initialize database
|
| 526 |
+
init_database()
|
| 527 |
+
|
| 528 |
+
# Create Gradio interface
|
| 529 |
+
with gr.Blocks(
|
| 530 |
+
title="OpenManus - Complete AI Platform",
|
| 531 |
+
theme=gr.themes.Soft(),
|
| 532 |
+
css="""
|
| 533 |
+
.container { max-width: 1400px; margin: 0 auto; }
|
| 534 |
+
.header { text-align: center; padding: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 15px; margin-bottom: 25px; }
|
| 535 |
+
.section { background: white; padding: 25px; border-radius: 15px; margin: 15px 0; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
| 536 |
+
""",
|
| 537 |
+
) as app:
|
| 538 |
+
|
| 539 |
+
# Header
|
| 540 |
+
gr.HTML(
|
| 541 |
+
"""
|
| 542 |
+
<div class="header">
|
| 543 |
+
<h1>🤖 OpenManus - Complete AI Platform</h1>
|
| 544 |
+
<p><strong>Mobile Authentication + 200+ AI Models + Cloudflare Services</strong></p>
|
| 545 |
+
<p>🧠 Qwen & DeepSeek | 🖼️ Image Processing | 🎵 TTS/STT | 👤 Face Swap | 🌍 Arabic-English | ☁️ Cloud Integration</p>
|
| 546 |
+
</div>
|
| 547 |
+
"""
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
with gr.Row():
|
| 551 |
+
# Authentication Section
|
| 552 |
+
with gr.Column(scale=1, elem_classes="section"):
|
| 553 |
+
gr.Markdown("## 🔐 Authentication System")
|
| 554 |
+
|
| 555 |
+
with gr.Tab("Sign Up"):
|
| 556 |
+
gr.Markdown("### Create New Account")
|
| 557 |
+
signup_mobile = gr.Textbox(
|
| 558 |
+
label="Mobile Number",
|
| 559 |
+
placeholder="+1234567890",
|
| 560 |
+
info="Enter your mobile number with country code",
|
| 561 |
+
)
|
| 562 |
+
signup_name = gr.Textbox(
|
| 563 |
+
label="Full Name", placeholder="Your full name"
|
| 564 |
+
)
|
| 565 |
+
signup_password = gr.Textbox(
|
| 566 |
+
label="Password", type="password", info="Minimum 6 characters"
|
| 567 |
+
)
|
| 568 |
+
signup_confirm = gr.Textbox(label="Confirm Password", type="password")
|
| 569 |
+
signup_btn = gr.Button("Create Account", variant="primary")
|
| 570 |
+
signup_result = gr.Textbox(
|
| 571 |
+
label="Registration Status", interactive=False, lines=2
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
signup_btn.click(
|
| 575 |
+
signup_user,
|
| 576 |
+
[signup_mobile, signup_name, signup_password, signup_confirm],
|
| 577 |
+
signup_result,
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
with gr.Tab("Login"):
|
| 581 |
+
gr.Markdown("### Access Your Account")
|
| 582 |
+
login_mobile = gr.Textbox(
|
| 583 |
+
label="Mobile Number", placeholder="+1234567890"
|
| 584 |
+
)
|
| 585 |
+
login_password = gr.Textbox(label="Password", type="password")
|
| 586 |
+
login_btn = gr.Button("Login", variant="primary")
|
| 587 |
+
login_result = gr.Textbox(
|
| 588 |
+
label="Login Status", interactive=False, lines=2
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
login_btn.click(
|
| 592 |
+
login_user, [login_mobile, login_password], login_result
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
# AI Models Section
|
| 596 |
+
with gr.Column(scale=2, elem_classes="section"):
|
| 597 |
+
gr.Markdown("## 🤖 AI Models Hub (200+ Models)")
|
| 598 |
+
|
| 599 |
+
with gr.Tab("Text Generation"):
|
| 600 |
+
with gr.Row():
|
| 601 |
+
with gr.Column():
|
| 602 |
+
gr.Markdown("### Qwen Models (35 models)")
|
| 603 |
+
qwen_model = gr.Dropdown(
|
| 604 |
+
choices=AI_MODELS["Text Generation"]["Qwen Models"],
|
| 605 |
+
label="Select Qwen Model",
|
| 606 |
+
value="Qwen/Qwen2.5-72B-Instruct",
|
| 607 |
+
)
|
| 608 |
+
qwen_input = gr.Textbox(
|
| 609 |
+
label="Input Text",
|
| 610 |
+
placeholder="Enter your prompt for Qwen...",
|
| 611 |
+
lines=3,
|
| 612 |
+
)
|
| 613 |
+
qwen_btn = gr.Button("Generate with Qwen")
|
| 614 |
+
qwen_output = gr.Textbox(
|
| 615 |
+
label="Qwen Response", lines=5, interactive=False
|
| 616 |
+
)
|
| 617 |
+
qwen_btn.click(
|
| 618 |
+
use_ai_model, [qwen_model, qwen_input], qwen_output
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
with gr.Column():
|
| 622 |
+
gr.Markdown("### DeepSeek Models (17 models)")
|
| 623 |
+
deepseek_model = gr.Dropdown(
|
| 624 |
+
choices=AI_MODELS["Text Generation"]["DeepSeek Models"],
|
| 625 |
+
label="Select DeepSeek Model",
|
| 626 |
+
value="deepseek-ai/deepseek-llm-67b-chat",
|
| 627 |
+
)
|
| 628 |
+
deepseek_input = gr.Textbox(
|
| 629 |
+
label="Input Text",
|
| 630 |
+
placeholder="Enter your prompt for DeepSeek...",
|
| 631 |
+
lines=3,
|
| 632 |
+
)
|
| 633 |
+
deepseek_btn = gr.Button("Generate with DeepSeek")
|
| 634 |
+
deepseek_output = gr.Textbox(
|
| 635 |
+
label="DeepSeek Response", lines=5, interactive=False
|
| 636 |
+
)
|
| 637 |
+
deepseek_btn.click(
|
| 638 |
+
use_ai_model,
|
| 639 |
+
[deepseek_model, deepseek_input],
|
| 640 |
+
deepseek_output,
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
with gr.Tab("Image Processing"):
|
| 644 |
+
with gr.Row():
|
| 645 |
+
with gr.Column():
|
| 646 |
+
gr.Markdown("### Image Generation")
|
| 647 |
+
img_gen_model = gr.Dropdown(
|
| 648 |
+
choices=AI_MODELS["Image Processing"]["Image Generation"],
|
| 649 |
+
label="Select Image Model",
|
| 650 |
+
value="black-forest-labs/FLUX.1-dev",
|
| 651 |
+
)
|
| 652 |
+
img_prompt = gr.Textbox(
|
| 653 |
+
label="Image Prompt",
|
| 654 |
+
placeholder="Describe the image you want to generate...",
|
| 655 |
+
lines=2,
|
| 656 |
+
)
|
| 657 |
+
img_gen_btn = gr.Button("Generate Image")
|
| 658 |
+
img_gen_output = gr.Textbox(
|
| 659 |
+
label="Generation Status", lines=4, interactive=False
|
| 660 |
+
)
|
| 661 |
+
img_gen_btn.click(
|
| 662 |
+
use_ai_model, [img_gen_model, img_prompt], img_gen_output
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
with gr.Column():
|
| 666 |
+
gr.Markdown("### Face Processing & Editing")
|
| 667 |
+
face_model = gr.Dropdown(
|
| 668 |
+
choices=AI_MODELS["Image Processing"]["Face Processing"],
|
| 669 |
+
label="Select Face Model",
|
| 670 |
+
value="InsightFace/inswapper_128.onnx",
|
| 671 |
+
)
|
| 672 |
+
face_input = gr.Textbox(
|
| 673 |
+
label="Face Processing Task",
|
| 674 |
+
placeholder="Describe face swap or enhancement task...",
|
| 675 |
+
lines=2,
|
| 676 |
+
)
|
| 677 |
+
face_btn = gr.Button("Process Face")
|
| 678 |
+
face_output = gr.Textbox(
|
| 679 |
+
label="Processing Status", lines=4, interactive=False
|
| 680 |
+
)
|
| 681 |
+
face_btn.click(
|
| 682 |
+
use_ai_model, [face_model, face_input], face_output
|
| 683 |
+
)
|
| 684 |
+
|
| 685 |
+
with gr.Tab("Image Editing"):
|
| 686 |
+
gr.Markdown("### ✏️ Advanced Image Editing & Manipulation (15+ models)")
|
| 687 |
+
with gr.Row():
|
| 688 |
+
with gr.Column():
|
| 689 |
+
gr.Markdown("### Image Editing Models")
|
| 690 |
+
edit_model = gr.Dropdown(
|
| 691 |
+
choices=AI_MODELS["Image Processing"]["Image Editing"],
|
| 692 |
+
label="Select Image Editing Model",
|
| 693 |
+
value="timbrooks/instruct-pix2pix",
|
| 694 |
+
)
|
| 695 |
+
edit_input = gr.Textbox(
|
| 696 |
+
label="Editing Instructions",
|
| 697 |
+
placeholder="Describe how to edit the image (e.g., 'make it winter', 'remove background')...",
|
| 698 |
+
lines=3,
|
| 699 |
+
)
|
| 700 |
+
edit_btn = gr.Button("Edit Image")
|
| 701 |
+
edit_output = gr.Textbox(
|
| 702 |
+
label="Editing Status", lines=4, interactive=False
|
| 703 |
+
)
|
| 704 |
+
edit_btn.click(
|
| 705 |
+
use_ai_model, [edit_model, edit_input], edit_output
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
with gr.Tab("Video Generation"):
|
| 709 |
+
gr.Markdown("### 🎬 Video Generation & Editing (18+ models)")
|
| 710 |
+
with gr.Row():
|
| 711 |
+
with gr.Column():
|
| 712 |
+
gr.Markdown("### Text-to-Video")
|
| 713 |
+
video_text_model = gr.Dropdown(
|
| 714 |
+
choices=AI_MODELS["Video Generation"]["Text-to-Video"],
|
| 715 |
+
label="Select Text-to-Video Model",
|
| 716 |
+
value="ali-vilab/text-to-video-ms-1.7b",
|
| 717 |
+
)
|
| 718 |
+
video_text_input = gr.Textbox(
|
| 719 |
+
label="Video Description",
|
| 720 |
+
placeholder="Describe the video you want to generate...",
|
| 721 |
+
lines=3,
|
| 722 |
+
)
|
| 723 |
+
video_text_btn = gr.Button("Generate Video from Text")
|
| 724 |
+
video_text_output = gr.Textbox(
|
| 725 |
+
label="Video Generation Status", lines=4, interactive=False
|
| 726 |
+
)
|
| 727 |
+
video_text_btn.click(
|
| 728 |
+
use_ai_model,
|
| 729 |
+
[video_text_model, video_text_input],
|
| 730 |
+
video_text_output,
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
with gr.Column():
|
| 734 |
+
gr.Markdown("### Image-to-Video & Video Editing")
|
| 735 |
+
video_img_model = gr.Dropdown(
|
| 736 |
+
choices=AI_MODELS["Video Generation"]["Image-to-Video"],
|
| 737 |
+
label="Select Image-to-Video Model",
|
| 738 |
+
value="stabilityai/stable-video-diffusion-img2vid",
|
| 739 |
+
)
|
| 740 |
+
video_img_input = gr.Textbox(
|
| 741 |
+
label="Animation Instructions",
|
| 742 |
+
placeholder="Describe how to animate the image or edit video...",
|
| 743 |
+
lines=3,
|
| 744 |
+
)
|
| 745 |
+
video_img_btn = gr.Button("Animate Image")
|
| 746 |
+
video_img_output = gr.Textbox(
|
| 747 |
+
label="Video Processing Status", lines=4, interactive=False
|
| 748 |
+
)
|
| 749 |
+
video_img_btn.click(
|
| 750 |
+
use_ai_model,
|
| 751 |
+
[video_img_model, video_img_input],
|
| 752 |
+
video_img_output,
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
with gr.Tab("AI Teacher & Education"):
|
| 756 |
+
gr.Markdown(
|
| 757 |
+
"### 🎓 AI Teacher - Math, Coding, Languages & More (20+ models)"
|
| 758 |
+
)
|
| 759 |
+
with gr.Row():
|
| 760 |
+
with gr.Column():
|
| 761 |
+
gr.Markdown("### Math & Science Tutor")
|
| 762 |
+
math_model = gr.Dropdown(
|
| 763 |
+
choices=AI_MODELS["AI Teacher & Education"][
|
| 764 |
+
"Math & Science"
|
| 765 |
+
],
|
| 766 |
+
label="Select Math/Science Model",
|
| 767 |
+
value="Qwen/Qwen2.5-Math-72B-Instruct",
|
| 768 |
+
)
|
| 769 |
+
math_input = gr.Textbox(
|
| 770 |
+
label="Math/Science Question",
|
| 771 |
+
placeholder="Ask a math or science question...",
|
| 772 |
+
lines=3,
|
| 773 |
+
)
|
| 774 |
+
math_btn = gr.Button("Solve with AI Teacher")
|
| 775 |
+
math_output = gr.Textbox(
|
| 776 |
+
label="Solution & Explanation", lines=6, interactive=False
|
| 777 |
+
)
|
| 778 |
+
math_btn.click(
|
| 779 |
+
use_ai_model, [math_model, math_input], math_output
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
with gr.Column():
|
| 783 |
+
gr.Markdown("### Coding Tutor & Language Learning")
|
| 784 |
+
edu_model = gr.Dropdown(
|
| 785 |
+
choices=AI_MODELS["AI Teacher & Education"]["Coding Tutor"],
|
| 786 |
+
label="Select Educational Model",
|
| 787 |
+
value="Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 788 |
+
)
|
| 789 |
+
edu_input = gr.Textbox(
|
| 790 |
+
label="Learning Request",
|
| 791 |
+
placeholder="Ask for coding help or language learning...",
|
| 792 |
+
lines=3,
|
| 793 |
+
)
|
| 794 |
+
edu_btn = gr.Button("Learn with AI")
|
| 795 |
+
edu_output = gr.Textbox(
|
| 796 |
+
label="Educational Response", lines=6, interactive=False
|
| 797 |
+
)
|
| 798 |
+
edu_btn.click(use_ai_model, [edu_model, edu_input], edu_output)
|
| 799 |
+
|
| 800 |
+
with gr.Tab("Software Engineer Agent"):
|
| 801 |
+
gr.Markdown(
|
| 802 |
+
"### 💻 Software Engineer Agent - Production Code, Architecture & DevOps (27+ models)"
|
| 803 |
+
)
|
| 804 |
+
with gr.Row():
|
| 805 |
+
with gr.Column():
|
| 806 |
+
gr.Markdown("### Code Generation & Development")
|
| 807 |
+
code_gen_model = gr.Dropdown(
|
| 808 |
+
choices=AI_MODELS["Software Engineer Agent"][
|
| 809 |
+
"Code Generation"
|
| 810 |
+
],
|
| 811 |
+
label="Select Code Generation Model",
|
| 812 |
+
value="Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 813 |
+
)
|
| 814 |
+
code_gen_input = gr.Textbox(
|
| 815 |
+
label="Coding Task",
|
| 816 |
+
placeholder="Describe the code you need (e.g., 'Create a REST API', 'Build a database schema')...",
|
| 817 |
+
lines=4,
|
| 818 |
+
)
|
| 819 |
+
code_gen_btn = gr.Button("Generate Production Code")
|
| 820 |
+
code_gen_output = gr.Textbox(
|
| 821 |
+
label="Generated Code & Documentation",
|
| 822 |
+
lines=8,
|
| 823 |
+
interactive=False,
|
| 824 |
+
)
|
| 825 |
+
code_gen_btn.click(
|
| 826 |
+
use_ai_model,
|
| 827 |
+
[code_gen_model, code_gen_input],
|
| 828 |
+
code_gen_output,
|
| 829 |
+
)
|
| 830 |
+
|
| 831 |
+
with gr.Column():
|
| 832 |
+
gr.Markdown("### Code Review & Analysis")
|
| 833 |
+
code_review_model = gr.Dropdown(
|
| 834 |
+
choices=AI_MODELS["Software Engineer Agent"][
|
| 835 |
+
"Code Analysis & Review"
|
| 836 |
+
],
|
| 837 |
+
label="Select Code Review Model",
|
| 838 |
+
value="bigcode/starcoder2-15b-instruct-v0.1",
|
| 839 |
+
)
|
| 840 |
+
code_review_input = gr.Textbox(
|
| 841 |
+
label="Code to Review",
|
| 842 |
+
placeholder="Paste your code for review, optimization, or debugging...",
|
| 843 |
+
lines=4,
|
| 844 |
+
)
|
| 845 |
+
code_review_btn = gr.Button("Review Code")
|
| 846 |
+
code_review_output = gr.Textbox(
|
| 847 |
+
label="Code Review & Suggestions",
|
| 848 |
+
lines=8,
|
| 849 |
+
interactive=False,
|
| 850 |
+
)
|
| 851 |
+
code_review_btn.click(
|
| 852 |
+
use_ai_model,
|
| 853 |
+
[code_review_model, code_review_input],
|
| 854 |
+
code_review_output,
|
| 855 |
+
)
|
| 856 |
+
|
| 857 |
+
with gr.Tab("Audio Processing"):
|
| 858 |
+
with gr.Row():
|
| 859 |
+
with gr.Column():
|
| 860 |
+
gr.Markdown("### Text-to-Speech (15 models)")
|
| 861 |
+
tts_model = gr.Dropdown(
|
| 862 |
+
choices=AI_MODELS["Audio Processing"]["Text-to-Speech"],
|
| 863 |
+
label="Select TTS Model",
|
| 864 |
+
value="microsoft/speecht5_tts",
|
| 865 |
+
)
|
| 866 |
+
tts_text = gr.Textbox(
|
| 867 |
+
label="Text to Speak",
|
| 868 |
+
placeholder="Enter text to convert to speech...",
|
| 869 |
+
lines=3,
|
| 870 |
+
)
|
| 871 |
+
tts_btn = gr.Button("Generate Speech")
|
| 872 |
+
tts_output = gr.Textbox(
|
| 873 |
+
label="TTS Status", lines=4, interactive=False
|
| 874 |
+
)
|
| 875 |
+
tts_btn.click(use_ai_model, [tts_model, tts_text], tts_output)
|
| 876 |
+
|
| 877 |
+
with gr.Column():
|
| 878 |
+
gr.Markdown("### Speech-to-Text (15 models)")
|
| 879 |
+
stt_model = gr.Dropdown(
|
| 880 |
+
choices=AI_MODELS["Audio Processing"]["Speech-to-Text"],
|
| 881 |
+
label="Select STT Model",
|
| 882 |
+
value="openai/whisper-large-v3",
|
| 883 |
+
)
|
| 884 |
+
stt_input = gr.Textbox(
|
| 885 |
+
label="Audio Description",
|
| 886 |
+
placeholder="Describe audio file to transcribe...",
|
| 887 |
+
lines=3,
|
| 888 |
+
)
|
| 889 |
+
stt_btn = gr.Button("Transcribe Audio")
|
| 890 |
+
stt_output = gr.Textbox(
|
| 891 |
+
label="STT Status", lines=4, interactive=False
|
| 892 |
+
)
|
| 893 |
+
stt_btn.click(use_ai_model, [stt_model, stt_input], stt_output)
|
| 894 |
+
|
| 895 |
+
with gr.Tab("Multimodal & Avatars"):
|
| 896 |
+
with gr.Row():
|
| 897 |
+
with gr.Column():
|
| 898 |
+
gr.Markdown("### Vision-Language Models")
|
| 899 |
+
vl_model = gr.Dropdown(
|
| 900 |
+
choices=AI_MODELS["Multimodal AI"]["Vision-Language"],
|
| 901 |
+
label="Select VL Model",
|
| 902 |
+
value="liuhaotian/llava-v1.6-34b",
|
| 903 |
+
)
|
| 904 |
+
vl_input = gr.Textbox(
|
| 905 |
+
label="Vision-Language Task",
|
| 906 |
+
placeholder="Describe image analysis or VQA task...",
|
| 907 |
+
lines=3,
|
| 908 |
+
)
|
| 909 |
+
vl_btn = gr.Button("Process with VL Model")
|
| 910 |
+
vl_output = gr.Textbox(
|
| 911 |
+
label="VL Response", lines=4, interactive=False
|
| 912 |
+
)
|
| 913 |
+
vl_btn.click(use_ai_model, [vl_model, vl_input], vl_output)
|
| 914 |
+
|
| 915 |
+
with gr.Column():
|
| 916 |
+
gr.Markdown("### Talking Avatars")
|
| 917 |
+
avatar_model = gr.Dropdown(
|
| 918 |
+
choices=AI_MODELS["Multimodal AI"]["Talking Avatars"],
|
| 919 |
+
label="Select Avatar Model",
|
| 920 |
+
value="Wav2Lip-HD",
|
| 921 |
+
)
|
| 922 |
+
avatar_input = gr.Textbox(
|
| 923 |
+
label="Avatar Generation Task",
|
| 924 |
+
placeholder="Describe talking avatar or lip-sync task...",
|
| 925 |
+
lines=3,
|
| 926 |
+
)
|
| 927 |
+
avatar_btn = gr.Button("Generate Avatar")
|
| 928 |
+
avatar_output = gr.Textbox(
|
| 929 |
+
label="Avatar Status", lines=4, interactive=False
|
| 930 |
+
)
|
| 931 |
+
avatar_btn.click(
|
| 932 |
+
use_ai_model, [avatar_model, avatar_input], avatar_output
|
| 933 |
+
)
|
| 934 |
+
|
| 935 |
+
with gr.Tab("Arabic-English"):
|
| 936 |
+
gr.Markdown("### Arabic-English Interactive Models (12 models)")
|
| 937 |
+
arabic_model = gr.Dropdown(
|
| 938 |
+
choices=AI_MODELS["Arabic-English Models"],
|
| 939 |
+
label="Select Arabic-English Model",
|
| 940 |
+
value="aubmindlab/bert-base-arabertv2",
|
| 941 |
+
)
|
| 942 |
+
arabic_input = gr.Textbox(
|
| 943 |
+
label="Text (Arabic or English)",
|
| 944 |
+
placeholder="أدخل النص باللغة العربية أو الإنجليزية / Enter text in Arabic or English...",
|
| 945 |
+
lines=4,
|
| 946 |
+
)
|
| 947 |
+
arabic_btn = gr.Button("Process Arabic-English")
|
| 948 |
+
arabic_output = gr.Textbox(
|
| 949 |
+
label="Processing Result", lines=6, interactive=False
|
| 950 |
+
)
|
| 951 |
+
arabic_btn.click(
|
| 952 |
+
use_ai_model, [arabic_model, arabic_input], arabic_output
|
| 953 |
+
)
|
| 954 |
+
|
| 955 |
+
# Services Status Section
|
| 956 |
+
with gr.Row():
|
| 957 |
+
with gr.Column(elem_classes="section"):
|
| 958 |
+
gr.Markdown("## ☁️ Cloudflare Services Integration")
|
| 959 |
+
|
| 960 |
+
with gr.Row():
|
| 961 |
+
with gr.Column():
|
| 962 |
+
gr.Markdown("### Services Status")
|
| 963 |
+
services_status = gr.Textbox(
|
| 964 |
+
label="Cloudflare Services",
|
| 965 |
+
value=get_cloudflare_status(),
|
| 966 |
+
lines=6,
|
| 967 |
+
interactive=False,
|
| 968 |
+
)
|
| 969 |
+
refresh_btn = gr.Button("Refresh Status")
|
| 970 |
+
refresh_btn.click(
|
| 971 |
+
lambda: get_cloudflare_status(), outputs=services_status
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
with gr.Column():
|
| 975 |
+
gr.Markdown("### Configuration")
|
| 976 |
+
gr.HTML(
|
| 977 |
+
"""
|
| 978 |
+
<div style="background: #f0f8ff; padding: 15px; border-radius: 10px;">
|
| 979 |
+
<h4>Environment Variables:</h4>
|
| 980 |
+
<ul>
|
| 981 |
+
<li><code>CLOUDFLARE_API_TOKEN</code> - API authentication</li>
|
| 982 |
+
<li><code>CLOUDFLARE_ACCOUNT_ID</code> - Account identifier</li>
|
| 983 |
+
<li><code>CLOUDFLARE_D1_DATABASE_ID</code> - D1 database</li>
|
| 984 |
+
<li><code>CLOUDFLARE_R2_BUCKET_NAME</code> - R2 storage</li>
|
| 985 |
+
<li><code>CLOUDFLARE_KV_NAMESPACE_ID</code> - KV cache</li>
|
| 986 |
+
<li><code>CLOUDFLARE_DURABLE_OBJECTS_ID</code> - Durable objects</li>
|
| 987 |
+
</ul>
|
| 988 |
+
</div>
|
| 989 |
+
"""
|
| 990 |
+
)
|
| 991 |
+
|
| 992 |
+
# Footer Status
|
| 993 |
+
gr.HTML(
|
| 994 |
+
"""
|
| 995 |
+
<div style="background: linear-gradient(45deg, #f0f8ff 0%, #e6f3ff 100%); padding: 20px; border-radius: 15px; margin-top: 25px; text-align: center;">
|
| 996 |
+
<h3>📊 Platform Status</h3>
|
| 997 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 15px 0;">
|
| 998 |
+
<div>✅ <strong>Authentication:</strong> Active</div>
|
| 999 |
+
<div>🧠 <strong>AI Models:</strong> 200+ Ready</div>
|
| 1000 |
+
<div>🖼️ <strong>Image Processing:</strong> Available</div>
|
| 1001 |
+
<div>🎵 <strong>Audio AI:</strong> Enabled</div>
|
| 1002 |
+
<div>👤 <strong>Face/Avatar:</strong> Ready</div>
|
| 1003 |
+
<div>🌍 <strong>Arabic-English:</strong> Supported</div>
|
| 1004 |
+
<div>☁️ <strong>Cloudflare:</strong> Configurable</div>
|
| 1005 |
+
<div>🚀 <strong>Platform:</strong> Production Ready</div>
|
| 1006 |
+
</div>
|
| 1007 |
+
<p><em>Complete AI Platform successfully deployed on HuggingFace Spaces!</em></p>
|
| 1008 |
+
</div>
|
| 1009 |
+
"""
|
| 1010 |
+
)
|
| 1011 |
+
|
| 1012 |
+
# Launch the app
|
| 1013 |
+
app.launch()
|
app/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python version check: 3.11-3.13
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if sys.version_info < (3, 11) or sys.version_info > (3, 13):
|
| 6 |
+
print(
|
| 7 |
+
"Warning: Unsupported Python version {ver}, please use 3.11-3.13".format(
|
| 8 |
+
ver=".".join(map(str, sys.version_info))
|
| 9 |
+
)
|
| 10 |
+
)
|
app/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (585 Bytes). View file
|
|
|
app/agent/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agent.base import BaseAgent
|
| 2 |
+
from app.agent.browser import BrowserAgent
|
| 3 |
+
from app.agent.mcp import MCPAgent
|
| 4 |
+
from app.agent.react import ReActAgent
|
| 5 |
+
from app.agent.swe import SWEAgent
|
| 6 |
+
from app.agent.toolcall import ToolCallAgent
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"BaseAgent",
|
| 11 |
+
"BrowserAgent",
|
| 12 |
+
"ReActAgent",
|
| 13 |
+
"SWEAgent",
|
| 14 |
+
"ToolCallAgent",
|
| 15 |
+
"MCPAgent",
|
| 16 |
+
]
|
app/agent/base.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
from contextlib import asynccontextmanager
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, Field, model_validator
|
| 6 |
+
|
| 7 |
+
from app.llm import LLM
|
| 8 |
+
from app.logger import logger
|
| 9 |
+
from app.sandbox.client import SANDBOX_CLIENT
|
| 10 |
+
from app.schema import ROLE_TYPE, AgentState, Memory, Message
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class BaseAgent(BaseModel, ABC):
|
| 14 |
+
"""Abstract base class for managing agent state and execution.
|
| 15 |
+
|
| 16 |
+
Provides foundational functionality for state transitions, memory management,
|
| 17 |
+
and a step-based execution loop. Subclasses must implement the `step` method.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
# Core attributes
|
| 21 |
+
name: str = Field(..., description="Unique name of the agent")
|
| 22 |
+
description: Optional[str] = Field(None, description="Optional agent description")
|
| 23 |
+
|
| 24 |
+
# Prompts
|
| 25 |
+
system_prompt: Optional[str] = Field(
|
| 26 |
+
None, description="System-level instruction prompt"
|
| 27 |
+
)
|
| 28 |
+
next_step_prompt: Optional[str] = Field(
|
| 29 |
+
None, description="Prompt for determining next action"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Dependencies
|
| 33 |
+
llm: LLM = Field(default_factory=LLM, description="Language model instance")
|
| 34 |
+
memory: Memory = Field(default_factory=Memory, description="Agent's memory store")
|
| 35 |
+
state: AgentState = Field(
|
| 36 |
+
default=AgentState.IDLE, description="Current agent state"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Execution control
|
| 40 |
+
max_steps: int = Field(default=10, description="Maximum steps before termination")
|
| 41 |
+
current_step: int = Field(default=0, description="Current step in execution")
|
| 42 |
+
|
| 43 |
+
duplicate_threshold: int = 2
|
| 44 |
+
|
| 45 |
+
class Config:
|
| 46 |
+
arbitrary_types_allowed = True
|
| 47 |
+
extra = "allow" # Allow extra fields for flexibility in subclasses
|
| 48 |
+
|
| 49 |
+
@model_validator(mode="after")
|
| 50 |
+
def initialize_agent(self) -> "BaseAgent":
|
| 51 |
+
"""Initialize agent with default settings if not provided."""
|
| 52 |
+
if self.llm is None or not isinstance(self.llm, LLM):
|
| 53 |
+
self.llm = LLM(config_name=self.name.lower())
|
| 54 |
+
if not isinstance(self.memory, Memory):
|
| 55 |
+
self.memory = Memory()
|
| 56 |
+
return self
|
| 57 |
+
|
| 58 |
+
@asynccontextmanager
|
| 59 |
+
async def state_context(self, new_state: AgentState):
|
| 60 |
+
"""Context manager for safe agent state transitions.
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
new_state: The state to transition to during the context.
|
| 64 |
+
|
| 65 |
+
Yields:
|
| 66 |
+
None: Allows execution within the new state.
|
| 67 |
+
|
| 68 |
+
Raises:
|
| 69 |
+
ValueError: If the new_state is invalid.
|
| 70 |
+
"""
|
| 71 |
+
if not isinstance(new_state, AgentState):
|
| 72 |
+
raise ValueError(f"Invalid state: {new_state}")
|
| 73 |
+
|
| 74 |
+
previous_state = self.state
|
| 75 |
+
self.state = new_state
|
| 76 |
+
try:
|
| 77 |
+
yield
|
| 78 |
+
except Exception as e:
|
| 79 |
+
self.state = AgentState.ERROR # Transition to ERROR on failure
|
| 80 |
+
raise e
|
| 81 |
+
finally:
|
| 82 |
+
self.state = previous_state # Revert to previous state
|
| 83 |
+
|
| 84 |
+
def update_memory(
|
| 85 |
+
self,
|
| 86 |
+
role: ROLE_TYPE, # type: ignore
|
| 87 |
+
content: str,
|
| 88 |
+
base64_image: Optional[str] = None,
|
| 89 |
+
**kwargs,
|
| 90 |
+
) -> None:
|
| 91 |
+
"""Add a message to the agent's memory.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
role: The role of the message sender (user, system, assistant, tool).
|
| 95 |
+
content: The message content.
|
| 96 |
+
base64_image: Optional base64 encoded image.
|
| 97 |
+
**kwargs: Additional arguments (e.g., tool_call_id for tool messages).
|
| 98 |
+
|
| 99 |
+
Raises:
|
| 100 |
+
ValueError: If the role is unsupported.
|
| 101 |
+
"""
|
| 102 |
+
message_map = {
|
| 103 |
+
"user": Message.user_message,
|
| 104 |
+
"system": Message.system_message,
|
| 105 |
+
"assistant": Message.assistant_message,
|
| 106 |
+
"tool": lambda content, **kw: Message.tool_message(content, **kw),
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
if role not in message_map:
|
| 110 |
+
raise ValueError(f"Unsupported message role: {role}")
|
| 111 |
+
|
| 112 |
+
# Create message with appropriate parameters based on role
|
| 113 |
+
kwargs = {"base64_image": base64_image, **(kwargs if role == "tool" else {})}
|
| 114 |
+
self.memory.add_message(message_map[role](content, **kwargs))
|
| 115 |
+
|
| 116 |
+
async def run(self, request: Optional[str] = None) -> str:
|
| 117 |
+
"""Execute the agent's main loop asynchronously.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
request: Optional initial user request to process.
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
A string summarizing the execution results.
|
| 124 |
+
|
| 125 |
+
Raises:
|
| 126 |
+
RuntimeError: If the agent is not in IDLE state at start.
|
| 127 |
+
"""
|
| 128 |
+
if self.state != AgentState.IDLE:
|
| 129 |
+
raise RuntimeError(f"Cannot run agent from state: {self.state}")
|
| 130 |
+
|
| 131 |
+
if request:
|
| 132 |
+
self.update_memory("user", request)
|
| 133 |
+
|
| 134 |
+
results: List[str] = []
|
| 135 |
+
async with self.state_context(AgentState.RUNNING):
|
| 136 |
+
while (
|
| 137 |
+
self.current_step < self.max_steps and self.state != AgentState.FINISHED
|
| 138 |
+
):
|
| 139 |
+
self.current_step += 1
|
| 140 |
+
logger.info(f"Executing step {self.current_step}/{self.max_steps}")
|
| 141 |
+
step_result = await self.step()
|
| 142 |
+
|
| 143 |
+
# Check for stuck state
|
| 144 |
+
if self.is_stuck():
|
| 145 |
+
self.handle_stuck_state()
|
| 146 |
+
|
| 147 |
+
results.append(f"Step {self.current_step}: {step_result}")
|
| 148 |
+
|
| 149 |
+
if self.current_step >= self.max_steps:
|
| 150 |
+
self.current_step = 0
|
| 151 |
+
self.state = AgentState.IDLE
|
| 152 |
+
results.append(f"Terminated: Reached max steps ({self.max_steps})")
|
| 153 |
+
await SANDBOX_CLIENT.cleanup()
|
| 154 |
+
return "\n".join(results) if results else "No steps executed"
|
| 155 |
+
|
| 156 |
+
@abstractmethod
|
| 157 |
+
async def step(self) -> str:
|
| 158 |
+
"""Execute a single step in the agent's workflow.
|
| 159 |
+
|
| 160 |
+
Must be implemented by subclasses to define specific behavior.
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
def handle_stuck_state(self):
|
| 164 |
+
"""Handle stuck state by adding a prompt to change strategy"""
|
| 165 |
+
stuck_prompt = "\
|
| 166 |
+
Observed duplicate responses. Consider new strategies and avoid repeating ineffective paths already attempted."
|
| 167 |
+
self.next_step_prompt = f"{stuck_prompt}\n{self.next_step_prompt}"
|
| 168 |
+
logger.warning(f"Agent detected stuck state. Added prompt: {stuck_prompt}")
|
| 169 |
+
|
| 170 |
+
def is_stuck(self) -> bool:
|
| 171 |
+
"""Check if the agent is stuck in a loop by detecting duplicate content"""
|
| 172 |
+
if len(self.memory.messages) < 2:
|
| 173 |
+
return False
|
| 174 |
+
|
| 175 |
+
last_message = self.memory.messages[-1]
|
| 176 |
+
if not last_message.content:
|
| 177 |
+
return False
|
| 178 |
+
|
| 179 |
+
# Count identical content occurrences
|
| 180 |
+
duplicate_count = sum(
|
| 181 |
+
1
|
| 182 |
+
for msg in reversed(self.memory.messages[:-1])
|
| 183 |
+
if msg.role == "assistant" and msg.content == last_message.content
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
return duplicate_count >= self.duplicate_threshold
|
| 187 |
+
|
| 188 |
+
@property
|
| 189 |
+
def messages(self) -> List[Message]:
|
| 190 |
+
"""Retrieve a list of messages from the agent's memory."""
|
| 191 |
+
return self.memory.messages
|
| 192 |
+
|
| 193 |
+
@messages.setter
|
| 194 |
+
def messages(self, value: List[Message]):
|
| 195 |
+
"""Set the list of messages in the agent's memory."""
|
| 196 |
+
self.memory.messages = value
|
app/agent/manus.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Optional
|
| 2 |
+
|
| 3 |
+
from pydantic import Field, model_validator
|
| 4 |
+
|
| 5 |
+
from app.agent.browser import BrowserContextHelper
|
| 6 |
+
from app.agent.toolcall import ToolCallAgent
|
| 7 |
+
from app.config import config
|
| 8 |
+
from app.logger import logger
|
| 9 |
+
from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT
|
| 10 |
+
from app.tool import Terminate, ToolCollection
|
| 11 |
+
from app.tool.ask_human import AskHuman
|
| 12 |
+
from app.tool.browser_use_tool import BrowserUseTool
|
| 13 |
+
from app.tool.mcp import MCPClients, MCPClientTool
|
| 14 |
+
from app.tool.python_execute import PythonExecute
|
| 15 |
+
from app.tool.str_replace_editor import StrReplaceEditor
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Manus(ToolCallAgent):
|
| 19 |
+
"""A versatile general-purpose agent with support for both local and MCP tools."""
|
| 20 |
+
|
| 21 |
+
name: str = "Manus"
|
| 22 |
+
description: str = "A versatile agent that can solve various tasks using multiple tools including MCP-based tools"
|
| 23 |
+
|
| 24 |
+
system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root)
|
| 25 |
+
next_step_prompt: str = NEXT_STEP_PROMPT
|
| 26 |
+
|
| 27 |
+
max_observe: int = 10000
|
| 28 |
+
max_steps: int = 20
|
| 29 |
+
|
| 30 |
+
# MCP clients for remote tool access
|
| 31 |
+
mcp_clients: MCPClients = Field(default_factory=MCPClients)
|
| 32 |
+
|
| 33 |
+
# Add general-purpose tools to the tool collection
|
| 34 |
+
available_tools: ToolCollection = Field(
|
| 35 |
+
default_factory=lambda: ToolCollection(
|
| 36 |
+
PythonExecute(),
|
| 37 |
+
BrowserUseTool(),
|
| 38 |
+
StrReplaceEditor(),
|
| 39 |
+
AskHuman(),
|
| 40 |
+
Terminate(),
|
| 41 |
+
)
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
|
| 45 |
+
browser_context_helper: Optional[BrowserContextHelper] = None
|
| 46 |
+
|
| 47 |
+
# Track connected MCP servers
|
| 48 |
+
connected_servers: Dict[str, str] = Field(
|
| 49 |
+
default_factory=dict
|
| 50 |
+
) # server_id -> url/command
|
| 51 |
+
_initialized: bool = False
|
| 52 |
+
|
| 53 |
+
@model_validator(mode="after")
|
| 54 |
+
def initialize_helper(self) -> "Manus":
|
| 55 |
+
"""Initialize basic components synchronously."""
|
| 56 |
+
self.browser_context_helper = BrowserContextHelper(self)
|
| 57 |
+
return self
|
| 58 |
+
|
| 59 |
+
@classmethod
|
| 60 |
+
async def create(cls, **kwargs) -> "Manus":
|
| 61 |
+
"""Factory method to create and properly initialize a Manus instance."""
|
| 62 |
+
instance = cls(**kwargs)
|
| 63 |
+
await instance.initialize_mcp_servers()
|
| 64 |
+
instance._initialized = True
|
| 65 |
+
return instance
|
| 66 |
+
|
| 67 |
+
async def initialize_mcp_servers(self) -> None:
|
| 68 |
+
"""Initialize connections to configured MCP servers."""
|
| 69 |
+
for server_id, server_config in config.mcp_config.servers.items():
|
| 70 |
+
try:
|
| 71 |
+
if server_config.type == "sse":
|
| 72 |
+
if server_config.url:
|
| 73 |
+
await self.connect_mcp_server(server_config.url, server_id)
|
| 74 |
+
logger.info(
|
| 75 |
+
f"Connected to MCP server {server_id} at {server_config.url}"
|
| 76 |
+
)
|
| 77 |
+
elif server_config.type == "stdio":
|
| 78 |
+
if server_config.command:
|
| 79 |
+
await self.connect_mcp_server(
|
| 80 |
+
server_config.command,
|
| 81 |
+
server_id,
|
| 82 |
+
use_stdio=True,
|
| 83 |
+
stdio_args=server_config.args,
|
| 84 |
+
)
|
| 85 |
+
logger.info(
|
| 86 |
+
f"Connected to MCP server {server_id} using command {server_config.command}"
|
| 87 |
+
)
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Failed to connect to MCP server {server_id}: {e}")
|
| 90 |
+
|
| 91 |
+
async def connect_mcp_server(
|
| 92 |
+
self,
|
| 93 |
+
server_url: str,
|
| 94 |
+
server_id: str = "",
|
| 95 |
+
use_stdio: bool = False,
|
| 96 |
+
stdio_args: List[str] = None,
|
| 97 |
+
) -> None:
|
| 98 |
+
"""Connect to an MCP server and add its tools."""
|
| 99 |
+
if use_stdio:
|
| 100 |
+
await self.mcp_clients.connect_stdio(
|
| 101 |
+
server_url, stdio_args or [], server_id
|
| 102 |
+
)
|
| 103 |
+
self.connected_servers[server_id or server_url] = server_url
|
| 104 |
+
else:
|
| 105 |
+
await self.mcp_clients.connect_sse(server_url, server_id)
|
| 106 |
+
self.connected_servers[server_id or server_url] = server_url
|
| 107 |
+
|
| 108 |
+
# Update available tools with only the new tools from this server
|
| 109 |
+
new_tools = [
|
| 110 |
+
tool for tool in self.mcp_clients.tools if tool.server_id == server_id
|
| 111 |
+
]
|
| 112 |
+
self.available_tools.add_tools(*new_tools)
|
| 113 |
+
|
| 114 |
+
async def disconnect_mcp_server(self, server_id: str = "") -> None:
|
| 115 |
+
"""Disconnect from an MCP server and remove its tools."""
|
| 116 |
+
await self.mcp_clients.disconnect(server_id)
|
| 117 |
+
if server_id:
|
| 118 |
+
self.connected_servers.pop(server_id, None)
|
| 119 |
+
else:
|
| 120 |
+
self.connected_servers.clear()
|
| 121 |
+
|
| 122 |
+
# Rebuild available tools without the disconnected server's tools
|
| 123 |
+
base_tools = [
|
| 124 |
+
tool
|
| 125 |
+
for tool in self.available_tools.tools
|
| 126 |
+
if not isinstance(tool, MCPClientTool)
|
| 127 |
+
]
|
| 128 |
+
self.available_tools = ToolCollection(*base_tools)
|
| 129 |
+
self.available_tools.add_tools(*self.mcp_clients.tools)
|
| 130 |
+
|
| 131 |
+
async def cleanup(self):
|
| 132 |
+
"""Clean up Manus agent resources."""
|
| 133 |
+
if self.browser_context_helper:
|
| 134 |
+
await self.browser_context_helper.cleanup_browser()
|
| 135 |
+
# Disconnect from all MCP servers only if we were initialized
|
| 136 |
+
if self._initialized:
|
| 137 |
+
await self.disconnect_mcp_server()
|
| 138 |
+
self._initialized = False
|
| 139 |
+
|
| 140 |
+
async def think(self) -> bool:
|
| 141 |
+
"""Process current state and decide next actions with appropriate context."""
|
| 142 |
+
if not self._initialized:
|
| 143 |
+
await self.initialize_mcp_servers()
|
| 144 |
+
self._initialized = True
|
| 145 |
+
|
| 146 |
+
original_prompt = self.next_step_prompt
|
| 147 |
+
recent_messages = self.memory.messages[-3:] if self.memory.messages else []
|
| 148 |
+
browser_in_use = any(
|
| 149 |
+
tc.function.name == BrowserUseTool().name
|
| 150 |
+
for msg in recent_messages
|
| 151 |
+
if msg.tool_calls
|
| 152 |
+
for tc in msg.tool_calls
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
if browser_in_use:
|
| 156 |
+
self.next_step_prompt = (
|
| 157 |
+
await self.browser_context_helper.format_next_step_prompt()
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
result = await super().think()
|
| 161 |
+
|
| 162 |
+
# Restore original prompt
|
| 163 |
+
self.next_step_prompt = original_prompt
|
| 164 |
+
|
| 165 |
+
return result
|
app/agent/toolcall.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
from typing import Any, List, Optional, Union
|
| 4 |
+
|
| 5 |
+
from pydantic import Field
|
| 6 |
+
|
| 7 |
+
from app.agent.react import ReActAgent
|
| 8 |
+
from app.exceptions import TokenLimitExceeded
|
| 9 |
+
from app.logger import logger
|
| 10 |
+
from app.prompt.toolcall import NEXT_STEP_PROMPT, SYSTEM_PROMPT
|
| 11 |
+
from app.schema import TOOL_CHOICE_TYPE, AgentState, Message, ToolCall, ToolChoice
|
| 12 |
+
from app.tool import CreateChatCompletion, Terminate, ToolCollection
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
TOOL_CALL_REQUIRED = "Tool calls required but none provided"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ToolCallAgent(ReActAgent):
|
| 19 |
+
"""Base agent class for handling tool/function calls with enhanced abstraction"""
|
| 20 |
+
|
| 21 |
+
name: str = "toolcall"
|
| 22 |
+
description: str = "an agent that can execute tool calls."
|
| 23 |
+
|
| 24 |
+
system_prompt: str = SYSTEM_PROMPT
|
| 25 |
+
next_step_prompt: str = NEXT_STEP_PROMPT
|
| 26 |
+
|
| 27 |
+
available_tools: ToolCollection = ToolCollection(
|
| 28 |
+
CreateChatCompletion(), Terminate()
|
| 29 |
+
)
|
| 30 |
+
tool_choices: TOOL_CHOICE_TYPE = ToolChoice.AUTO # type: ignore
|
| 31 |
+
special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name])
|
| 32 |
+
|
| 33 |
+
tool_calls: List[ToolCall] = Field(default_factory=list)
|
| 34 |
+
_current_base64_image: Optional[str] = None
|
| 35 |
+
|
| 36 |
+
max_steps: int = 30
|
| 37 |
+
max_observe: Optional[Union[int, bool]] = None
|
| 38 |
+
|
| 39 |
+
async def think(self) -> bool:
|
| 40 |
+
"""Process current state and decide next actions using tools"""
|
| 41 |
+
if self.next_step_prompt:
|
| 42 |
+
user_msg = Message.user_message(self.next_step_prompt)
|
| 43 |
+
self.messages += [user_msg]
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
# Get response with tool options
|
| 47 |
+
response = await self.llm.ask_tool(
|
| 48 |
+
messages=self.messages,
|
| 49 |
+
system_msgs=(
|
| 50 |
+
[Message.system_message(self.system_prompt)]
|
| 51 |
+
if self.system_prompt
|
| 52 |
+
else None
|
| 53 |
+
),
|
| 54 |
+
tools=self.available_tools.to_params(),
|
| 55 |
+
tool_choice=self.tool_choices,
|
| 56 |
+
)
|
| 57 |
+
except ValueError:
|
| 58 |
+
raise
|
| 59 |
+
except Exception as e:
|
| 60 |
+
# Check if this is a RetryError containing TokenLimitExceeded
|
| 61 |
+
if hasattr(e, "__cause__") and isinstance(e.__cause__, TokenLimitExceeded):
|
| 62 |
+
token_limit_error = e.__cause__
|
| 63 |
+
logger.error(
|
| 64 |
+
f"🚨 Token limit error (from RetryError): {token_limit_error}"
|
| 65 |
+
)
|
| 66 |
+
self.memory.add_message(
|
| 67 |
+
Message.assistant_message(
|
| 68 |
+
f"Maximum token limit reached, cannot continue execution: {str(token_limit_error)}"
|
| 69 |
+
)
|
| 70 |
+
)
|
| 71 |
+
self.state = AgentState.FINISHED
|
| 72 |
+
return False
|
| 73 |
+
raise
|
| 74 |
+
|
| 75 |
+
self.tool_calls = tool_calls = (
|
| 76 |
+
response.tool_calls if response and response.tool_calls else []
|
| 77 |
+
)
|
| 78 |
+
content = response.content if response and response.content else ""
|
| 79 |
+
|
| 80 |
+
# Log response info
|
| 81 |
+
logger.info(f"✨ {self.name}'s thoughts: {content}")
|
| 82 |
+
logger.info(
|
| 83 |
+
f"🛠️ {self.name} selected {len(tool_calls) if tool_calls else 0} tools to use"
|
| 84 |
+
)
|
| 85 |
+
if tool_calls:
|
| 86 |
+
logger.info(
|
| 87 |
+
f"🧰 Tools being prepared: {[call.function.name for call in tool_calls]}"
|
| 88 |
+
)
|
| 89 |
+
logger.info(f"🔧 Tool arguments: {tool_calls[0].function.arguments}")
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
if response is None:
|
| 93 |
+
raise RuntimeError("No response received from the LLM")
|
| 94 |
+
|
| 95 |
+
# Handle different tool_choices modes
|
| 96 |
+
if self.tool_choices == ToolChoice.NONE:
|
| 97 |
+
if tool_calls:
|
| 98 |
+
logger.warning(
|
| 99 |
+
f"🤔 Hmm, {self.name} tried to use tools when they weren't available!"
|
| 100 |
+
)
|
| 101 |
+
if content:
|
| 102 |
+
self.memory.add_message(Message.assistant_message(content))
|
| 103 |
+
return True
|
| 104 |
+
return False
|
| 105 |
+
|
| 106 |
+
# Create and add assistant message
|
| 107 |
+
assistant_msg = (
|
| 108 |
+
Message.from_tool_calls(content=content, tool_calls=self.tool_calls)
|
| 109 |
+
if self.tool_calls
|
| 110 |
+
else Message.assistant_message(content)
|
| 111 |
+
)
|
| 112 |
+
self.memory.add_message(assistant_msg)
|
| 113 |
+
|
| 114 |
+
if self.tool_choices == ToolChoice.REQUIRED and not self.tool_calls:
|
| 115 |
+
return True # Will be handled in act()
|
| 116 |
+
|
| 117 |
+
# For 'auto' mode, continue with content if no commands but content exists
|
| 118 |
+
if self.tool_choices == ToolChoice.AUTO and not self.tool_calls:
|
| 119 |
+
return bool(content)
|
| 120 |
+
|
| 121 |
+
return bool(self.tool_calls)
|
| 122 |
+
except Exception as e:
|
| 123 |
+
logger.error(f"🚨 Oops! The {self.name}'s thinking process hit a snag: {e}")
|
| 124 |
+
self.memory.add_message(
|
| 125 |
+
Message.assistant_message(
|
| 126 |
+
f"Error encountered while processing: {str(e)}"
|
| 127 |
+
)
|
| 128 |
+
)
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
async def act(self) -> str:
|
| 132 |
+
"""Execute tool calls and handle their results"""
|
| 133 |
+
if not self.tool_calls:
|
| 134 |
+
if self.tool_choices == ToolChoice.REQUIRED:
|
| 135 |
+
raise ValueError(TOOL_CALL_REQUIRED)
|
| 136 |
+
|
| 137 |
+
# Return last message content if no tool calls
|
| 138 |
+
return self.messages[-1].content or "No content or commands to execute"
|
| 139 |
+
|
| 140 |
+
results = []
|
| 141 |
+
for command in self.tool_calls:
|
| 142 |
+
# Reset base64_image for each tool call
|
| 143 |
+
self._current_base64_image = None
|
| 144 |
+
|
| 145 |
+
result = await self.execute_tool(command)
|
| 146 |
+
|
| 147 |
+
if self.max_observe:
|
| 148 |
+
result = result[: self.max_observe]
|
| 149 |
+
|
| 150 |
+
logger.info(
|
| 151 |
+
f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}"
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# Add tool response to memory
|
| 155 |
+
tool_msg = Message.tool_message(
|
| 156 |
+
content=result,
|
| 157 |
+
tool_call_id=command.id,
|
| 158 |
+
name=command.function.name,
|
| 159 |
+
base64_image=self._current_base64_image,
|
| 160 |
+
)
|
| 161 |
+
self.memory.add_message(tool_msg)
|
| 162 |
+
results.append(result)
|
| 163 |
+
|
| 164 |
+
return "\n\n".join(results)
|
| 165 |
+
|
| 166 |
+
async def execute_tool(self, command: ToolCall) -> str:
|
| 167 |
+
"""Execute a single tool call with robust error handling"""
|
| 168 |
+
if not command or not command.function or not command.function.name:
|
| 169 |
+
return "Error: Invalid command format"
|
| 170 |
+
|
| 171 |
+
name = command.function.name
|
| 172 |
+
if name not in self.available_tools.tool_map:
|
| 173 |
+
return f"Error: Unknown tool '{name}'"
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
# Parse arguments
|
| 177 |
+
args = json.loads(command.function.arguments or "{}")
|
| 178 |
+
|
| 179 |
+
# Execute the tool
|
| 180 |
+
logger.info(f"🔧 Activating tool: '{name}'...")
|
| 181 |
+
result = await self.available_tools.execute(name=name, tool_input=args)
|
| 182 |
+
|
| 183 |
+
# Handle special tools
|
| 184 |
+
await self._handle_special_tool(name=name, result=result)
|
| 185 |
+
|
| 186 |
+
# Check if result is a ToolResult with base64_image
|
| 187 |
+
if hasattr(result, "base64_image") and result.base64_image:
|
| 188 |
+
# Store the base64_image for later use in tool_message
|
| 189 |
+
self._current_base64_image = result.base64_image
|
| 190 |
+
|
| 191 |
+
# Format result for display (standard case)
|
| 192 |
+
observation = (
|
| 193 |
+
f"Observed output of cmd `{name}` executed:\n{str(result)}"
|
| 194 |
+
if result
|
| 195 |
+
else f"Cmd `{name}` completed with no output"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
return observation
|
| 199 |
+
except json.JSONDecodeError:
|
| 200 |
+
error_msg = f"Error parsing arguments for {name}: Invalid JSON format"
|
| 201 |
+
logger.error(
|
| 202 |
+
f"📝 Oops! The arguments for '{name}' don't make sense - invalid JSON, arguments:{command.function.arguments}"
|
| 203 |
+
)
|
| 204 |
+
return f"Error: {error_msg}"
|
| 205 |
+
except Exception as e:
|
| 206 |
+
error_msg = f"⚠️ Tool '{name}' encountered a problem: {str(e)}"
|
| 207 |
+
logger.exception(error_msg)
|
| 208 |
+
return f"Error: {error_msg}"
|
| 209 |
+
|
| 210 |
+
async def _handle_special_tool(self, name: str, result: Any, **kwargs):
|
| 211 |
+
"""Handle special tool execution and state changes"""
|
| 212 |
+
if not self._is_special_tool(name):
|
| 213 |
+
return
|
| 214 |
+
|
| 215 |
+
if self._should_finish_execution(name=name, result=result, **kwargs):
|
| 216 |
+
# Set agent state to finished
|
| 217 |
+
logger.info(f"🏁 Special tool '{name}' has completed the task!")
|
| 218 |
+
self.state = AgentState.FINISHED
|
| 219 |
+
|
| 220 |
+
@staticmethod
|
| 221 |
+
def _should_finish_execution(**kwargs) -> bool:
|
| 222 |
+
"""Determine if tool execution should finish the agent"""
|
| 223 |
+
return True
|
| 224 |
+
|
| 225 |
+
def _is_special_tool(self, name: str) -> bool:
|
| 226 |
+
"""Check if tool name is in special tools list"""
|
| 227 |
+
return name.lower() in [n.lower() for n in self.special_tool_names]
|
| 228 |
+
|
| 229 |
+
async def cleanup(self):
|
| 230 |
+
"""Clean up resources used by the agent's tools."""
|
| 231 |
+
logger.info(f"🧹 Cleaning up resources for agent '{self.name}'...")
|
| 232 |
+
for tool_name, tool_instance in self.available_tools.tool_map.items():
|
| 233 |
+
if hasattr(tool_instance, "cleanup") and asyncio.iscoroutinefunction(
|
| 234 |
+
tool_instance.cleanup
|
| 235 |
+
):
|
| 236 |
+
try:
|
| 237 |
+
logger.debug(f"🧼 Cleaning up tool: {tool_name}")
|
| 238 |
+
await tool_instance.cleanup()
|
| 239 |
+
except Exception as e:
|
| 240 |
+
logger.error(
|
| 241 |
+
f"🚨 Error cleaning up tool '{tool_name}': {e}", exc_info=True
|
| 242 |
+
)
|
| 243 |
+
logger.info(f"✨ Cleanup complete for agent '{self.name}'.")
|
| 244 |
+
|
| 245 |
+
async def run(self, request: Optional[str] = None) -> str:
|
| 246 |
+
"""Run the agent with cleanup when done."""
|
| 247 |
+
try:
|
| 248 |
+
return await super().run(request)
|
| 249 |
+
finally:
|
| 250 |
+
await self.cleanup()
|
app/auth.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
User authentication models and validation for OpenManus
|
| 3 |
+
Mobile number + password based authentication system
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import hashlib
|
| 7 |
+
import re
|
| 8 |
+
import secrets
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
from typing import Optional
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from pydantic import BaseModel, validator
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class UserSignupRequest(BaseModel):
|
| 16 |
+
"""User signup request model"""
|
| 17 |
+
|
| 18 |
+
full_name: str
|
| 19 |
+
mobile_number: str
|
| 20 |
+
password: str
|
| 21 |
+
confirm_password: str
|
| 22 |
+
|
| 23 |
+
@validator("full_name")
|
| 24 |
+
def validate_full_name(cls, v):
|
| 25 |
+
if not v or len(v.strip()) < 2:
|
| 26 |
+
raise ValueError("Full name must be at least 2 characters long")
|
| 27 |
+
if len(v.strip()) > 100:
|
| 28 |
+
raise ValueError("Full name must be less than 100 characters")
|
| 29 |
+
return v.strip()
|
| 30 |
+
|
| 31 |
+
@validator("mobile_number")
|
| 32 |
+
def validate_mobile_number(cls, v):
|
| 33 |
+
# Remove all non-digit characters
|
| 34 |
+
digits_only = re.sub(r"\D", "", v)
|
| 35 |
+
|
| 36 |
+
# Check if it's a valid mobile number (10-15 digits)
|
| 37 |
+
if len(digits_only) < 10 or len(digits_only) > 15:
|
| 38 |
+
raise ValueError("Mobile number must be between 10-15 digits")
|
| 39 |
+
|
| 40 |
+
# Ensure it starts with country code or local format
|
| 41 |
+
if not re.match(r"^(\+?[1-9]\d{9,14})$", digits_only):
|
| 42 |
+
raise ValueError("Invalid mobile number format")
|
| 43 |
+
|
| 44 |
+
return digits_only
|
| 45 |
+
|
| 46 |
+
@validator("password")
|
| 47 |
+
def validate_password(cls, v):
|
| 48 |
+
if len(v) < 8:
|
| 49 |
+
raise ValueError("Password must be at least 8 characters long")
|
| 50 |
+
if len(v) > 128:
|
| 51 |
+
raise ValueError("Password must be less than 128 characters")
|
| 52 |
+
|
| 53 |
+
# Check for at least one uppercase, lowercase, and digit
|
| 54 |
+
if not re.search(r"[A-Z]", v):
|
| 55 |
+
raise ValueError("Password must contain at least one uppercase letter")
|
| 56 |
+
if not re.search(r"[a-z]", v):
|
| 57 |
+
raise ValueError("Password must contain at least one lowercase letter")
|
| 58 |
+
if not re.search(r"\d", v):
|
| 59 |
+
raise ValueError("Password must contain at least one digit")
|
| 60 |
+
|
| 61 |
+
return v
|
| 62 |
+
|
| 63 |
+
@validator("confirm_password")
|
| 64 |
+
def validate_confirm_password(cls, v, values):
|
| 65 |
+
if "password" in values and v != values["password"]:
|
| 66 |
+
raise ValueError("Passwords do not match")
|
| 67 |
+
return v
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class UserLoginRequest(BaseModel):
|
| 71 |
+
"""User login request model"""
|
| 72 |
+
|
| 73 |
+
mobile_number: str
|
| 74 |
+
password: str
|
| 75 |
+
|
| 76 |
+
@validator("mobile_number")
|
| 77 |
+
def validate_mobile_number(cls, v):
|
| 78 |
+
# Remove all non-digit characters
|
| 79 |
+
digits_only = re.sub(r"\D", "", v)
|
| 80 |
+
|
| 81 |
+
if len(digits_only) < 10 or len(digits_only) > 15:
|
| 82 |
+
raise ValueError("Invalid mobile number")
|
| 83 |
+
|
| 84 |
+
return digits_only
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@dataclass
|
| 88 |
+
class User:
|
| 89 |
+
"""User model"""
|
| 90 |
+
|
| 91 |
+
id: str
|
| 92 |
+
mobile_number: str
|
| 93 |
+
full_name: str
|
| 94 |
+
password_hash: str
|
| 95 |
+
avatar_url: Optional[str] = None
|
| 96 |
+
preferences: Optional[str] = None
|
| 97 |
+
is_active: bool = True
|
| 98 |
+
created_at: Optional[datetime] = None
|
| 99 |
+
updated_at: Optional[datetime] = None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@dataclass
|
| 103 |
+
class UserSession:
|
| 104 |
+
"""User session model"""
|
| 105 |
+
|
| 106 |
+
session_id: str
|
| 107 |
+
user_id: str
|
| 108 |
+
mobile_number: str
|
| 109 |
+
full_name: str
|
| 110 |
+
created_at: datetime
|
| 111 |
+
expires_at: datetime
|
| 112 |
+
|
| 113 |
+
@property
|
| 114 |
+
def is_valid(self) -> bool:
|
| 115 |
+
"""Check if session is still valid"""
|
| 116 |
+
return datetime.utcnow() < self.expires_at
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class UserAuth:
|
| 120 |
+
"""User authentication utilities"""
|
| 121 |
+
|
| 122 |
+
@staticmethod
|
| 123 |
+
def hash_password(password: str) -> str:
|
| 124 |
+
"""Hash password using SHA-256 with salt"""
|
| 125 |
+
salt = secrets.token_hex(32)
|
| 126 |
+
password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
|
| 127 |
+
return f"{salt}:{password_hash}"
|
| 128 |
+
|
| 129 |
+
@staticmethod
|
| 130 |
+
def verify_password(password: str, password_hash: str) -> bool:
|
| 131 |
+
"""Verify password against stored hash"""
|
| 132 |
+
try:
|
| 133 |
+
salt, stored_hash = password_hash.split(":")
|
| 134 |
+
password_hash_check = hashlib.sha256((password + salt).encode()).hexdigest()
|
| 135 |
+
return password_hash_check == stored_hash
|
| 136 |
+
except ValueError:
|
| 137 |
+
return False
|
| 138 |
+
|
| 139 |
+
@staticmethod
|
| 140 |
+
def generate_session_id() -> str:
|
| 141 |
+
"""Generate secure session ID"""
|
| 142 |
+
return secrets.token_urlsafe(32)
|
| 143 |
+
|
| 144 |
+
@staticmethod
|
| 145 |
+
def generate_user_id() -> str:
|
| 146 |
+
"""Generate unique user ID"""
|
| 147 |
+
return f"user_{secrets.token_hex(16)}"
|
| 148 |
+
|
| 149 |
+
@staticmethod
|
| 150 |
+
def format_mobile_number(mobile_number: str) -> str:
|
| 151 |
+
"""Format mobile number for consistent storage"""
|
| 152 |
+
# Remove all non-digit characters
|
| 153 |
+
digits_only = re.sub(r"\D", "", mobile_number)
|
| 154 |
+
|
| 155 |
+
# Add + prefix if not present and format consistently
|
| 156 |
+
if not digits_only.startswith("+"):
|
| 157 |
+
# Assume it's a local number, add default country code if needed
|
| 158 |
+
if len(digits_only) == 10: # US format
|
| 159 |
+
digits_only = f"1{digits_only}"
|
| 160 |
+
|
| 161 |
+
return f"+{digits_only}"
|
| 162 |
+
|
| 163 |
+
@staticmethod
|
| 164 |
+
def create_session(user: User, duration_hours: int = 24) -> UserSession:
|
| 165 |
+
"""Create a new user session"""
|
| 166 |
+
session_id = UserAuth.generate_session_id()
|
| 167 |
+
created_at = datetime.utcnow()
|
| 168 |
+
expires_at = created_at + timedelta(hours=duration_hours)
|
| 169 |
+
|
| 170 |
+
return UserSession(
|
| 171 |
+
session_id=session_id,
|
| 172 |
+
user_id=user.id,
|
| 173 |
+
mobile_number=user.mobile_number,
|
| 174 |
+
full_name=user.full_name,
|
| 175 |
+
created_at=created_at,
|
| 176 |
+
expires_at=expires_at,
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# Response models
|
| 181 |
+
class AuthResponse(BaseModel):
|
| 182 |
+
"""Authentication response model"""
|
| 183 |
+
|
| 184 |
+
success: bool
|
| 185 |
+
message: str
|
| 186 |
+
session_id: Optional[str] = None
|
| 187 |
+
user_id: Optional[str] = None
|
| 188 |
+
full_name: Optional[str] = None
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class UserProfile(BaseModel):
|
| 192 |
+
"""User profile response model"""
|
| 193 |
+
|
| 194 |
+
user_id: str
|
| 195 |
+
full_name: str
|
| 196 |
+
mobile_number: str # Masked for security
|
| 197 |
+
avatar_url: Optional[str] = None
|
| 198 |
+
created_at: Optional[str] = None
|
| 199 |
+
|
| 200 |
+
@staticmethod
|
| 201 |
+
def mask_mobile_number(mobile_number: str) -> str:
|
| 202 |
+
"""Mask mobile number for security (show only last 4 digits)"""
|
| 203 |
+
if len(mobile_number) <= 4:
|
| 204 |
+
return "*" * len(mobile_number)
|
| 205 |
+
return "*" * (len(mobile_number) - 4) + mobile_number[-4:]
|
app/auth_interface.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication Web Interface for OpenManus
|
| 3 |
+
Mobile number + password based authentication forms
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import sqlite3
|
| 8 |
+
from typing import Optional, Tuple
|
| 9 |
+
|
| 10 |
+
import gradio as gr
|
| 11 |
+
|
| 12 |
+
from app.auth import UserSignupRequest, UserLoginRequest
|
| 13 |
+
from app.auth_service import AuthService
|
| 14 |
+
from app.logger import logger
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class AuthInterface:
|
| 18 |
+
"""Authentication interface with Gradio"""
|
| 19 |
+
|
| 20 |
+
def __init__(self, db_path: str = "openmanus.db"):
|
| 21 |
+
self.db_path = db_path
|
| 22 |
+
self.auth_service = None
|
| 23 |
+
self.current_session = None
|
| 24 |
+
self.init_database()
|
| 25 |
+
|
| 26 |
+
def init_database(self):
|
| 27 |
+
"""Initialize database with schema"""
|
| 28 |
+
try:
|
| 29 |
+
conn = sqlite3.connect(self.db_path)
|
| 30 |
+
|
| 31 |
+
# Create users table with mobile auth
|
| 32 |
+
conn.execute(
|
| 33 |
+
"""
|
| 34 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 35 |
+
id TEXT PRIMARY KEY,
|
| 36 |
+
mobile_number TEXT UNIQUE NOT NULL,
|
| 37 |
+
full_name TEXT NOT NULL,
|
| 38 |
+
password_hash TEXT NOT NULL,
|
| 39 |
+
avatar_url TEXT,
|
| 40 |
+
preferences TEXT,
|
| 41 |
+
is_active BOOLEAN DEFAULT TRUE,
|
| 42 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 43 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 44 |
+
)
|
| 45 |
+
"""
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Create sessions table
|
| 49 |
+
conn.execute(
|
| 50 |
+
"""
|
| 51 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 52 |
+
id TEXT PRIMARY KEY,
|
| 53 |
+
user_id TEXT NOT NULL,
|
| 54 |
+
title TEXT,
|
| 55 |
+
metadata TEXT,
|
| 56 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 57 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 58 |
+
expires_at DATETIME,
|
| 59 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 60 |
+
)
|
| 61 |
+
"""
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
conn.commit()
|
| 65 |
+
conn.close()
|
| 66 |
+
logger.info("Database initialized successfully")
|
| 67 |
+
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.error(f"Database initialization error: {str(e)}")
|
| 70 |
+
|
| 71 |
+
def get_db_connection(self):
|
| 72 |
+
"""Get database connection"""
|
| 73 |
+
return sqlite3.connect(self.db_path)
|
| 74 |
+
|
| 75 |
+
async def handle_signup(
|
| 76 |
+
self, full_name: str, mobile_number: str, password: str, confirm_password: str
|
| 77 |
+
) -> Tuple[str, bool, dict]:
|
| 78 |
+
"""Handle user signup"""
|
| 79 |
+
try:
|
| 80 |
+
# Validate input
|
| 81 |
+
if not all([full_name, mobile_number, password, confirm_password]):
|
| 82 |
+
return "All fields are required", False, gr.update(visible=True)
|
| 83 |
+
|
| 84 |
+
# Create signup request
|
| 85 |
+
signup_data = UserSignupRequest(
|
| 86 |
+
full_name=full_name,
|
| 87 |
+
mobile_number=mobile_number,
|
| 88 |
+
password=password,
|
| 89 |
+
confirm_password=confirm_password,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Process signup
|
| 93 |
+
db_conn = self.get_db_connection()
|
| 94 |
+
auth_service = AuthService(db_conn)
|
| 95 |
+
|
| 96 |
+
result = await auth_service.register_user(signup_data)
|
| 97 |
+
db_conn.close()
|
| 98 |
+
|
| 99 |
+
if result.success:
|
| 100 |
+
self.current_session = {
|
| 101 |
+
"session_id": result.session_id,
|
| 102 |
+
"user_id": result.user_id,
|
| 103 |
+
"full_name": result.full_name,
|
| 104 |
+
}
|
| 105 |
+
return (
|
| 106 |
+
f"Welcome {result.full_name}! Account created successfully.",
|
| 107 |
+
True,
|
| 108 |
+
gr.update(visible=False),
|
| 109 |
+
)
|
| 110 |
+
else:
|
| 111 |
+
return result.message, False, gr.update(visible=True)
|
| 112 |
+
|
| 113 |
+
except ValueError as e:
|
| 114 |
+
return str(e), False, gr.update(visible=True)
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"Signup error: {str(e)}")
|
| 117 |
+
return "An error occurred during signup", False, gr.update(visible=True)
|
| 118 |
+
|
| 119 |
+
async def handle_login(
|
| 120 |
+
self, mobile_number: str, password: str
|
| 121 |
+
) -> Tuple[str, bool, dict]:
|
| 122 |
+
"""Handle user login"""
|
| 123 |
+
try:
|
| 124 |
+
# Validate input
|
| 125 |
+
if not all([mobile_number, password]):
|
| 126 |
+
return (
|
| 127 |
+
"Mobile number and password are required",
|
| 128 |
+
False,
|
| 129 |
+
gr.update(visible=True),
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Create login request
|
| 133 |
+
login_data = UserLoginRequest(
|
| 134 |
+
mobile_number=mobile_number, password=password
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Process login
|
| 138 |
+
db_conn = self.get_db_connection()
|
| 139 |
+
auth_service = AuthService(db_conn)
|
| 140 |
+
|
| 141 |
+
result = await auth_service.login_user(login_data)
|
| 142 |
+
db_conn.close()
|
| 143 |
+
|
| 144 |
+
if result.success:
|
| 145 |
+
self.current_session = {
|
| 146 |
+
"session_id": result.session_id,
|
| 147 |
+
"user_id": result.user_id,
|
| 148 |
+
"full_name": result.full_name,
|
| 149 |
+
}
|
| 150 |
+
return (
|
| 151 |
+
f"Welcome back, {result.full_name}!",
|
| 152 |
+
True,
|
| 153 |
+
gr.update(visible=False),
|
| 154 |
+
)
|
| 155 |
+
else:
|
| 156 |
+
return result.message, False, gr.update(visible=True)
|
| 157 |
+
|
| 158 |
+
except ValueError as e:
|
| 159 |
+
return str(e), False, gr.update(visible=True)
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logger.error(f"Login error: {str(e)}")
|
| 162 |
+
return "An error occurred during login", False, gr.update(visible=True)
|
| 163 |
+
|
| 164 |
+
def handle_logout(self) -> Tuple[str, bool, dict]:
|
| 165 |
+
"""Handle user logout"""
|
| 166 |
+
if self.current_session:
|
| 167 |
+
# In a real app, you'd delete the session from database
|
| 168 |
+
self.current_session = None
|
| 169 |
+
|
| 170 |
+
return "Logged out successfully", False, gr.update(visible=True)
|
| 171 |
+
|
| 172 |
+
def create_interface(self) -> gr.Interface:
|
| 173 |
+
"""Create the authentication interface"""
|
| 174 |
+
|
| 175 |
+
with gr.Blocks(
|
| 176 |
+
title="OpenManus Authentication", theme=gr.themes.Soft()
|
| 177 |
+
) as auth_interface:
|
| 178 |
+
gr.Markdown(
|
| 179 |
+
"""
|
| 180 |
+
# 🔐 OpenManus Authentication
|
| 181 |
+
### Secure Mobile Number + Password Login System
|
| 182 |
+
"""
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Session status
|
| 186 |
+
session_status = gr.Textbox(
|
| 187 |
+
value="Not logged in", label="Status", interactive=False
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
# Auth forms container
|
| 191 |
+
with gr.Column(visible=True) as auth_forms:
|
| 192 |
+
|
| 193 |
+
with gr.Tabs():
|
| 194 |
+
|
| 195 |
+
# Login Tab
|
| 196 |
+
with gr.TabItem("🔑 Login"):
|
| 197 |
+
gr.Markdown("### Login with your mobile number and password")
|
| 198 |
+
|
| 199 |
+
login_mobile = gr.Textbox(
|
| 200 |
+
label="📱 Mobile Number",
|
| 201 |
+
placeholder="Enter your mobile number (e.g., +1234567890)",
|
| 202 |
+
lines=1,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
login_password = gr.Textbox(
|
| 206 |
+
label="🔒 Password",
|
| 207 |
+
type="password",
|
| 208 |
+
placeholder="Enter your password",
|
| 209 |
+
lines=1,
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
login_btn = gr.Button("🔑 Login", variant="primary", size="lg")
|
| 213 |
+
login_result = gr.Textbox(label="Result", interactive=False)
|
| 214 |
+
|
| 215 |
+
# Signup Tab
|
| 216 |
+
with gr.TabItem("📝 Sign Up"):
|
| 217 |
+
gr.Markdown("### Create your new account")
|
| 218 |
+
|
| 219 |
+
signup_fullname = gr.Textbox(
|
| 220 |
+
label="👤 Full Name",
|
| 221 |
+
placeholder="Enter your full name",
|
| 222 |
+
lines=1,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
signup_mobile = gr.Textbox(
|
| 226 |
+
label="📱 Mobile Number",
|
| 227 |
+
placeholder="Enter your mobile number (e.g., +1234567890)",
|
| 228 |
+
lines=1,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
signup_password = gr.Textbox(
|
| 232 |
+
label="🔒 Password",
|
| 233 |
+
type="password",
|
| 234 |
+
placeholder="Create a strong password (min 8 chars, include uppercase, lowercase, digit)",
|
| 235 |
+
lines=1,
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
signup_confirm_password = gr.Textbox(
|
| 239 |
+
label="🔒 Confirm Password",
|
| 240 |
+
type="password",
|
| 241 |
+
placeholder="Confirm your password",
|
| 242 |
+
lines=1,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
signup_btn = gr.Button(
|
| 246 |
+
"📝 Create Account", variant="primary", size="lg"
|
| 247 |
+
)
|
| 248 |
+
signup_result = gr.Textbox(label="Result", interactive=False)
|
| 249 |
+
|
| 250 |
+
# Logged in section
|
| 251 |
+
with gr.Column(visible=False) as logged_in_section:
|
| 252 |
+
gr.Markdown("### ✅ You are logged in!")
|
| 253 |
+
|
| 254 |
+
user_info = gr.Markdown("Welcome!")
|
| 255 |
+
|
| 256 |
+
logout_btn = gr.Button("🚪 Logout", variant="secondary")
|
| 257 |
+
logout_result = gr.Textbox(label="Result", interactive=False)
|
| 258 |
+
|
| 259 |
+
# Password requirements info
|
| 260 |
+
with gr.Accordion("📋 Password Requirements", open=False):
|
| 261 |
+
gr.Markdown(
|
| 262 |
+
"""
|
| 263 |
+
**Password must contain:**
|
| 264 |
+
- At least 8 characters
|
| 265 |
+
- At least 1 uppercase letter (A-Z)
|
| 266 |
+
- At least 1 lowercase letter (a-z)
|
| 267 |
+
- At least 1 digit (0-9)
|
| 268 |
+
- Maximum 128 characters
|
| 269 |
+
|
| 270 |
+
**Mobile Number Format:**
|
| 271 |
+
- 10-15 digits
|
| 272 |
+
- Can include country code
|
| 273 |
+
- Examples: +1234567890, 1234567890, +91987654321
|
| 274 |
+
"""
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
# Event handlers
|
| 278 |
+
def sync_signup(*args):
|
| 279 |
+
"""Synchronous wrapper for signup"""
|
| 280 |
+
return asyncio.run(self.handle_signup(*args))
|
| 281 |
+
|
| 282 |
+
def sync_login(*args):
|
| 283 |
+
"""Synchronous wrapper for login"""
|
| 284 |
+
return asyncio.run(self.handle_login(*args))
|
| 285 |
+
|
| 286 |
+
def update_ui_after_auth(result_text, success, auth_forms_update):
|
| 287 |
+
"""Update UI after authentication"""
|
| 288 |
+
if success:
|
| 289 |
+
return (
|
| 290 |
+
result_text, # session_status
|
| 291 |
+
auth_forms_update, # auth_forms visibility
|
| 292 |
+
gr.update(visible=True), # logged_in_section visibility
|
| 293 |
+
f"### 👋 {self.current_session['full_name'] if self.current_session else 'User'}", # user_info
|
| 294 |
+
)
|
| 295 |
+
else:
|
| 296 |
+
return (
|
| 297 |
+
"Not logged in", # session_status
|
| 298 |
+
auth_forms_update, # auth_forms visibility
|
| 299 |
+
gr.update(visible=False), # logged_in_section visibility
|
| 300 |
+
"Welcome!", # user_info
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
def update_ui_after_logout(result_text, success, auth_forms_update):
|
| 304 |
+
"""Update UI after logout"""
|
| 305 |
+
return (
|
| 306 |
+
"Not logged in", # session_status
|
| 307 |
+
auth_forms_update, # auth_forms visibility
|
| 308 |
+
gr.update(visible=False), # logged_in_section visibility
|
| 309 |
+
"Welcome!", # user_info
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
# Login button click
|
| 313 |
+
login_btn.click(
|
| 314 |
+
fn=sync_login,
|
| 315 |
+
inputs=[login_mobile, login_password],
|
| 316 |
+
outputs=[login_result, gr.State(), gr.State()],
|
| 317 |
+
).then(
|
| 318 |
+
fn=update_ui_after_auth,
|
| 319 |
+
inputs=[login_result, gr.State(), gr.State()],
|
| 320 |
+
outputs=[session_status, auth_forms, logged_in_section, user_info],
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
# Signup button click
|
| 324 |
+
signup_btn.click(
|
| 325 |
+
fn=sync_signup,
|
| 326 |
+
inputs=[
|
| 327 |
+
signup_fullname,
|
| 328 |
+
signup_mobile,
|
| 329 |
+
signup_password,
|
| 330 |
+
signup_confirm_password,
|
| 331 |
+
],
|
| 332 |
+
outputs=[signup_result, gr.State(), gr.State()],
|
| 333 |
+
).then(
|
| 334 |
+
fn=update_ui_after_auth,
|
| 335 |
+
inputs=[signup_result, gr.State(), gr.State()],
|
| 336 |
+
outputs=[session_status, auth_forms, logged_in_section, user_info],
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
# Logout button click
|
| 340 |
+
logout_btn.click(
|
| 341 |
+
fn=self.handle_logout, outputs=[logout_result, gr.State(), gr.State()]
|
| 342 |
+
).then(
|
| 343 |
+
fn=update_ui_after_logout,
|
| 344 |
+
inputs=[logout_result, gr.State(), gr.State()],
|
| 345 |
+
outputs=[session_status, auth_forms, logged_in_section, user_info],
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
return auth_interface
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
# Standalone authentication app
|
| 352 |
+
def create_auth_app(db_path: str = "openmanus.db") -> gr.Interface:
|
| 353 |
+
"""Create standalone authentication app"""
|
| 354 |
+
auth_interface = AuthInterface(db_path)
|
| 355 |
+
return auth_interface.create_interface()
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
if __name__ == "__main__":
|
| 359 |
+
# Run standalone auth interface for testing
|
| 360 |
+
auth_app = create_auth_app()
|
| 361 |
+
auth_app.launch(server_name="0.0.0.0", server_port=7860, share=False, debug=True)
|
app/auth_service.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
User authentication service for OpenManus
|
| 3 |
+
Handles user registration, login, and session management with D1 database
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import sqlite3
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import Optional, Tuple
|
| 10 |
+
|
| 11 |
+
from app.auth import (
|
| 12 |
+
User,
|
| 13 |
+
UserAuth,
|
| 14 |
+
UserSession,
|
| 15 |
+
UserSignupRequest,
|
| 16 |
+
UserLoginRequest,
|
| 17 |
+
AuthResponse,
|
| 18 |
+
UserProfile,
|
| 19 |
+
)
|
| 20 |
+
from app.logger import logger
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class AuthService:
|
| 24 |
+
"""Authentication service for user management"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, db_connection=None):
|
| 27 |
+
"""Initialize auth service with database connection"""
|
| 28 |
+
self.db = db_connection
|
| 29 |
+
self.logger = logger
|
| 30 |
+
|
| 31 |
+
async def register_user(self, signup_data: UserSignupRequest) -> AuthResponse:
|
| 32 |
+
"""Register a new user"""
|
| 33 |
+
try:
|
| 34 |
+
# Format mobile number consistently
|
| 35 |
+
formatted_mobile = UserAuth.format_mobile_number(signup_data.mobile_number)
|
| 36 |
+
|
| 37 |
+
# Check if user already exists
|
| 38 |
+
existing_user = await self.get_user_by_mobile(formatted_mobile)
|
| 39 |
+
if existing_user:
|
| 40 |
+
return AuthResponse(
|
| 41 |
+
success=False, message="User with this mobile number already exists"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Create new user
|
| 45 |
+
user_id = UserAuth.generate_user_id()
|
| 46 |
+
password_hash = UserAuth.hash_password(signup_data.password)
|
| 47 |
+
|
| 48 |
+
user = User(
|
| 49 |
+
id=user_id,
|
| 50 |
+
mobile_number=formatted_mobile,
|
| 51 |
+
full_name=signup_data.full_name,
|
| 52 |
+
password_hash=password_hash,
|
| 53 |
+
created_at=datetime.utcnow(),
|
| 54 |
+
updated_at=datetime.utcnow(),
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# Save user to database
|
| 58 |
+
success = await self.save_user(user)
|
| 59 |
+
if not success:
|
| 60 |
+
return AuthResponse(
|
| 61 |
+
success=False, message="Failed to create user account"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Create session
|
| 65 |
+
session = UserAuth.create_session(user)
|
| 66 |
+
session_saved = await self.save_session(session)
|
| 67 |
+
|
| 68 |
+
if not session_saved:
|
| 69 |
+
return AuthResponse(
|
| 70 |
+
success=False, message="User created but failed to create session"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
self.logger.info(f"New user registered: {formatted_mobile}")
|
| 74 |
+
|
| 75 |
+
return AuthResponse(
|
| 76 |
+
success=True,
|
| 77 |
+
message="Account created successfully",
|
| 78 |
+
session_id=session.session_id,
|
| 79 |
+
user_id=user.id,
|
| 80 |
+
full_name=user.full_name,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
self.logger.error(f"User registration error: {str(e)}")
|
| 85 |
+
return AuthResponse(
|
| 86 |
+
success=False, message="An error occurred during registration"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
async def login_user(self, login_data: UserLoginRequest) -> AuthResponse:
|
| 90 |
+
"""Authenticate user login"""
|
| 91 |
+
try:
|
| 92 |
+
# Format mobile number consistently
|
| 93 |
+
formatted_mobile = UserAuth.format_mobile_number(login_data.mobile_number)
|
| 94 |
+
|
| 95 |
+
# Get user from database
|
| 96 |
+
user = await self.get_user_by_mobile(formatted_mobile)
|
| 97 |
+
if not user:
|
| 98 |
+
return AuthResponse(
|
| 99 |
+
success=False, message="Invalid mobile number or password"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Verify password
|
| 103 |
+
if not UserAuth.verify_password(login_data.password, user.password_hash):
|
| 104 |
+
return AuthResponse(
|
| 105 |
+
success=False, message="Invalid mobile number or password"
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Check if user is active
|
| 109 |
+
if not user.is_active:
|
| 110 |
+
return AuthResponse(
|
| 111 |
+
success=False,
|
| 112 |
+
message="Account is deactivated. Please contact support.",
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Create new session
|
| 116 |
+
session = UserAuth.create_session(user)
|
| 117 |
+
session_saved = await self.save_session(session)
|
| 118 |
+
|
| 119 |
+
if not session_saved:
|
| 120 |
+
return AuthResponse(
|
| 121 |
+
success=False,
|
| 122 |
+
message="Login successful but failed to create session",
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
self.logger.info(f"User logged in: {formatted_mobile}")
|
| 126 |
+
|
| 127 |
+
return AuthResponse(
|
| 128 |
+
success=True,
|
| 129 |
+
message="Login successful",
|
| 130 |
+
session_id=session.session_id,
|
| 131 |
+
user_id=user.id,
|
| 132 |
+
full_name=user.full_name,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
self.logger.error(f"User login error: {str(e)}")
|
| 137 |
+
return AuthResponse(success=False, message="An error occurred during login")
|
| 138 |
+
|
| 139 |
+
async def validate_session(self, session_id: str) -> Optional[UserSession]:
|
| 140 |
+
"""Validate user session"""
|
| 141 |
+
try:
|
| 142 |
+
if not self.db:
|
| 143 |
+
return None
|
| 144 |
+
|
| 145 |
+
cursor = self.db.cursor()
|
| 146 |
+
cursor.execute(
|
| 147 |
+
"""
|
| 148 |
+
SELECT s.id, s.user_id, u.mobile_number, u.full_name,
|
| 149 |
+
s.created_at, s.expires_at
|
| 150 |
+
FROM sessions s
|
| 151 |
+
JOIN users u ON s.user_id = u.id
|
| 152 |
+
WHERE s.id = ? AND u.is_active = 1
|
| 153 |
+
""",
|
| 154 |
+
(session_id,),
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
row = cursor.fetchone()
|
| 158 |
+
if not row:
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
session = UserSession(
|
| 162 |
+
session_id=row[0],
|
| 163 |
+
user_id=row[1],
|
| 164 |
+
mobile_number=row[2],
|
| 165 |
+
full_name=row[3],
|
| 166 |
+
created_at=datetime.fromisoformat(row[4]),
|
| 167 |
+
expires_at=datetime.fromisoformat(row[5]),
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Check if session is still valid
|
| 171 |
+
if not session.is_valid:
|
| 172 |
+
# Clean up expired session
|
| 173 |
+
await self.delete_session(session_id)
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
return session
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
self.logger.error(f"Session validation error: {str(e)}")
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
async def logout_user(self, session_id: str) -> bool:
|
| 183 |
+
"""Logout user by deleting session"""
|
| 184 |
+
return await self.delete_session(session_id)
|
| 185 |
+
|
| 186 |
+
async def get_user_profile(self, user_id: str) -> Optional[UserProfile]:
|
| 187 |
+
"""Get user profile by user ID"""
|
| 188 |
+
try:
|
| 189 |
+
user = await self.get_user_by_id(user_id)
|
| 190 |
+
if not user:
|
| 191 |
+
return None
|
| 192 |
+
|
| 193 |
+
return UserProfile(
|
| 194 |
+
user_id=user.id,
|
| 195 |
+
full_name=user.full_name,
|
| 196 |
+
mobile_number=UserProfile.mask_mobile_number(user.mobile_number),
|
| 197 |
+
avatar_url=user.avatar_url,
|
| 198 |
+
created_at=user.created_at.isoformat() if user.created_at else None,
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
except Exception as e:
|
| 202 |
+
self.logger.error(f"Get user profile error: {str(e)}")
|
| 203 |
+
return None
|
| 204 |
+
|
| 205 |
+
# Database operations
|
| 206 |
+
async def save_user(self, user: User) -> bool:
|
| 207 |
+
"""Save user to database"""
|
| 208 |
+
try:
|
| 209 |
+
if not self.db:
|
| 210 |
+
return False
|
| 211 |
+
|
| 212 |
+
cursor = self.db.cursor()
|
| 213 |
+
cursor.execute(
|
| 214 |
+
"""
|
| 215 |
+
INSERT INTO users (id, mobile_number, full_name, password_hash,
|
| 216 |
+
avatar_url, preferences, is_active, created_at, updated_at)
|
| 217 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 218 |
+
""",
|
| 219 |
+
(
|
| 220 |
+
user.id,
|
| 221 |
+
user.mobile_number,
|
| 222 |
+
user.full_name,
|
| 223 |
+
user.password_hash,
|
| 224 |
+
user.avatar_url,
|
| 225 |
+
user.preferences,
|
| 226 |
+
user.is_active,
|
| 227 |
+
user.created_at.isoformat() if user.created_at else None,
|
| 228 |
+
user.updated_at.isoformat() if user.updated_at else None,
|
| 229 |
+
),
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
self.db.commit()
|
| 233 |
+
return True
|
| 234 |
+
|
| 235 |
+
except Exception as e:
|
| 236 |
+
self.logger.error(f"Save user error: {str(e)}")
|
| 237 |
+
return False
|
| 238 |
+
|
| 239 |
+
async def get_user_by_mobile(self, mobile_number: str) -> Optional[User]:
|
| 240 |
+
"""Get user by mobile number"""
|
| 241 |
+
try:
|
| 242 |
+
if not self.db:
|
| 243 |
+
return None
|
| 244 |
+
|
| 245 |
+
cursor = self.db.cursor()
|
| 246 |
+
cursor.execute(
|
| 247 |
+
"""
|
| 248 |
+
SELECT id, mobile_number, full_name, password_hash, avatar_url,
|
| 249 |
+
preferences, is_active, created_at, updated_at
|
| 250 |
+
FROM users
|
| 251 |
+
WHERE mobile_number = ?
|
| 252 |
+
""",
|
| 253 |
+
(mobile_number,),
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
row = cursor.fetchone()
|
| 257 |
+
if not row:
|
| 258 |
+
return None
|
| 259 |
+
|
| 260 |
+
return User(
|
| 261 |
+
id=row[0],
|
| 262 |
+
mobile_number=row[1],
|
| 263 |
+
full_name=row[2],
|
| 264 |
+
password_hash=row[3],
|
| 265 |
+
avatar_url=row[4],
|
| 266 |
+
preferences=row[5],
|
| 267 |
+
is_active=bool(row[6]),
|
| 268 |
+
created_at=datetime.fromisoformat(row[7]) if row[7] else None,
|
| 269 |
+
updated_at=datetime.fromisoformat(row[8]) if row[8] else None,
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
except Exception as e:
|
| 273 |
+
self.logger.error(f"Get user by mobile error: {str(e)}")
|
| 274 |
+
return None
|
| 275 |
+
|
| 276 |
+
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
| 277 |
+
"""Get user by ID"""
|
| 278 |
+
try:
|
| 279 |
+
if not self.db:
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
cursor = self.db.cursor()
|
| 283 |
+
cursor.execute(
|
| 284 |
+
"""
|
| 285 |
+
SELECT id, mobile_number, full_name, password_hash, avatar_url,
|
| 286 |
+
preferences, is_active, created_at, updated_at
|
| 287 |
+
FROM users
|
| 288 |
+
WHERE id = ? AND is_active = 1
|
| 289 |
+
""",
|
| 290 |
+
(user_id,),
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
row = cursor.fetchone()
|
| 294 |
+
if not row:
|
| 295 |
+
return None
|
| 296 |
+
|
| 297 |
+
return User(
|
| 298 |
+
id=row[0],
|
| 299 |
+
mobile_number=row[1],
|
| 300 |
+
full_name=row[2],
|
| 301 |
+
password_hash=row[3],
|
| 302 |
+
avatar_url=row[4],
|
| 303 |
+
preferences=row[5],
|
| 304 |
+
is_active=bool(row[6]),
|
| 305 |
+
created_at=datetime.fromisoformat(row[7]) if row[7] else None,
|
| 306 |
+
updated_at=datetime.fromisoformat(row[8]) if row[8] else None,
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
except Exception as e:
|
| 310 |
+
self.logger.error(f"Get user by ID error: {str(e)}")
|
| 311 |
+
return None
|
| 312 |
+
|
| 313 |
+
async def save_session(self, session: UserSession) -> bool:
|
| 314 |
+
"""Save session to database"""
|
| 315 |
+
try:
|
| 316 |
+
if not self.db:
|
| 317 |
+
return False
|
| 318 |
+
|
| 319 |
+
cursor = self.db.cursor()
|
| 320 |
+
cursor.execute(
|
| 321 |
+
"""
|
| 322 |
+
INSERT INTO sessions (id, user_id, title, metadata, created_at,
|
| 323 |
+
updated_at, expires_at)
|
| 324 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 325 |
+
""",
|
| 326 |
+
(
|
| 327 |
+
session.session_id,
|
| 328 |
+
session.user_id,
|
| 329 |
+
"User Session",
|
| 330 |
+
json.dumps({"login_type": "mobile_password"}),
|
| 331 |
+
session.created_at.isoformat(),
|
| 332 |
+
session.created_at.isoformat(),
|
| 333 |
+
session.expires_at.isoformat(),
|
| 334 |
+
),
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
self.db.commit()
|
| 338 |
+
return True
|
| 339 |
+
|
| 340 |
+
except Exception as e:
|
| 341 |
+
self.logger.error(f"Save session error: {str(e)}")
|
| 342 |
+
return False
|
| 343 |
+
|
| 344 |
+
async def delete_session(self, session_id: str) -> bool:
|
| 345 |
+
"""Delete session from database"""
|
| 346 |
+
try:
|
| 347 |
+
if not self.db:
|
| 348 |
+
return False
|
| 349 |
+
|
| 350 |
+
cursor = self.db.cursor()
|
| 351 |
+
cursor.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
|
| 352 |
+
self.db.commit()
|
| 353 |
+
return True
|
| 354 |
+
|
| 355 |
+
except Exception as e:
|
| 356 |
+
self.logger.error(f"Delete session error: {str(e)}")
|
| 357 |
+
return False
|
app/cloudflare/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cloudflare services integration for OpenManus
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .client import CloudflareClient
|
| 6 |
+
from .d1 import D1Database
|
| 7 |
+
from .durable_objects import DurableObjects
|
| 8 |
+
from .kv import KVStorage
|
| 9 |
+
from .r2 import R2Storage
|
| 10 |
+
|
| 11 |
+
__all__ = ["CloudflareClient", "D1Database", "R2Storage", "KVStorage", "DurableObjects"]
|
app/cloudflare/client.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cloudflare API Client
|
| 3 |
+
Handles authentication and base HTTP operations for Cloudflare services
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import json
|
| 8 |
+
from typing import Any, Dict, Optional, Union
|
| 9 |
+
|
| 10 |
+
import aiohttp
|
| 11 |
+
|
| 12 |
+
from app.logger import logger
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class CloudflareClient:
|
| 16 |
+
"""Base client for Cloudflare API operations"""
|
| 17 |
+
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
api_token: str,
|
| 21 |
+
account_id: str,
|
| 22 |
+
worker_url: Optional[str] = None,
|
| 23 |
+
timeout: int = 30,
|
| 24 |
+
):
|
| 25 |
+
self.api_token = api_token
|
| 26 |
+
self.account_id = account_id
|
| 27 |
+
self.worker_url = worker_url
|
| 28 |
+
self.timeout = timeout
|
| 29 |
+
self.base_url = "https://api.cloudflare.com/client/v4"
|
| 30 |
+
|
| 31 |
+
# HTTP headers for API requests
|
| 32 |
+
self.headers = {
|
| 33 |
+
"Authorization": f"Bearer {api_token}",
|
| 34 |
+
"Content-Type": "application/json",
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async def _make_request(
|
| 38 |
+
self,
|
| 39 |
+
method: str,
|
| 40 |
+
url: str,
|
| 41 |
+
data: Optional[Dict[str, Any]] = None,
|
| 42 |
+
headers: Optional[Dict[str, str]] = None,
|
| 43 |
+
use_worker: bool = False,
|
| 44 |
+
) -> Dict[str, Any]:
|
| 45 |
+
"""Make HTTP request to Cloudflare API or Worker"""
|
| 46 |
+
|
| 47 |
+
# Use worker URL if specified and use_worker is True
|
| 48 |
+
if use_worker and self.worker_url:
|
| 49 |
+
full_url = f"{self.worker_url.rstrip('/')}/{url.lstrip('/')}"
|
| 50 |
+
else:
|
| 51 |
+
full_url = f"{self.base_url}/{url.lstrip('/')}"
|
| 52 |
+
|
| 53 |
+
request_headers = self.headers.copy()
|
| 54 |
+
if headers:
|
| 55 |
+
request_headers.update(headers)
|
| 56 |
+
|
| 57 |
+
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
| 61 |
+
async with session.request(
|
| 62 |
+
method=method.upper(),
|
| 63 |
+
url=full_url,
|
| 64 |
+
headers=request_headers,
|
| 65 |
+
json=data if data else None,
|
| 66 |
+
) as response:
|
| 67 |
+
response_text = await response.text()
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
response_data = (
|
| 71 |
+
json.loads(response_text) if response_text else {}
|
| 72 |
+
)
|
| 73 |
+
except json.JSONDecodeError:
|
| 74 |
+
response_data = {"raw_response": response_text}
|
| 75 |
+
|
| 76 |
+
if not response.ok:
|
| 77 |
+
logger.error(
|
| 78 |
+
f"Cloudflare API error: {response.status} - {response_text}"
|
| 79 |
+
)
|
| 80 |
+
raise CloudflareError(
|
| 81 |
+
f"HTTP {response.status}: {response_text}",
|
| 82 |
+
response.status,
|
| 83 |
+
response_data,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
return response_data
|
| 87 |
+
|
| 88 |
+
except asyncio.TimeoutError:
|
| 89 |
+
logger.error(f"Timeout making request to {full_url}")
|
| 90 |
+
raise CloudflareError(f"Request timeout after {self.timeout}s")
|
| 91 |
+
except aiohttp.ClientError as e:
|
| 92 |
+
logger.error(f"HTTP client error: {e}")
|
| 93 |
+
raise CloudflareError(f"Client error: {e}")
|
| 94 |
+
|
| 95 |
+
async def get(
|
| 96 |
+
self,
|
| 97 |
+
url: str,
|
| 98 |
+
headers: Optional[Dict[str, str]] = None,
|
| 99 |
+
use_worker: bool = False,
|
| 100 |
+
) -> Dict[str, Any]:
|
| 101 |
+
"""Make GET request"""
|
| 102 |
+
return await self._make_request(
|
| 103 |
+
"GET", url, headers=headers, use_worker=use_worker
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
async def post(
|
| 107 |
+
self,
|
| 108 |
+
url: str,
|
| 109 |
+
data: Optional[Dict[str, Any]] = None,
|
| 110 |
+
headers: Optional[Dict[str, str]] = None,
|
| 111 |
+
use_worker: bool = False,
|
| 112 |
+
) -> Dict[str, Any]:
|
| 113 |
+
"""Make POST request"""
|
| 114 |
+
return await self._make_request(
|
| 115 |
+
"POST", url, data=data, headers=headers, use_worker=use_worker
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
async def put(
|
| 119 |
+
self,
|
| 120 |
+
url: str,
|
| 121 |
+
data: Optional[Dict[str, Any]] = None,
|
| 122 |
+
headers: Optional[Dict[str, str]] = None,
|
| 123 |
+
use_worker: bool = False,
|
| 124 |
+
) -> Dict[str, Any]:
|
| 125 |
+
"""Make PUT request"""
|
| 126 |
+
return await self._make_request(
|
| 127 |
+
"PUT", url, data=data, headers=headers, use_worker=use_worker
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
async def delete(
|
| 131 |
+
self,
|
| 132 |
+
url: str,
|
| 133 |
+
headers: Optional[Dict[str, str]] = None,
|
| 134 |
+
use_worker: bool = False,
|
| 135 |
+
) -> Dict[str, Any]:
|
| 136 |
+
"""Make DELETE request"""
|
| 137 |
+
return await self._make_request(
|
| 138 |
+
"DELETE", url, headers=headers, use_worker=use_worker
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
async def upload_file(
|
| 142 |
+
self,
|
| 143 |
+
url: str,
|
| 144 |
+
file_data: bytes,
|
| 145 |
+
content_type: str = "application/octet-stream",
|
| 146 |
+
headers: Optional[Dict[str, str]] = None,
|
| 147 |
+
use_worker: bool = False,
|
| 148 |
+
) -> Dict[str, Any]:
|
| 149 |
+
"""Upload file data"""
|
| 150 |
+
|
| 151 |
+
# Use worker URL if specified and use_worker is True
|
| 152 |
+
if use_worker and self.worker_url:
|
| 153 |
+
full_url = f"{self.worker_url.rstrip('/')}/{url.lstrip('/')}"
|
| 154 |
+
else:
|
| 155 |
+
full_url = f"{self.base_url}/{url.lstrip('/')}"
|
| 156 |
+
|
| 157 |
+
upload_headers = {
|
| 158 |
+
"Authorization": f"Bearer {self.api_token}",
|
| 159 |
+
"Content-Type": content_type,
|
| 160 |
+
}
|
| 161 |
+
if headers:
|
| 162 |
+
upload_headers.update(headers)
|
| 163 |
+
|
| 164 |
+
timeout = aiohttp.ClientTimeout(
|
| 165 |
+
total=self.timeout * 2
|
| 166 |
+
) # Longer timeout for uploads
|
| 167 |
+
|
| 168 |
+
try:
|
| 169 |
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
| 170 |
+
async with session.put(
|
| 171 |
+
url=full_url, headers=upload_headers, data=file_data
|
| 172 |
+
) as response:
|
| 173 |
+
response_text = await response.text()
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
response_data = (
|
| 177 |
+
json.loads(response_text) if response_text else {}
|
| 178 |
+
)
|
| 179 |
+
except json.JSONDecodeError:
|
| 180 |
+
response_data = {"raw_response": response_text}
|
| 181 |
+
|
| 182 |
+
if not response.ok:
|
| 183 |
+
logger.error(
|
| 184 |
+
f"File upload error: {response.status} - {response_text}"
|
| 185 |
+
)
|
| 186 |
+
raise CloudflareError(
|
| 187 |
+
f"Upload failed: HTTP {response.status}",
|
| 188 |
+
response.status,
|
| 189 |
+
response_data,
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
return response_data
|
| 193 |
+
|
| 194 |
+
except asyncio.TimeoutError:
|
| 195 |
+
logger.error(f"Timeout uploading file to {full_url}")
|
| 196 |
+
raise CloudflareError(f"Upload timeout after {self.timeout * 2}s")
|
| 197 |
+
except aiohttp.ClientError as e:
|
| 198 |
+
logger.error(f"Upload client error: {e}")
|
| 199 |
+
raise CloudflareError(f"Upload error: {e}")
|
| 200 |
+
|
| 201 |
+
def get_account_url(self, endpoint: str) -> str:
|
| 202 |
+
"""Get URL for account-scoped endpoint"""
|
| 203 |
+
return f"accounts/{self.account_id}/{endpoint}"
|
| 204 |
+
|
| 205 |
+
def get_worker_url(self, endpoint: str) -> str:
|
| 206 |
+
"""Get URL for worker endpoint"""
|
| 207 |
+
if not self.worker_url:
|
| 208 |
+
raise CloudflareError("Worker URL not configured")
|
| 209 |
+
return endpoint
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class CloudflareError(Exception):
|
| 213 |
+
"""Cloudflare API error"""
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
message: str,
|
| 218 |
+
status_code: Optional[int] = None,
|
| 219 |
+
response_data: Optional[Dict[str, Any]] = None,
|
| 220 |
+
):
|
| 221 |
+
super().__init__(message)
|
| 222 |
+
self.status_code = status_code
|
| 223 |
+
self.response_data = response_data or {}
|
| 224 |
+
|
| 225 |
+
def __str__(self) -> str:
|
| 226 |
+
if self.status_code:
|
| 227 |
+
return f"CloudflareError({self.status_code}): {super().__str__()}"
|
| 228 |
+
return f"CloudflareError: {super().__str__()}"
|
app/cloudflare/d1.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
D1 Database integration for OpenManus
|
| 3 |
+
Provides interface to Cloudflare D1 database operations
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Any, Dict, List, Optional, Union
|
| 7 |
+
|
| 8 |
+
from app.logger import logger
|
| 9 |
+
|
| 10 |
+
from .client import CloudflareClient, CloudflareError
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class D1Database:
|
| 14 |
+
"""Cloudflare D1 Database client"""
|
| 15 |
+
|
| 16 |
+
def __init__(self, client: CloudflareClient, database_id: str):
|
| 17 |
+
self.client = client
|
| 18 |
+
self.database_id = database_id
|
| 19 |
+
self.base_endpoint = f"accounts/{client.account_id}/d1/database/{database_id}"
|
| 20 |
+
|
| 21 |
+
async def execute_query(
|
| 22 |
+
self, sql: str, params: Optional[List[Any]] = None, use_worker: bool = True
|
| 23 |
+
) -> Dict[str, Any]:
|
| 24 |
+
"""Execute a SQL query"""
|
| 25 |
+
|
| 26 |
+
query_data = {"sql": sql}
|
| 27 |
+
|
| 28 |
+
if params:
|
| 29 |
+
query_data["params"] = params
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
if use_worker:
|
| 33 |
+
# Use worker endpoint for better performance
|
| 34 |
+
response = await self.client.post(
|
| 35 |
+
"api/database/query", data=query_data, use_worker=True
|
| 36 |
+
)
|
| 37 |
+
else:
|
| 38 |
+
# Use Cloudflare API directly
|
| 39 |
+
response = await self.client.post(
|
| 40 |
+
f"{self.base_endpoint}/query", data=query_data
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
return response
|
| 44 |
+
|
| 45 |
+
except CloudflareError as e:
|
| 46 |
+
logger.error(f"D1 query execution failed: {e}")
|
| 47 |
+
raise
|
| 48 |
+
|
| 49 |
+
async def batch_execute(
|
| 50 |
+
self, queries: List[Dict[str, Any]], use_worker: bool = True
|
| 51 |
+
) -> Dict[str, Any]:
|
| 52 |
+
"""Execute multiple queries in a batch"""
|
| 53 |
+
|
| 54 |
+
batch_data = {"queries": queries}
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
if use_worker:
|
| 58 |
+
response = await self.client.post(
|
| 59 |
+
"api/database/batch", data=batch_data, use_worker=True
|
| 60 |
+
)
|
| 61 |
+
else:
|
| 62 |
+
response = await self.client.post(
|
| 63 |
+
f"{self.base_endpoint}/query", data=batch_data
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
return response
|
| 67 |
+
|
| 68 |
+
except CloudflareError as e:
|
| 69 |
+
logger.error(f"D1 batch execution failed: {e}")
|
| 70 |
+
raise
|
| 71 |
+
|
| 72 |
+
# User management methods
|
| 73 |
+
async def create_user(
|
| 74 |
+
self,
|
| 75 |
+
user_id: str,
|
| 76 |
+
username: str,
|
| 77 |
+
email: Optional[str] = None,
|
| 78 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 79 |
+
) -> Dict[str, Any]:
|
| 80 |
+
"""Create a new user"""
|
| 81 |
+
|
| 82 |
+
sql = """
|
| 83 |
+
INSERT INTO users (id, username, email, metadata)
|
| 84 |
+
VALUES (?, ?, ?, ?)
|
| 85 |
+
ON CONFLICT(id) DO UPDATE SET
|
| 86 |
+
username = excluded.username,
|
| 87 |
+
email = excluded.email,
|
| 88 |
+
metadata = excluded.metadata,
|
| 89 |
+
updated_at = strftime('%s', 'now')
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
import json
|
| 93 |
+
|
| 94 |
+
params = [user_id, username, email, json.dumps(metadata or {})]
|
| 95 |
+
|
| 96 |
+
return await self.execute_query(sql, params)
|
| 97 |
+
|
| 98 |
+
async def get_user(self, user_id: str) -> Optional[Dict[str, Any]]:
|
| 99 |
+
"""Get user by ID"""
|
| 100 |
+
|
| 101 |
+
sql = "SELECT * FROM users WHERE id = ?"
|
| 102 |
+
params = [user_id]
|
| 103 |
+
|
| 104 |
+
result = await self.execute_query(sql, params)
|
| 105 |
+
|
| 106 |
+
# Parse response based on Cloudflare D1 format
|
| 107 |
+
if result.get("success") and result.get("result"):
|
| 108 |
+
rows = result["result"][0].get("results", [])
|
| 109 |
+
if rows:
|
| 110 |
+
user = rows[0]
|
| 111 |
+
if user.get("metadata"):
|
| 112 |
+
import json
|
| 113 |
+
|
| 114 |
+
user["metadata"] = json.loads(user["metadata"])
|
| 115 |
+
return user
|
| 116 |
+
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
async def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]:
|
| 120 |
+
"""Get user by username"""
|
| 121 |
+
|
| 122 |
+
sql = "SELECT * FROM users WHERE username = ?"
|
| 123 |
+
params = [username]
|
| 124 |
+
|
| 125 |
+
result = await self.execute_query(sql, params)
|
| 126 |
+
|
| 127 |
+
if result.get("success") and result.get("result"):
|
| 128 |
+
rows = result["result"][0].get("results", [])
|
| 129 |
+
if rows:
|
| 130 |
+
user = rows[0]
|
| 131 |
+
if user.get("metadata"):
|
| 132 |
+
import json
|
| 133 |
+
|
| 134 |
+
user["metadata"] = json.loads(user["metadata"])
|
| 135 |
+
return user
|
| 136 |
+
|
| 137 |
+
return None
|
| 138 |
+
|
| 139 |
+
# Session management methods
|
| 140 |
+
async def create_session(
|
| 141 |
+
self,
|
| 142 |
+
session_id: str,
|
| 143 |
+
user_id: str,
|
| 144 |
+
session_data: Dict[str, Any],
|
| 145 |
+
expires_at: Optional[int] = None,
|
| 146 |
+
) -> Dict[str, Any]:
|
| 147 |
+
"""Create a new session"""
|
| 148 |
+
|
| 149 |
+
sql = """
|
| 150 |
+
INSERT INTO sessions (id, user_id, session_data, expires_at)
|
| 151 |
+
VALUES (?, ?, ?, ?)
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
import json
|
| 155 |
+
|
| 156 |
+
params = [session_id, user_id, json.dumps(session_data), expires_at]
|
| 157 |
+
|
| 158 |
+
return await self.execute_query(sql, params)
|
| 159 |
+
|
| 160 |
+
async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 161 |
+
"""Get session by ID"""
|
| 162 |
+
|
| 163 |
+
sql = """
|
| 164 |
+
SELECT * FROM sessions
|
| 165 |
+
WHERE id = ? AND (expires_at IS NULL OR expires_at > strftime('%s', 'now'))
|
| 166 |
+
"""
|
| 167 |
+
params = [session_id]
|
| 168 |
+
|
| 169 |
+
result = await self.execute_query(sql, params)
|
| 170 |
+
|
| 171 |
+
if result.get("success") and result.get("result"):
|
| 172 |
+
rows = result["result"][0].get("results", [])
|
| 173 |
+
if rows:
|
| 174 |
+
session = rows[0]
|
| 175 |
+
if session.get("session_data"):
|
| 176 |
+
import json
|
| 177 |
+
|
| 178 |
+
session["session_data"] = json.loads(session["session_data"])
|
| 179 |
+
return session
|
| 180 |
+
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
async def delete_session(self, session_id: str) -> Dict[str, Any]:
|
| 184 |
+
"""Delete a session"""
|
| 185 |
+
|
| 186 |
+
sql = "DELETE FROM sessions WHERE id = ?"
|
| 187 |
+
params = [session_id]
|
| 188 |
+
|
| 189 |
+
return await self.execute_query(sql, params)
|
| 190 |
+
|
| 191 |
+
# Conversation methods
|
| 192 |
+
async def create_conversation(
|
| 193 |
+
self,
|
| 194 |
+
conversation_id: str,
|
| 195 |
+
user_id: str,
|
| 196 |
+
title: Optional[str] = None,
|
| 197 |
+
messages: Optional[List[Dict[str, Any]]] = None,
|
| 198 |
+
) -> Dict[str, Any]:
|
| 199 |
+
"""Create a new conversation"""
|
| 200 |
+
|
| 201 |
+
sql = """
|
| 202 |
+
INSERT INTO conversations (id, user_id, title, messages)
|
| 203 |
+
VALUES (?, ?, ?, ?)
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
import json
|
| 207 |
+
|
| 208 |
+
params = [conversation_id, user_id, title, json.dumps(messages or [])]
|
| 209 |
+
|
| 210 |
+
return await self.execute_query(sql, params)
|
| 211 |
+
|
| 212 |
+
async def get_conversation(self, conversation_id: str) -> Optional[Dict[str, Any]]:
|
| 213 |
+
"""Get conversation by ID"""
|
| 214 |
+
|
| 215 |
+
sql = "SELECT * FROM conversations WHERE id = ?"
|
| 216 |
+
params = [conversation_id]
|
| 217 |
+
|
| 218 |
+
result = await self.execute_query(sql, params)
|
| 219 |
+
|
| 220 |
+
if result.get("success") and result.get("result"):
|
| 221 |
+
rows = result["result"][0].get("results", [])
|
| 222 |
+
if rows:
|
| 223 |
+
conversation = rows[0]
|
| 224 |
+
if conversation.get("messages"):
|
| 225 |
+
import json
|
| 226 |
+
|
| 227 |
+
conversation["messages"] = json.loads(conversation["messages"])
|
| 228 |
+
return conversation
|
| 229 |
+
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
async def update_conversation_messages(
|
| 233 |
+
self, conversation_id: str, messages: List[Dict[str, Any]]
|
| 234 |
+
) -> Dict[str, Any]:
|
| 235 |
+
"""Update conversation messages"""
|
| 236 |
+
|
| 237 |
+
sql = """
|
| 238 |
+
UPDATE conversations
|
| 239 |
+
SET messages = ?, updated_at = strftime('%s', 'now')
|
| 240 |
+
WHERE id = ?
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
import json
|
| 244 |
+
|
| 245 |
+
params = [json.dumps(messages), conversation_id]
|
| 246 |
+
|
| 247 |
+
return await self.execute_query(sql, params)
|
| 248 |
+
|
| 249 |
+
async def get_user_conversations(
|
| 250 |
+
self, user_id: str, limit: int = 50
|
| 251 |
+
) -> List[Dict[str, Any]]:
|
| 252 |
+
"""Get user's conversations"""
|
| 253 |
+
|
| 254 |
+
sql = """
|
| 255 |
+
SELECT id, user_id, title, created_at, updated_at
|
| 256 |
+
FROM conversations
|
| 257 |
+
WHERE user_id = ?
|
| 258 |
+
ORDER BY updated_at DESC
|
| 259 |
+
LIMIT ?
|
| 260 |
+
"""
|
| 261 |
+
params = [user_id, limit]
|
| 262 |
+
|
| 263 |
+
result = await self.execute_query(sql, params)
|
| 264 |
+
|
| 265 |
+
if result.get("success") and result.get("result"):
|
| 266 |
+
return result["result"][0].get("results", [])
|
| 267 |
+
|
| 268 |
+
return []
|
| 269 |
+
|
| 270 |
+
# Agent execution methods
|
| 271 |
+
async def create_agent_execution(
|
| 272 |
+
self,
|
| 273 |
+
execution_id: str,
|
| 274 |
+
user_id: str,
|
| 275 |
+
session_id: Optional[str] = None,
|
| 276 |
+
task_description: Optional[str] = None,
|
| 277 |
+
status: str = "pending",
|
| 278 |
+
) -> Dict[str, Any]:
|
| 279 |
+
"""Create a new agent execution record"""
|
| 280 |
+
|
| 281 |
+
sql = """
|
| 282 |
+
INSERT INTO agent_executions (id, user_id, session_id, task_description, status)
|
| 283 |
+
VALUES (?, ?, ?, ?, ?)
|
| 284 |
+
"""
|
| 285 |
+
|
| 286 |
+
params = [execution_id, user_id, session_id, task_description, status]
|
| 287 |
+
|
| 288 |
+
return await self.execute_query(sql, params)
|
| 289 |
+
|
| 290 |
+
async def update_agent_execution(
|
| 291 |
+
self,
|
| 292 |
+
execution_id: str,
|
| 293 |
+
status: Optional[str] = None,
|
| 294 |
+
result: Optional[str] = None,
|
| 295 |
+
execution_time: Optional[int] = None,
|
| 296 |
+
) -> Dict[str, Any]:
|
| 297 |
+
"""Update agent execution record"""
|
| 298 |
+
|
| 299 |
+
updates = []
|
| 300 |
+
params = []
|
| 301 |
+
|
| 302 |
+
if status:
|
| 303 |
+
updates.append("status = ?")
|
| 304 |
+
params.append(status)
|
| 305 |
+
|
| 306 |
+
if result:
|
| 307 |
+
updates.append("result = ?")
|
| 308 |
+
params.append(result)
|
| 309 |
+
|
| 310 |
+
if execution_time is not None:
|
| 311 |
+
updates.append("execution_time = ?")
|
| 312 |
+
params.append(execution_time)
|
| 313 |
+
|
| 314 |
+
if status in ["completed", "failed"]:
|
| 315 |
+
updates.append("completed_at = strftime('%s', 'now')")
|
| 316 |
+
|
| 317 |
+
if not updates:
|
| 318 |
+
return {"success": True, "message": "No updates provided"}
|
| 319 |
+
|
| 320 |
+
sql = f"""
|
| 321 |
+
UPDATE agent_executions
|
| 322 |
+
SET {', '.join(updates)}
|
| 323 |
+
WHERE id = ?
|
| 324 |
+
"""
|
| 325 |
+
params.append(execution_id)
|
| 326 |
+
|
| 327 |
+
return await self.execute_query(sql, params)
|
| 328 |
+
|
| 329 |
+
async def get_agent_execution(self, execution_id: str) -> Optional[Dict[str, Any]]:
|
| 330 |
+
"""Get agent execution by ID"""
|
| 331 |
+
|
| 332 |
+
sql = "SELECT * FROM agent_executions WHERE id = ?"
|
| 333 |
+
params = [execution_id]
|
| 334 |
+
|
| 335 |
+
result = await self.execute_query(sql, params)
|
| 336 |
+
|
| 337 |
+
if result.get("success") and result.get("result"):
|
| 338 |
+
rows = result["result"][0].get("results", [])
|
| 339 |
+
if rows:
|
| 340 |
+
return rows[0]
|
| 341 |
+
|
| 342 |
+
return None
|
| 343 |
+
|
| 344 |
+
async def get_user_executions(
|
| 345 |
+
self, user_id: str, limit: int = 50
|
| 346 |
+
) -> List[Dict[str, Any]]:
|
| 347 |
+
"""Get user's agent executions"""
|
| 348 |
+
|
| 349 |
+
sql = """
|
| 350 |
+
SELECT * FROM agent_executions
|
| 351 |
+
WHERE user_id = ?
|
| 352 |
+
ORDER BY created_at DESC
|
| 353 |
+
LIMIT ?
|
| 354 |
+
"""
|
| 355 |
+
params = [user_id, limit]
|
| 356 |
+
|
| 357 |
+
result = await self.execute_query(sql, params)
|
| 358 |
+
|
| 359 |
+
if result.get("success") and result.get("result"):
|
| 360 |
+
return result["result"][0].get("results", [])
|
| 361 |
+
|
| 362 |
+
return []
|
| 363 |
+
|
| 364 |
+
# File record methods
|
| 365 |
+
async def create_file_record(
|
| 366 |
+
self,
|
| 367 |
+
file_id: str,
|
| 368 |
+
user_id: str,
|
| 369 |
+
filename: str,
|
| 370 |
+
file_key: str,
|
| 371 |
+
file_size: int,
|
| 372 |
+
content_type: str,
|
| 373 |
+
bucket: str = "storage",
|
| 374 |
+
) -> Dict[str, Any]:
|
| 375 |
+
"""Create a file record"""
|
| 376 |
+
|
| 377 |
+
sql = """
|
| 378 |
+
INSERT INTO files (id, user_id, filename, file_key, file_size, content_type, bucket)
|
| 379 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 380 |
+
"""
|
| 381 |
+
|
| 382 |
+
params = [file_id, user_id, filename, file_key, file_size, content_type, bucket]
|
| 383 |
+
|
| 384 |
+
return await self.execute_query(sql, params)
|
| 385 |
+
|
| 386 |
+
async def get_file_record(self, file_id: str) -> Optional[Dict[str, Any]]:
|
| 387 |
+
"""Get file record by ID"""
|
| 388 |
+
|
| 389 |
+
sql = "SELECT * FROM files WHERE id = ?"
|
| 390 |
+
params = [file_id]
|
| 391 |
+
|
| 392 |
+
result = await self.execute_query(sql, params)
|
| 393 |
+
|
| 394 |
+
if result.get("success") and result.get("result"):
|
| 395 |
+
rows = result["result"][0].get("results", [])
|
| 396 |
+
if rows:
|
| 397 |
+
return rows[0]
|
| 398 |
+
|
| 399 |
+
return None
|
| 400 |
+
|
| 401 |
+
async def get_user_files(
|
| 402 |
+
self, user_id: str, limit: int = 100
|
| 403 |
+
) -> List[Dict[str, Any]]:
|
| 404 |
+
"""Get user's files"""
|
| 405 |
+
|
| 406 |
+
sql = """
|
| 407 |
+
SELECT * FROM files
|
| 408 |
+
WHERE user_id = ?
|
| 409 |
+
ORDER BY created_at DESC
|
| 410 |
+
LIMIT ?
|
| 411 |
+
"""
|
| 412 |
+
params = [user_id, limit]
|
| 413 |
+
|
| 414 |
+
result = await self.execute_query(sql, params)
|
| 415 |
+
|
| 416 |
+
if result.get("success") and result.get("result"):
|
| 417 |
+
return result["result"][0].get("results", [])
|
| 418 |
+
|
| 419 |
+
return []
|
| 420 |
+
|
| 421 |
+
async def delete_file_record(self, file_id: str) -> Dict[str, Any]:
|
| 422 |
+
"""Delete a file record"""
|
| 423 |
+
|
| 424 |
+
sql = "DELETE FROM files WHERE id = ?"
|
| 425 |
+
params = [file_id]
|
| 426 |
+
|
| 427 |
+
return await self.execute_query(sql, params)
|
| 428 |
+
|
| 429 |
+
# Schema initialization
|
| 430 |
+
async def initialize_schema(self) -> Dict[str, Any]:
|
| 431 |
+
"""Initialize database schema"""
|
| 432 |
+
|
| 433 |
+
schema_queries = [
|
| 434 |
+
{
|
| 435 |
+
"sql": """CREATE TABLE IF NOT EXISTS users (
|
| 436 |
+
id TEXT PRIMARY KEY,
|
| 437 |
+
username TEXT UNIQUE NOT NULL,
|
| 438 |
+
email TEXT UNIQUE,
|
| 439 |
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
| 440 |
+
updated_at INTEGER DEFAULT (strftime('%s', 'now')),
|
| 441 |
+
metadata TEXT
|
| 442 |
+
)"""
|
| 443 |
+
},
|
| 444 |
+
{
|
| 445 |
+
"sql": """CREATE TABLE IF NOT EXISTS sessions (
|
| 446 |
+
id TEXT PRIMARY KEY,
|
| 447 |
+
user_id TEXT NOT NULL,
|
| 448 |
+
session_data TEXT,
|
| 449 |
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
| 450 |
+
expires_at INTEGER,
|
| 451 |
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
| 452 |
+
)"""
|
| 453 |
+
},
|
| 454 |
+
{
|
| 455 |
+
"sql": """CREATE TABLE IF NOT EXISTS conversations (
|
| 456 |
+
id TEXT PRIMARY KEY,
|
| 457 |
+
user_id TEXT NOT NULL,
|
| 458 |
+
title TEXT,
|
| 459 |
+
messages TEXT,
|
| 460 |
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
| 461 |
+
updated_at INTEGER DEFAULT (strftime('%s', 'now')),
|
| 462 |
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
| 463 |
+
)"""
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"sql": """CREATE TABLE IF NOT EXISTS files (
|
| 467 |
+
id TEXT PRIMARY KEY,
|
| 468 |
+
user_id TEXT NOT NULL,
|
| 469 |
+
filename TEXT NOT NULL,
|
| 470 |
+
file_key TEXT NOT NULL,
|
| 471 |
+
file_size INTEGER,
|
| 472 |
+
content_type TEXT,
|
| 473 |
+
bucket TEXT DEFAULT 'storage',
|
| 474 |
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
| 475 |
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
| 476 |
+
)"""
|
| 477 |
+
},
|
| 478 |
+
{
|
| 479 |
+
"sql": """CREATE TABLE IF NOT EXISTS agent_executions (
|
| 480 |
+
id TEXT PRIMARY KEY,
|
| 481 |
+
user_id TEXT NOT NULL,
|
| 482 |
+
session_id TEXT,
|
| 483 |
+
task_description TEXT,
|
| 484 |
+
status TEXT DEFAULT 'pending',
|
| 485 |
+
result TEXT,
|
| 486 |
+
execution_time INTEGER,
|
| 487 |
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
| 488 |
+
completed_at INTEGER,
|
| 489 |
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
| 490 |
+
)"""
|
| 491 |
+
},
|
| 492 |
+
]
|
| 493 |
+
|
| 494 |
+
# Add indexes
|
| 495 |
+
index_queries = [
|
| 496 |
+
{
|
| 497 |
+
"sql": "CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id)"
|
| 498 |
+
},
|
| 499 |
+
{
|
| 500 |
+
"sql": "CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id)"
|
| 501 |
+
},
|
| 502 |
+
{"sql": "CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id)"},
|
| 503 |
+
{
|
| 504 |
+
"sql": "CREATE INDEX IF NOT EXISTS idx_agent_executions_user_id ON agent_executions(user_id)"
|
| 505 |
+
},
|
| 506 |
+
]
|
| 507 |
+
|
| 508 |
+
all_queries = schema_queries + index_queries
|
| 509 |
+
|
| 510 |
+
return await self.batch_execute(all_queries)
|
app/cloudflare/durable_objects.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Durable Objects integration for OpenManus
|
| 3 |
+
Provides interface to Cloudflare Durable Objects operations
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import time
|
| 8 |
+
from typing import Any, Dict, List, Optional
|
| 9 |
+
|
| 10 |
+
from app.logger import logger
|
| 11 |
+
|
| 12 |
+
from .client import CloudflareClient, CloudflareError
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class DurableObjects:
|
| 16 |
+
"""Cloudflare Durable Objects client"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, client: CloudflareClient):
|
| 19 |
+
self.client = client
|
| 20 |
+
|
| 21 |
+
async def create_agent_session(
|
| 22 |
+
self, session_id: str, user_id: str, metadata: Optional[Dict[str, Any]] = None
|
| 23 |
+
) -> Dict[str, Any]:
|
| 24 |
+
"""Create a new agent session"""
|
| 25 |
+
|
| 26 |
+
session_data = {
|
| 27 |
+
"sessionId": session_id,
|
| 28 |
+
"userId": user_id,
|
| 29 |
+
"metadata": metadata or {},
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
response = await self.client.post(
|
| 34 |
+
f"do/agent/{session_id}/start", data=session_data, use_worker=True
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
return {
|
| 38 |
+
"success": True,
|
| 39 |
+
"session_id": session_id,
|
| 40 |
+
"user_id": user_id,
|
| 41 |
+
**response,
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
except CloudflareError as e:
|
| 45 |
+
logger.error(f"Failed to create agent session: {e}")
|
| 46 |
+
raise
|
| 47 |
+
|
| 48 |
+
async def get_agent_session_status(self, session_id: str) -> Dict[str, Any]:
|
| 49 |
+
"""Get agent session status"""
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
response = await self.client.get(
|
| 53 |
+
f"do/agent/{session_id}/status?sessionId={session_id}", use_worker=True
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
return response
|
| 57 |
+
|
| 58 |
+
except CloudflareError as e:
|
| 59 |
+
logger.error(f"Failed to get agent session status: {e}")
|
| 60 |
+
raise
|
| 61 |
+
|
| 62 |
+
async def update_agent_session(
|
| 63 |
+
self, session_id: str, updates: Dict[str, Any]
|
| 64 |
+
) -> Dict[str, Any]:
|
| 65 |
+
"""Update agent session"""
|
| 66 |
+
|
| 67 |
+
update_data = {"sessionId": session_id, "updates": updates}
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
response = await self.client.post(
|
| 71 |
+
f"do/agent/{session_id}/update", data=update_data, use_worker=True
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
return {"success": True, "session_id": session_id, **response}
|
| 75 |
+
|
| 76 |
+
except CloudflareError as e:
|
| 77 |
+
logger.error(f"Failed to update agent session: {e}")
|
| 78 |
+
raise
|
| 79 |
+
|
| 80 |
+
async def stop_agent_session(self, session_id: str) -> Dict[str, Any]:
|
| 81 |
+
"""Stop agent session"""
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
response = await self.client.post(
|
| 85 |
+
f"do/agent/{session_id}/stop",
|
| 86 |
+
data={"sessionId": session_id},
|
| 87 |
+
use_worker=True,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
return {"success": True, "session_id": session_id, **response}
|
| 91 |
+
|
| 92 |
+
except CloudflareError as e:
|
| 93 |
+
logger.error(f"Failed to stop agent session: {e}")
|
| 94 |
+
raise
|
| 95 |
+
|
| 96 |
+
async def add_agent_message(
|
| 97 |
+
self, session_id: str, message: Dict[str, Any]
|
| 98 |
+
) -> Dict[str, Any]:
|
| 99 |
+
"""Add a message to agent session"""
|
| 100 |
+
|
| 101 |
+
message_data = {
|
| 102 |
+
"sessionId": session_id,
|
| 103 |
+
"message": {"timestamp": int(time.time()), **message},
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
response = await self.client.post(
|
| 108 |
+
f"do/agent/{session_id}/messages", data=message_data, use_worker=True
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
return {"success": True, "session_id": session_id, **response}
|
| 112 |
+
|
| 113 |
+
except CloudflareError as e:
|
| 114 |
+
logger.error(f"Failed to add agent message: {e}")
|
| 115 |
+
raise
|
| 116 |
+
|
| 117 |
+
async def get_agent_messages(
|
| 118 |
+
self, session_id: str, limit: int = 50, offset: int = 0
|
| 119 |
+
) -> Dict[str, Any]:
|
| 120 |
+
"""Get agent session messages"""
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
response = await self.client.get(
|
| 124 |
+
f"do/agent/{session_id}/messages?sessionId={session_id}&limit={limit}&offset={offset}",
|
| 125 |
+
use_worker=True,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
return response
|
| 129 |
+
|
| 130 |
+
except CloudflareError as e:
|
| 131 |
+
logger.error(f"Failed to get agent messages: {e}")
|
| 132 |
+
raise
|
| 133 |
+
|
| 134 |
+
# Chat Room methods
|
| 135 |
+
async def join_chat_room(
|
| 136 |
+
self,
|
| 137 |
+
room_id: str,
|
| 138 |
+
user_id: str,
|
| 139 |
+
username: str,
|
| 140 |
+
room_config: Optional[Dict[str, Any]] = None,
|
| 141 |
+
) -> Dict[str, Any]:
|
| 142 |
+
"""Join a chat room"""
|
| 143 |
+
|
| 144 |
+
join_data = {
|
| 145 |
+
"userId": user_id,
|
| 146 |
+
"username": username,
|
| 147 |
+
"roomConfig": room_config or {},
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
response = await self.client.post(
|
| 152 |
+
f"do/chat/{room_id}/join", data=join_data, use_worker=True
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
return {"success": True, "room_id": room_id, "user_id": user_id, **response}
|
| 156 |
+
|
| 157 |
+
except CloudflareError as e:
|
| 158 |
+
logger.error(f"Failed to join chat room: {e}")
|
| 159 |
+
raise
|
| 160 |
+
|
| 161 |
+
async def leave_chat_room(self, room_id: str, user_id: str) -> Dict[str, Any]:
|
| 162 |
+
"""Leave a chat room"""
|
| 163 |
+
|
| 164 |
+
leave_data = {"userId": user_id}
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
response = await self.client.post(
|
| 168 |
+
f"do/chat/{room_id}/leave", data=leave_data, use_worker=True
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
return {"success": True, "room_id": room_id, "user_id": user_id, **response}
|
| 172 |
+
|
| 173 |
+
except CloudflareError as e:
|
| 174 |
+
logger.error(f"Failed to leave chat room: {e}")
|
| 175 |
+
raise
|
| 176 |
+
|
| 177 |
+
async def get_chat_room_info(self, room_id: str) -> Dict[str, Any]:
|
| 178 |
+
"""Get chat room information"""
|
| 179 |
+
|
| 180 |
+
try:
|
| 181 |
+
response = await self.client.get(f"do/chat/{room_id}/info", use_worker=True)
|
| 182 |
+
|
| 183 |
+
return response
|
| 184 |
+
|
| 185 |
+
except CloudflareError as e:
|
| 186 |
+
logger.error(f"Failed to get chat room info: {e}")
|
| 187 |
+
raise
|
| 188 |
+
|
| 189 |
+
async def send_chat_message(
|
| 190 |
+
self,
|
| 191 |
+
room_id: str,
|
| 192 |
+
user_id: str,
|
| 193 |
+
username: str,
|
| 194 |
+
content: str,
|
| 195 |
+
message_type: str = "text",
|
| 196 |
+
) -> Dict[str, Any]:
|
| 197 |
+
"""Send a message to chat room"""
|
| 198 |
+
|
| 199 |
+
message_data = {
|
| 200 |
+
"userId": user_id,
|
| 201 |
+
"username": username,
|
| 202 |
+
"content": content,
|
| 203 |
+
"messageType": message_type,
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
try:
|
| 207 |
+
response = await self.client.post(
|
| 208 |
+
f"do/chat/{room_id}/messages", data=message_data, use_worker=True
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
return {"success": True, "room_id": room_id, **response}
|
| 212 |
+
|
| 213 |
+
except CloudflareError as e:
|
| 214 |
+
logger.error(f"Failed to send chat message: {e}")
|
| 215 |
+
raise
|
| 216 |
+
|
| 217 |
+
async def get_chat_messages(
|
| 218 |
+
self, room_id: str, limit: int = 50, offset: int = 0
|
| 219 |
+
) -> Dict[str, Any]:
|
| 220 |
+
"""Get chat room messages"""
|
| 221 |
+
|
| 222 |
+
try:
|
| 223 |
+
response = await self.client.get(
|
| 224 |
+
f"do/chat/{room_id}/messages?limit={limit}&offset={offset}",
|
| 225 |
+
use_worker=True,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
return response
|
| 229 |
+
|
| 230 |
+
except CloudflareError as e:
|
| 231 |
+
logger.error(f"Failed to get chat messages: {e}")
|
| 232 |
+
raise
|
| 233 |
+
|
| 234 |
+
async def get_chat_participants(self, room_id: str) -> Dict[str, Any]:
|
| 235 |
+
"""Get chat room participants"""
|
| 236 |
+
|
| 237 |
+
try:
|
| 238 |
+
response = await self.client.get(
|
| 239 |
+
f"do/chat/{room_id}/participants", use_worker=True
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
return response
|
| 243 |
+
|
| 244 |
+
except CloudflareError as e:
|
| 245 |
+
logger.error(f"Failed to get chat participants: {e}")
|
| 246 |
+
raise
|
| 247 |
+
|
| 248 |
+
# WebSocket connection helpers
|
| 249 |
+
def get_agent_websocket_url(self, session_id: str, user_id: str) -> str:
|
| 250 |
+
"""Get WebSocket URL for agent session"""
|
| 251 |
+
|
| 252 |
+
if not self.client.worker_url:
|
| 253 |
+
raise CloudflareError("Worker URL not configured")
|
| 254 |
+
|
| 255 |
+
base_url = self.client.worker_url.replace("https://", "wss://").replace(
|
| 256 |
+
"http://", "ws://"
|
| 257 |
+
)
|
| 258 |
+
return (
|
| 259 |
+
f"{base_url}/do/agent/{session_id}?sessionId={session_id}&userId={user_id}"
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
def get_chat_websocket_url(self, room_id: str, user_id: str, username: str) -> str:
|
| 263 |
+
"""Get WebSocket URL for chat room"""
|
| 264 |
+
|
| 265 |
+
if not self.client.worker_url:
|
| 266 |
+
raise CloudflareError("Worker URL not configured")
|
| 267 |
+
|
| 268 |
+
base_url = self.client.worker_url.replace("https://", "wss://").replace(
|
| 269 |
+
"http://", "ws://"
|
| 270 |
+
)
|
| 271 |
+
return f"{base_url}/do/chat/{room_id}?userId={user_id}&username={username}"
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
class DurableObjectsWebSocket:
|
| 275 |
+
"""Helper class for WebSocket connections to Durable Objects"""
|
| 276 |
+
|
| 277 |
+
def __init__(self, url: str):
|
| 278 |
+
self.url = url
|
| 279 |
+
self.websocket = None
|
| 280 |
+
self.connected = False
|
| 281 |
+
self.message_handlers = {}
|
| 282 |
+
|
| 283 |
+
async def connect(self):
|
| 284 |
+
"""Connect to WebSocket"""
|
| 285 |
+
try:
|
| 286 |
+
import websockets
|
| 287 |
+
|
| 288 |
+
self.websocket = await websockets.connect(self.url)
|
| 289 |
+
self.connected = True
|
| 290 |
+
logger.info(f"Connected to Durable Object WebSocket: {self.url}")
|
| 291 |
+
|
| 292 |
+
# Start message handling loop
|
| 293 |
+
import asyncio
|
| 294 |
+
|
| 295 |
+
asyncio.create_task(self._message_loop())
|
| 296 |
+
|
| 297 |
+
except Exception as e:
|
| 298 |
+
logger.error(f"Failed to connect to WebSocket: {e}")
|
| 299 |
+
raise CloudflareError(f"WebSocket connection failed: {e}")
|
| 300 |
+
|
| 301 |
+
async def disconnect(self):
|
| 302 |
+
"""Disconnect from WebSocket"""
|
| 303 |
+
if self.websocket and self.connected:
|
| 304 |
+
await self.websocket.close()
|
| 305 |
+
self.connected = False
|
| 306 |
+
logger.info("Disconnected from Durable Object WebSocket")
|
| 307 |
+
|
| 308 |
+
async def send_message(self, message_type: str, payload: Dict[str, Any]):
|
| 309 |
+
"""Send message via WebSocket"""
|
| 310 |
+
if not self.connected or not self.websocket:
|
| 311 |
+
raise CloudflareError("WebSocket not connected")
|
| 312 |
+
|
| 313 |
+
message = {
|
| 314 |
+
"type": message_type,
|
| 315 |
+
"payload": payload,
|
| 316 |
+
"timestamp": int(time.time()),
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
try:
|
| 320 |
+
await self.websocket.send(json.dumps(message))
|
| 321 |
+
except Exception as e:
|
| 322 |
+
logger.error(f"Failed to send WebSocket message: {e}")
|
| 323 |
+
raise CloudflareError(f"Failed to send message: {e}")
|
| 324 |
+
|
| 325 |
+
def add_message_handler(self, message_type: str, handler):
|
| 326 |
+
"""Add a message handler for specific message types"""
|
| 327 |
+
if message_type not in self.message_handlers:
|
| 328 |
+
self.message_handlers[message_type] = []
|
| 329 |
+
self.message_handlers[message_type].append(handler)
|
| 330 |
+
|
| 331 |
+
async def _message_loop(self):
|
| 332 |
+
"""Handle incoming WebSocket messages"""
|
| 333 |
+
try:
|
| 334 |
+
async for message in self.websocket:
|
| 335 |
+
try:
|
| 336 |
+
data = json.loads(message)
|
| 337 |
+
message_type = data.get("type")
|
| 338 |
+
|
| 339 |
+
if message_type in self.message_handlers:
|
| 340 |
+
for handler in self.message_handlers[message_type]:
|
| 341 |
+
try:
|
| 342 |
+
if callable(handler):
|
| 343 |
+
if asyncio.iscoroutinefunction(handler):
|
| 344 |
+
await handler(data)
|
| 345 |
+
else:
|
| 346 |
+
handler(data)
|
| 347 |
+
except Exception as e:
|
| 348 |
+
logger.error(f"Message handler error: {e}")
|
| 349 |
+
|
| 350 |
+
except json.JSONDecodeError as e:
|
| 351 |
+
logger.error(f"Failed to parse WebSocket message: {e}")
|
| 352 |
+
except Exception as e:
|
| 353 |
+
logger.error(f"WebSocket message processing error: {e}")
|
| 354 |
+
|
| 355 |
+
except Exception as e:
|
| 356 |
+
logger.error(f"WebSocket message loop error: {e}")
|
| 357 |
+
self.connected = False
|
| 358 |
+
|
| 359 |
+
# Context manager support
|
| 360 |
+
async def __aenter__(self):
|
| 361 |
+
await self.connect()
|
| 362 |
+
return self
|
| 363 |
+
|
| 364 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 365 |
+
await self.disconnect()
|
app/cloudflare/kv.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KV Storage integration for OpenManus
|
| 3 |
+
Provides interface to Cloudflare KV operations
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from typing import Any, Dict, List, Optional
|
| 8 |
+
|
| 9 |
+
from app.logger import logger
|
| 10 |
+
|
| 11 |
+
from .client import CloudflareClient, CloudflareError
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class KVStorage:
|
| 15 |
+
"""Cloudflare KV Storage client"""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
client: CloudflareClient,
|
| 20 |
+
sessions_namespace_id: str,
|
| 21 |
+
cache_namespace_id: str,
|
| 22 |
+
):
|
| 23 |
+
self.client = client
|
| 24 |
+
self.sessions_namespace_id = sessions_namespace_id
|
| 25 |
+
self.cache_namespace_id = cache_namespace_id
|
| 26 |
+
self.base_endpoint = f"accounts/{client.account_id}/storage/kv/namespaces"
|
| 27 |
+
|
| 28 |
+
def _get_namespace_id(self, namespace_type: str) -> str:
|
| 29 |
+
"""Get namespace ID based on type"""
|
| 30 |
+
if namespace_type == "cache":
|
| 31 |
+
return self.cache_namespace_id
|
| 32 |
+
return self.sessions_namespace_id
|
| 33 |
+
|
| 34 |
+
async def set_value(
|
| 35 |
+
self,
|
| 36 |
+
key: str,
|
| 37 |
+
value: Any,
|
| 38 |
+
namespace_type: str = "sessions",
|
| 39 |
+
ttl: Optional[int] = None,
|
| 40 |
+
use_worker: bool = True,
|
| 41 |
+
) -> Dict[str, Any]:
|
| 42 |
+
"""Set a value in KV store"""
|
| 43 |
+
|
| 44 |
+
namespace_id = self._get_namespace_id(namespace_type)
|
| 45 |
+
|
| 46 |
+
# Serialize value to JSON
|
| 47 |
+
if isinstance(value, (dict, list)):
|
| 48 |
+
serialized_value = json.dumps(value)
|
| 49 |
+
elif isinstance(value, str):
|
| 50 |
+
serialized_value = value
|
| 51 |
+
else:
|
| 52 |
+
serialized_value = json.dumps(value)
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
if use_worker:
|
| 56 |
+
set_data = {
|
| 57 |
+
"key": key,
|
| 58 |
+
"value": serialized_value,
|
| 59 |
+
"namespace": namespace_type,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
if ttl:
|
| 63 |
+
set_data["ttl"] = ttl
|
| 64 |
+
|
| 65 |
+
response = await self.client.post(
|
| 66 |
+
f"api/kv/set", data=set_data, use_worker=True
|
| 67 |
+
)
|
| 68 |
+
else:
|
| 69 |
+
# Use KV API directly
|
| 70 |
+
params = {}
|
| 71 |
+
if ttl:
|
| 72 |
+
params["expiration_ttl"] = ttl
|
| 73 |
+
|
| 74 |
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
| 75 |
+
endpoint = f"{self.base_endpoint}/{namespace_id}/values/{key}"
|
| 76 |
+
if query_string:
|
| 77 |
+
endpoint += f"?{query_string}"
|
| 78 |
+
|
| 79 |
+
response = await self.client.put(
|
| 80 |
+
endpoint, data={"value": serialized_value}
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
return {
|
| 84 |
+
"success": True,
|
| 85 |
+
"key": key,
|
| 86 |
+
"namespace": namespace_type,
|
| 87 |
+
"ttl": ttl,
|
| 88 |
+
**response,
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
except CloudflareError as e:
|
| 92 |
+
logger.error(f"KV set value failed: {e}")
|
| 93 |
+
raise
|
| 94 |
+
|
| 95 |
+
async def get_value(
|
| 96 |
+
self,
|
| 97 |
+
key: str,
|
| 98 |
+
namespace_type: str = "sessions",
|
| 99 |
+
parse_json: bool = True,
|
| 100 |
+
use_worker: bool = True,
|
| 101 |
+
) -> Optional[Any]:
|
| 102 |
+
"""Get a value from KV store"""
|
| 103 |
+
|
| 104 |
+
namespace_id = self._get_namespace_id(namespace_type)
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
if use_worker:
|
| 108 |
+
response = await self.client.get(
|
| 109 |
+
f"api/kv/get/{key}?namespace={namespace_type}", use_worker=True
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if response and "value" in response:
|
| 113 |
+
value = response["value"]
|
| 114 |
+
|
| 115 |
+
if parse_json and isinstance(value, str):
|
| 116 |
+
try:
|
| 117 |
+
return json.loads(value)
|
| 118 |
+
except json.JSONDecodeError:
|
| 119 |
+
return value
|
| 120 |
+
|
| 121 |
+
return value
|
| 122 |
+
else:
|
| 123 |
+
response = await self.client.get(
|
| 124 |
+
f"{self.base_endpoint}/{namespace_id}/values/{key}"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# KV API returns the value directly as text
|
| 128 |
+
value = (
|
| 129 |
+
response.get("result", {}).get("value")
|
| 130 |
+
if "result" in response
|
| 131 |
+
else response
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
if value and parse_json and isinstance(value, str):
|
| 135 |
+
try:
|
| 136 |
+
return json.loads(value)
|
| 137 |
+
except json.JSONDecodeError:
|
| 138 |
+
return value
|
| 139 |
+
|
| 140 |
+
return value
|
| 141 |
+
|
| 142 |
+
except CloudflareError as e:
|
| 143 |
+
if e.status_code == 404:
|
| 144 |
+
return None
|
| 145 |
+
logger.error(f"KV get value failed: {e}")
|
| 146 |
+
raise
|
| 147 |
+
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
async def delete_value(
|
| 151 |
+
self, key: str, namespace_type: str = "sessions", use_worker: bool = True
|
| 152 |
+
) -> Dict[str, Any]:
|
| 153 |
+
"""Delete a value from KV store"""
|
| 154 |
+
|
| 155 |
+
namespace_id = self._get_namespace_id(namespace_type)
|
| 156 |
+
|
| 157 |
+
try:
|
| 158 |
+
if use_worker:
|
| 159 |
+
response = await self.client.delete(
|
| 160 |
+
f"api/kv/delete/{key}?namespace={namespace_type}", use_worker=True
|
| 161 |
+
)
|
| 162 |
+
else:
|
| 163 |
+
response = await self.client.delete(
|
| 164 |
+
f"{self.base_endpoint}/{namespace_id}/values/{key}"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
return {
|
| 168 |
+
"success": True,
|
| 169 |
+
"key": key,
|
| 170 |
+
"namespace": namespace_type,
|
| 171 |
+
**response,
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
except CloudflareError as e:
|
| 175 |
+
logger.error(f"KV delete value failed: {e}")
|
| 176 |
+
raise
|
| 177 |
+
|
| 178 |
+
async def list_keys(
|
| 179 |
+
self,
|
| 180 |
+
namespace_type: str = "sessions",
|
| 181 |
+
prefix: str = "",
|
| 182 |
+
limit: int = 1000,
|
| 183 |
+
use_worker: bool = True,
|
| 184 |
+
) -> Dict[str, Any]:
|
| 185 |
+
"""List keys in KV namespace"""
|
| 186 |
+
|
| 187 |
+
namespace_id = self._get_namespace_id(namespace_type)
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
if use_worker:
|
| 191 |
+
params = {"namespace": namespace_type, "prefix": prefix, "limit": limit}
|
| 192 |
+
|
| 193 |
+
query_string = "&".join([f"{k}={v}" for k, v in params.items() if v])
|
| 194 |
+
response = await self.client.get(
|
| 195 |
+
f"api/kv/list?{query_string}", use_worker=True
|
| 196 |
+
)
|
| 197 |
+
else:
|
| 198 |
+
params = {"prefix": prefix, "limit": limit}
|
| 199 |
+
|
| 200 |
+
query_string = "&".join([f"{k}={v}" for k, v in params.items() if v])
|
| 201 |
+
response = await self.client.get(
|
| 202 |
+
f"{self.base_endpoint}/{namespace_id}/keys?{query_string}"
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
return {
|
| 206 |
+
"namespace": namespace_type,
|
| 207 |
+
"prefix": prefix,
|
| 208 |
+
"keys": (
|
| 209 |
+
response.get("result", [])
|
| 210 |
+
if "result" in response
|
| 211 |
+
else response.get("keys", [])
|
| 212 |
+
),
|
| 213 |
+
**response,
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
except CloudflareError as e:
|
| 217 |
+
logger.error(f"KV list keys failed: {e}")
|
| 218 |
+
raise
|
| 219 |
+
|
| 220 |
+
# Session-specific methods
|
| 221 |
+
async def set_session(
|
| 222 |
+
self,
|
| 223 |
+
session_id: str,
|
| 224 |
+
session_data: Dict[str, Any],
|
| 225 |
+
ttl: int = 86400, # 24 hours default
|
| 226 |
+
) -> Dict[str, Any]:
|
| 227 |
+
"""Set session data"""
|
| 228 |
+
|
| 229 |
+
data = {
|
| 230 |
+
**session_data,
|
| 231 |
+
"created_at": session_data.get("created_at", int(time.time())),
|
| 232 |
+
"expires_at": int(time.time()) + ttl,
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
return await self.set_value(f"session:{session_id}", data, "sessions", ttl)
|
| 236 |
+
|
| 237 |
+
async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 238 |
+
"""Get session data"""
|
| 239 |
+
|
| 240 |
+
session = await self.get_value(f"session:{session_id}", "sessions")
|
| 241 |
+
|
| 242 |
+
if session and isinstance(session, dict):
|
| 243 |
+
# Check if session is expired
|
| 244 |
+
expires_at = session.get("expires_at")
|
| 245 |
+
if expires_at and int(time.time()) > expires_at:
|
| 246 |
+
await self.delete_session(session_id)
|
| 247 |
+
return None
|
| 248 |
+
|
| 249 |
+
return session
|
| 250 |
+
|
| 251 |
+
async def delete_session(self, session_id: str) -> Dict[str, Any]:
|
| 252 |
+
"""Delete session data"""
|
| 253 |
+
|
| 254 |
+
return await self.delete_value(f"session:{session_id}", "sessions")
|
| 255 |
+
|
| 256 |
+
async def update_session(
|
| 257 |
+
self, session_id: str, updates: Dict[str, Any], extend_ttl: Optional[int] = None
|
| 258 |
+
) -> Dict[str, Any]:
|
| 259 |
+
"""Update session data"""
|
| 260 |
+
|
| 261 |
+
existing_session = await self.get_session(session_id)
|
| 262 |
+
|
| 263 |
+
if not existing_session:
|
| 264 |
+
raise CloudflareError("Session not found")
|
| 265 |
+
|
| 266 |
+
updated_data = {**existing_session, **updates, "updated_at": int(time.time())}
|
| 267 |
+
|
| 268 |
+
# Calculate TTL
|
| 269 |
+
ttl = None
|
| 270 |
+
if extend_ttl:
|
| 271 |
+
ttl = extend_ttl
|
| 272 |
+
elif existing_session.get("expires_at"):
|
| 273 |
+
ttl = max(0, existing_session["expires_at"] - int(time.time()))
|
| 274 |
+
|
| 275 |
+
return await self.set_session(session_id, updated_data, ttl or 86400)
|
| 276 |
+
|
| 277 |
+
# Cache-specific methods
|
| 278 |
+
async def set_cache(
|
| 279 |
+
self, key: str, data: Any, ttl: int = 3600 # 1 hour default
|
| 280 |
+
) -> Dict[str, Any]:
|
| 281 |
+
"""Set cache data"""
|
| 282 |
+
|
| 283 |
+
cache_data = {
|
| 284 |
+
"data": data,
|
| 285 |
+
"cached_at": int(time.time()),
|
| 286 |
+
"expires_at": int(time.time()) + ttl,
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
return await self.set_value(f"cache:{key}", cache_data, "cache", ttl)
|
| 290 |
+
|
| 291 |
+
async def get_cache(self, key: str) -> Optional[Any]:
|
| 292 |
+
"""Get cache data"""
|
| 293 |
+
|
| 294 |
+
cached = await self.get_value(f"cache:{key}", "cache")
|
| 295 |
+
|
| 296 |
+
if cached and isinstance(cached, dict):
|
| 297 |
+
# Check if cache is expired
|
| 298 |
+
expires_at = cached.get("expires_at")
|
| 299 |
+
if expires_at and int(time.time()) > expires_at:
|
| 300 |
+
await self.delete_cache(key)
|
| 301 |
+
return None
|
| 302 |
+
|
| 303 |
+
return cached.get("data")
|
| 304 |
+
|
| 305 |
+
return cached
|
| 306 |
+
|
| 307 |
+
async def delete_cache(self, key: str) -> Dict[str, Any]:
|
| 308 |
+
"""Delete cache data"""
|
| 309 |
+
|
| 310 |
+
return await self.delete_value(f"cache:{key}", "cache")
|
| 311 |
+
|
| 312 |
+
# User-specific methods
|
| 313 |
+
async def set_user_cache(
|
| 314 |
+
self, user_id: str, key: str, data: Any, ttl: int = 3600
|
| 315 |
+
) -> Dict[str, Any]:
|
| 316 |
+
"""Set user-specific cache"""
|
| 317 |
+
|
| 318 |
+
user_key = f"user:{user_id}:{key}"
|
| 319 |
+
return await self.set_cache(user_key, data, ttl)
|
| 320 |
+
|
| 321 |
+
async def get_user_cache(self, user_id: str, key: str) -> Optional[Any]:
|
| 322 |
+
"""Get user-specific cache"""
|
| 323 |
+
|
| 324 |
+
user_key = f"user:{user_id}:{key}"
|
| 325 |
+
return await self.get_cache(user_key)
|
| 326 |
+
|
| 327 |
+
async def delete_user_cache(self, user_id: str, key: str) -> Dict[str, Any]:
|
| 328 |
+
"""Delete user-specific cache"""
|
| 329 |
+
|
| 330 |
+
user_key = f"user:{user_id}:{key}"
|
| 331 |
+
return await self.delete_cache(user_key)
|
| 332 |
+
|
| 333 |
+
async def get_user_cache_keys(self, user_id: str, limit: int = 100) -> List[str]:
|
| 334 |
+
"""Get all cache keys for a user"""
|
| 335 |
+
|
| 336 |
+
result = await self.list_keys("cache", f"cache:user:{user_id}:", limit)
|
| 337 |
+
|
| 338 |
+
keys = []
|
| 339 |
+
for key_info in result.get("keys", []):
|
| 340 |
+
if isinstance(key_info, dict):
|
| 341 |
+
key = key_info.get("name", "")
|
| 342 |
+
else:
|
| 343 |
+
key = str(key_info)
|
| 344 |
+
|
| 345 |
+
# Remove prefix to get the actual key
|
| 346 |
+
if key.startswith(f"cache:user:{user_id}:"):
|
| 347 |
+
clean_key = key.replace(f"cache:user:{user_id}:", "")
|
| 348 |
+
keys.append(clean_key)
|
| 349 |
+
|
| 350 |
+
return keys
|
| 351 |
+
|
| 352 |
+
# Conversation caching
|
| 353 |
+
async def cache_conversation(
|
| 354 |
+
self,
|
| 355 |
+
conversation_id: str,
|
| 356 |
+
messages: List[Dict[str, Any]],
|
| 357 |
+
ttl: int = 7200, # 2 hours default
|
| 358 |
+
) -> Dict[str, Any]:
|
| 359 |
+
"""Cache conversation messages"""
|
| 360 |
+
|
| 361 |
+
return await self.set_cache(
|
| 362 |
+
f"conversation:{conversation_id}",
|
| 363 |
+
{"messages": messages, "last_updated": int(time.time())},
|
| 364 |
+
ttl,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
async def get_cached_conversation(
|
| 368 |
+
self, conversation_id: str
|
| 369 |
+
) -> Optional[Dict[str, Any]]:
|
| 370 |
+
"""Get cached conversation"""
|
| 371 |
+
|
| 372 |
+
return await self.get_cache(f"conversation:{conversation_id}")
|
| 373 |
+
|
| 374 |
+
# Agent execution caching
|
| 375 |
+
async def cache_agent_execution(
|
| 376 |
+
self, execution_id: str, execution_data: Dict[str, Any], ttl: int = 3600
|
| 377 |
+
) -> Dict[str, Any]:
|
| 378 |
+
"""Cache agent execution data"""
|
| 379 |
+
|
| 380 |
+
return await self.set_cache(f"execution:{execution_id}", execution_data, ttl)
|
| 381 |
+
|
| 382 |
+
async def get_cached_agent_execution(
|
| 383 |
+
self, execution_id: str
|
| 384 |
+
) -> Optional[Dict[str, Any]]:
|
| 385 |
+
"""Get cached agent execution"""
|
| 386 |
+
|
| 387 |
+
return await self.get_cache(f"execution:{execution_id}")
|
| 388 |
+
|
| 389 |
+
# Batch operations
|
| 390 |
+
async def set_batch(
|
| 391 |
+
self,
|
| 392 |
+
items: List[Dict[str, Any]],
|
| 393 |
+
namespace_type: str = "cache",
|
| 394 |
+
ttl: Optional[int] = None,
|
| 395 |
+
) -> Dict[str, Any]:
|
| 396 |
+
"""Set multiple values (simulated batch operation)"""
|
| 397 |
+
|
| 398 |
+
results = []
|
| 399 |
+
successful = 0
|
| 400 |
+
failed = 0
|
| 401 |
+
|
| 402 |
+
for item in items:
|
| 403 |
+
try:
|
| 404 |
+
key = item["key"]
|
| 405 |
+
value = item["value"]
|
| 406 |
+
item_ttl = item.get("ttl", ttl)
|
| 407 |
+
|
| 408 |
+
result = await self.set_value(key, value, namespace_type, item_ttl)
|
| 409 |
+
results.append({"key": key, "success": True, "result": result})
|
| 410 |
+
successful += 1
|
| 411 |
+
|
| 412 |
+
except Exception as e:
|
| 413 |
+
results.append(
|
| 414 |
+
{"key": item.get("key"), "success": False, "error": str(e)}
|
| 415 |
+
)
|
| 416 |
+
failed += 1
|
| 417 |
+
|
| 418 |
+
return {
|
| 419 |
+
"success": failed == 0,
|
| 420 |
+
"successful": successful,
|
| 421 |
+
"failed": failed,
|
| 422 |
+
"total": len(items),
|
| 423 |
+
"results": results,
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
async def get_batch(
|
| 427 |
+
self, keys: List[str], namespace_type: str = "cache"
|
| 428 |
+
) -> Dict[str, Any]:
|
| 429 |
+
"""Get multiple values (simulated batch operation)"""
|
| 430 |
+
|
| 431 |
+
results = {}
|
| 432 |
+
|
| 433 |
+
for key in keys:
|
| 434 |
+
try:
|
| 435 |
+
value = await self.get_value(key, namespace_type)
|
| 436 |
+
results[key] = value
|
| 437 |
+
except Exception as e:
|
| 438 |
+
logger.error(f"Failed to get key {key}: {e}")
|
| 439 |
+
results[key] = None
|
| 440 |
+
|
| 441 |
+
return results
|
| 442 |
+
|
| 443 |
+
def _hash_params(self, params: Dict[str, Any]) -> str:
|
| 444 |
+
"""Create a hash for cache keys from parameters"""
|
| 445 |
+
|
| 446 |
+
if not params:
|
| 447 |
+
return "no-params"
|
| 448 |
+
|
| 449 |
+
# Simple hash function for cache keys
|
| 450 |
+
import hashlib
|
| 451 |
+
|
| 452 |
+
params_str = json.dumps(params, sort_keys=True)
|
| 453 |
+
return hashlib.md5(params_str.encode()).hexdigest()[:16]
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
# Add time import at the top
|
| 457 |
+
import time
|
app/cloudflare/r2.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
R2 Storage integration for OpenManus
|
| 3 |
+
Provides interface to Cloudflare R2 storage operations
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import io
|
| 7 |
+
from typing import Any, BinaryIO, Dict, List, Optional
|
| 8 |
+
|
| 9 |
+
from app.logger import logger
|
| 10 |
+
|
| 11 |
+
from .client import CloudflareClient, CloudflareError
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class R2Storage:
|
| 15 |
+
"""Cloudflare R2 Storage client"""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
client: CloudflareClient,
|
| 20 |
+
storage_bucket: str,
|
| 21 |
+
assets_bucket: Optional[str] = None,
|
| 22 |
+
):
|
| 23 |
+
self.client = client
|
| 24 |
+
self.storage_bucket = storage_bucket
|
| 25 |
+
self.assets_bucket = assets_bucket or storage_bucket
|
| 26 |
+
self.base_endpoint = f"accounts/{client.account_id}/r2/buckets"
|
| 27 |
+
|
| 28 |
+
def _get_bucket_name(self, bucket_type: str = "storage") -> str:
|
| 29 |
+
"""Get bucket name based on type"""
|
| 30 |
+
if bucket_type == "assets":
|
| 31 |
+
return self.assets_bucket
|
| 32 |
+
return self.storage_bucket
|
| 33 |
+
|
| 34 |
+
async def upload_file(
|
| 35 |
+
self,
|
| 36 |
+
key: str,
|
| 37 |
+
file_data: bytes,
|
| 38 |
+
content_type: str = "application/octet-stream",
|
| 39 |
+
bucket_type: str = "storage",
|
| 40 |
+
metadata: Optional[Dict[str, str]] = None,
|
| 41 |
+
use_worker: bool = True,
|
| 42 |
+
) -> Dict[str, Any]:
|
| 43 |
+
"""Upload a file to R2"""
|
| 44 |
+
|
| 45 |
+
bucket_name = self._get_bucket_name(bucket_type)
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
if use_worker:
|
| 49 |
+
# Use worker endpoint for better performance
|
| 50 |
+
form_data = {
|
| 51 |
+
"file": file_data,
|
| 52 |
+
"bucket": bucket_type,
|
| 53 |
+
"key": key,
|
| 54 |
+
"contentType": content_type,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if metadata:
|
| 58 |
+
form_data["metadata"] = metadata
|
| 59 |
+
|
| 60 |
+
response = await self.client.post(
|
| 61 |
+
"api/files", data=form_data, use_worker=True
|
| 62 |
+
)
|
| 63 |
+
else:
|
| 64 |
+
# Use R2 API directly
|
| 65 |
+
headers = {"Content-Type": content_type}
|
| 66 |
+
|
| 67 |
+
if metadata:
|
| 68 |
+
for k, v in metadata.items():
|
| 69 |
+
headers[f"x-amz-meta-{k}"] = v
|
| 70 |
+
|
| 71 |
+
response = await self.client.upload_file(
|
| 72 |
+
f"{self.base_endpoint}/{bucket_name}/objects/{key}",
|
| 73 |
+
file_data,
|
| 74 |
+
content_type,
|
| 75 |
+
headers,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"success": True,
|
| 80 |
+
"key": key,
|
| 81 |
+
"bucket": bucket_type,
|
| 82 |
+
"bucket_name": bucket_name,
|
| 83 |
+
"size": len(file_data),
|
| 84 |
+
"content_type": content_type,
|
| 85 |
+
"url": f"/{bucket_type}/{key}",
|
| 86 |
+
**response,
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
except CloudflareError as e:
|
| 90 |
+
logger.error(f"R2 upload failed: {e}")
|
| 91 |
+
raise
|
| 92 |
+
|
| 93 |
+
async def upload_file_stream(
|
| 94 |
+
self,
|
| 95 |
+
key: str,
|
| 96 |
+
file_stream: BinaryIO,
|
| 97 |
+
content_type: str = "application/octet-stream",
|
| 98 |
+
bucket_type: str = "storage",
|
| 99 |
+
metadata: Optional[Dict[str, str]] = None,
|
| 100 |
+
) -> Dict[str, Any]:
|
| 101 |
+
"""Upload a file from stream"""
|
| 102 |
+
|
| 103 |
+
file_data = file_stream.read()
|
| 104 |
+
return await self.upload_file(
|
| 105 |
+
key, file_data, content_type, bucket_type, metadata
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
async def get_file(
|
| 109 |
+
self, key: str, bucket_type: str = "storage", use_worker: bool = True
|
| 110 |
+
) -> Optional[Dict[str, Any]]:
|
| 111 |
+
"""Get a file from R2"""
|
| 112 |
+
|
| 113 |
+
bucket_name = self._get_bucket_name(bucket_type)
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
if use_worker:
|
| 117 |
+
response = await self.client.get(
|
| 118 |
+
f"api/files/{key}?bucket={bucket_type}", use_worker=True
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
if response:
|
| 122 |
+
return {
|
| 123 |
+
"key": key,
|
| 124 |
+
"bucket": bucket_type,
|
| 125 |
+
"bucket_name": bucket_name,
|
| 126 |
+
"data": response, # Binary data would be handled by worker
|
| 127 |
+
"exists": True,
|
| 128 |
+
}
|
| 129 |
+
else:
|
| 130 |
+
response = await self.client.get(
|
| 131 |
+
f"{self.base_endpoint}/{bucket_name}/objects/{key}"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
return {
|
| 135 |
+
"key": key,
|
| 136 |
+
"bucket": bucket_type,
|
| 137 |
+
"bucket_name": bucket_name,
|
| 138 |
+
"data": response,
|
| 139 |
+
"exists": True,
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
except CloudflareError as e:
|
| 143 |
+
if e.status_code == 404:
|
| 144 |
+
return None
|
| 145 |
+
logger.error(f"R2 get file failed: {e}")
|
| 146 |
+
raise
|
| 147 |
+
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
async def delete_file(
|
| 151 |
+
self, key: str, bucket_type: str = "storage", use_worker: bool = True
|
| 152 |
+
) -> Dict[str, Any]:
|
| 153 |
+
"""Delete a file from R2"""
|
| 154 |
+
|
| 155 |
+
bucket_name = self._get_bucket_name(bucket_type)
|
| 156 |
+
|
| 157 |
+
try:
|
| 158 |
+
if use_worker:
|
| 159 |
+
response = await self.client.delete(
|
| 160 |
+
f"api/files/{key}?bucket={bucket_type}", use_worker=True
|
| 161 |
+
)
|
| 162 |
+
else:
|
| 163 |
+
response = await self.client.delete(
|
| 164 |
+
f"{self.base_endpoint}/{bucket_name}/objects/{key}"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
return {
|
| 168 |
+
"success": True,
|
| 169 |
+
"key": key,
|
| 170 |
+
"bucket": bucket_type,
|
| 171 |
+
"bucket_name": bucket_name,
|
| 172 |
+
**response,
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
except CloudflareError as e:
|
| 176 |
+
logger.error(f"R2 delete failed: {e}")
|
| 177 |
+
raise
|
| 178 |
+
|
| 179 |
+
async def list_files(
|
| 180 |
+
self,
|
| 181 |
+
bucket_type: str = "storage",
|
| 182 |
+
prefix: str = "",
|
| 183 |
+
limit: int = 1000,
|
| 184 |
+
use_worker: bool = True,
|
| 185 |
+
) -> Dict[str, Any]:
|
| 186 |
+
"""List files in R2 bucket"""
|
| 187 |
+
|
| 188 |
+
bucket_name = self._get_bucket_name(bucket_type)
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
if use_worker:
|
| 192 |
+
params = {"bucket": bucket_type, "prefix": prefix, "limit": limit}
|
| 193 |
+
|
| 194 |
+
query_string = "&".join([f"{k}={v}" for k, v in params.items() if v])
|
| 195 |
+
response = await self.client.get(
|
| 196 |
+
f"api/files/list?{query_string}", use_worker=True
|
| 197 |
+
)
|
| 198 |
+
else:
|
| 199 |
+
params = {"prefix": prefix, "max-keys": limit}
|
| 200 |
+
|
| 201 |
+
query_string = "&".join([f"{k}={v}" for k, v in params.items() if v])
|
| 202 |
+
response = await self.client.get(
|
| 203 |
+
f"{self.base_endpoint}/{bucket_name}/objects?{query_string}"
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
return {
|
| 207 |
+
"bucket": bucket_type,
|
| 208 |
+
"bucket_name": bucket_name,
|
| 209 |
+
"prefix": prefix,
|
| 210 |
+
"files": response.get("objects", []),
|
| 211 |
+
"truncated": response.get("truncated", False),
|
| 212 |
+
**response,
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
except CloudflareError as e:
|
| 216 |
+
logger.error(f"R2 list files failed: {e}")
|
| 217 |
+
raise
|
| 218 |
+
|
| 219 |
+
async def get_file_metadata(
|
| 220 |
+
self, key: str, bucket_type: str = "storage", use_worker: bool = True
|
| 221 |
+
) -> Optional[Dict[str, Any]]:
|
| 222 |
+
"""Get file metadata without downloading content"""
|
| 223 |
+
|
| 224 |
+
bucket_name = self._get_bucket_name(bucket_type)
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
if use_worker:
|
| 228 |
+
response = await self.client.get(
|
| 229 |
+
f"api/files/{key}/metadata?bucket={bucket_type}", use_worker=True
|
| 230 |
+
)
|
| 231 |
+
else:
|
| 232 |
+
# Use HEAD request to get metadata only
|
| 233 |
+
response = await self.client.get(
|
| 234 |
+
f"{self.base_endpoint}/{bucket_name}/objects/{key}",
|
| 235 |
+
headers={"Range": "bytes=0-0"}, # Minimal range to get headers
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
if response:
|
| 239 |
+
return {
|
| 240 |
+
"key": key,
|
| 241 |
+
"bucket": bucket_type,
|
| 242 |
+
"bucket_name": bucket_name,
|
| 243 |
+
**response,
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
except CloudflareError as e:
|
| 247 |
+
if e.status_code == 404:
|
| 248 |
+
return None
|
| 249 |
+
logger.error(f"R2 get metadata failed: {e}")
|
| 250 |
+
raise
|
| 251 |
+
|
| 252 |
+
return None
|
| 253 |
+
|
| 254 |
+
async def copy_file(
|
| 255 |
+
self,
|
| 256 |
+
source_key: str,
|
| 257 |
+
destination_key: str,
|
| 258 |
+
source_bucket: str = "storage",
|
| 259 |
+
destination_bucket: str = "storage",
|
| 260 |
+
use_worker: bool = True,
|
| 261 |
+
) -> Dict[str, Any]:
|
| 262 |
+
"""Copy a file within R2 or between buckets"""
|
| 263 |
+
|
| 264 |
+
try:
|
| 265 |
+
if use_worker:
|
| 266 |
+
copy_data = {
|
| 267 |
+
"sourceKey": source_key,
|
| 268 |
+
"destinationKey": destination_key,
|
| 269 |
+
"sourceBucket": source_bucket,
|
| 270 |
+
"destinationBucket": destination_bucket,
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
response = await self.client.post(
|
| 274 |
+
"api/files/copy", data=copy_data, use_worker=True
|
| 275 |
+
)
|
| 276 |
+
else:
|
| 277 |
+
# Get source file first
|
| 278 |
+
source_file = await self.get_file(source_key, source_bucket, False)
|
| 279 |
+
|
| 280 |
+
if not source_file:
|
| 281 |
+
raise CloudflareError(f"Source file {source_key} not found")
|
| 282 |
+
|
| 283 |
+
# Upload to destination
|
| 284 |
+
response = await self.upload_file(
|
| 285 |
+
destination_key,
|
| 286 |
+
source_file["data"],
|
| 287 |
+
bucket_type=destination_bucket,
|
| 288 |
+
use_worker=False,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
return {
|
| 292 |
+
"success": True,
|
| 293 |
+
"source_key": source_key,
|
| 294 |
+
"destination_key": destination_key,
|
| 295 |
+
"source_bucket": source_bucket,
|
| 296 |
+
"destination_bucket": destination_bucket,
|
| 297 |
+
**response,
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
except CloudflareError as e:
|
| 301 |
+
logger.error(f"R2 copy failed: {e}")
|
| 302 |
+
raise
|
| 303 |
+
|
| 304 |
+
async def move_file(
|
| 305 |
+
self,
|
| 306 |
+
source_key: str,
|
| 307 |
+
destination_key: str,
|
| 308 |
+
source_bucket: str = "storage",
|
| 309 |
+
destination_bucket: str = "storage",
|
| 310 |
+
use_worker: bool = True,
|
| 311 |
+
) -> Dict[str, Any]:
|
| 312 |
+
"""Move a file (copy then delete)"""
|
| 313 |
+
|
| 314 |
+
try:
|
| 315 |
+
# Copy file first
|
| 316 |
+
copy_result = await self.copy_file(
|
| 317 |
+
source_key,
|
| 318 |
+
destination_key,
|
| 319 |
+
source_bucket,
|
| 320 |
+
destination_bucket,
|
| 321 |
+
use_worker,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
# Delete source file
|
| 325 |
+
delete_result = await self.delete_file(
|
| 326 |
+
source_key, source_bucket, use_worker
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
return {
|
| 330 |
+
"success": True,
|
| 331 |
+
"source_key": source_key,
|
| 332 |
+
"destination_key": destination_key,
|
| 333 |
+
"source_bucket": source_bucket,
|
| 334 |
+
"destination_bucket": destination_bucket,
|
| 335 |
+
"copy_result": copy_result,
|
| 336 |
+
"delete_result": delete_result,
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
except CloudflareError as e:
|
| 340 |
+
logger.error(f"R2 move failed: {e}")
|
| 341 |
+
raise
|
| 342 |
+
|
| 343 |
+
async def generate_presigned_url(
|
| 344 |
+
self,
|
| 345 |
+
key: str,
|
| 346 |
+
bucket_type: str = "storage",
|
| 347 |
+
expires_in: int = 3600,
|
| 348 |
+
method: str = "GET",
|
| 349 |
+
) -> Dict[str, Any]:
|
| 350 |
+
"""Generate a presigned URL for direct access"""
|
| 351 |
+
|
| 352 |
+
# Note: This would typically require additional R2 configuration
|
| 353 |
+
# For now, return a worker endpoint URL
|
| 354 |
+
|
| 355 |
+
try:
|
| 356 |
+
url_data = {
|
| 357 |
+
"key": key,
|
| 358 |
+
"bucket": bucket_type,
|
| 359 |
+
"expiresIn": expires_in,
|
| 360 |
+
"method": method,
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
response = await self.client.post(
|
| 364 |
+
"api/files/presigned-url", data=url_data, use_worker=True
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
return {
|
| 368 |
+
"success": True,
|
| 369 |
+
"key": key,
|
| 370 |
+
"bucket": bucket_type,
|
| 371 |
+
"method": method,
|
| 372 |
+
"expires_in": expires_in,
|
| 373 |
+
**response,
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
except CloudflareError as e:
|
| 377 |
+
logger.error(f"R2 presigned URL generation failed: {e}")
|
| 378 |
+
raise
|
| 379 |
+
|
| 380 |
+
async def get_storage_stats(self, use_worker: bool = True) -> Dict[str, Any]:
|
| 381 |
+
"""Get storage statistics"""
|
| 382 |
+
|
| 383 |
+
try:
|
| 384 |
+
if use_worker:
|
| 385 |
+
response = await self.client.get("api/files/stats", use_worker=True)
|
| 386 |
+
else:
|
| 387 |
+
# Get stats for both buckets
|
| 388 |
+
storage_list = await self.list_files("storage", use_worker=False)
|
| 389 |
+
assets_list = await self.list_files("assets", use_worker=False)
|
| 390 |
+
|
| 391 |
+
storage_size = sum(
|
| 392 |
+
file.get("size", 0) for file in storage_list.get("files", [])
|
| 393 |
+
)
|
| 394 |
+
assets_size = sum(
|
| 395 |
+
file.get("size", 0) for file in assets_list.get("files", [])
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
response = {
|
| 399 |
+
"storage": {
|
| 400 |
+
"file_count": len(storage_list.get("files", [])),
|
| 401 |
+
"total_size": storage_size,
|
| 402 |
+
},
|
| 403 |
+
"assets": {
|
| 404 |
+
"file_count": len(assets_list.get("files", [])),
|
| 405 |
+
"total_size": assets_size,
|
| 406 |
+
},
|
| 407 |
+
"total": {
|
| 408 |
+
"file_count": len(storage_list.get("files", []))
|
| 409 |
+
+ len(assets_list.get("files", [])),
|
| 410 |
+
"total_size": storage_size + assets_size,
|
| 411 |
+
},
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
return response
|
| 415 |
+
|
| 416 |
+
except CloudflareError as e:
|
| 417 |
+
logger.error(f"R2 storage stats failed: {e}")
|
| 418 |
+
raise
|
| 419 |
+
|
| 420 |
+
def create_file_stream(self, data: bytes) -> io.BytesIO:
|
| 421 |
+
"""Create a file stream from bytes"""
|
| 422 |
+
return io.BytesIO(data)
|
| 423 |
+
|
| 424 |
+
def get_public_url(self, key: str, bucket_type: str = "storage") -> str:
|
| 425 |
+
"""Get public URL for a file (if bucket is configured for public access)"""
|
| 426 |
+
bucket_name = self._get_bucket_name(bucket_type)
|
| 427 |
+
|
| 428 |
+
# This would depend on your R2 custom domain configuration
|
| 429 |
+
# For now, return the worker endpoint
|
| 430 |
+
if self.client.worker_url:
|
| 431 |
+
return f"{self.client.worker_url}/api/files/{key}?bucket={bucket_type}"
|
| 432 |
+
|
| 433 |
+
# Default R2 URL format (requires public access configuration)
|
| 434 |
+
return f"https://pub-{bucket_name}.r2.dev/{key}"
|
app/config.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import threading
|
| 3 |
+
import tomllib
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def get_project_root() -> Path:
|
| 11 |
+
"""Get the project root directory"""
|
| 12 |
+
return Path(__file__).resolve().parent.parent
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
PROJECT_ROOT = get_project_root()
|
| 16 |
+
WORKSPACE_ROOT = PROJECT_ROOT / "workspace"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class LLMSettings(BaseModel):
|
| 20 |
+
model: str = Field(..., description="Model name")
|
| 21 |
+
base_url: str = Field(..., description="API base URL")
|
| 22 |
+
api_key: str = Field(..., description="API key")
|
| 23 |
+
max_tokens: int = Field(4096, description="Maximum number of tokens per request")
|
| 24 |
+
max_input_tokens: Optional[int] = Field(
|
| 25 |
+
None,
|
| 26 |
+
description="Maximum input tokens to use across all requests (None for unlimited)",
|
| 27 |
+
)
|
| 28 |
+
temperature: float = Field(1.0, description="Sampling temperature")
|
| 29 |
+
api_type: str = Field(..., description="Azure, Openai, or Ollama")
|
| 30 |
+
api_version: str = Field(..., description="Azure Openai version if AzureOpenai")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ProxySettings(BaseModel):
|
| 34 |
+
server: str = Field(None, description="Proxy server address")
|
| 35 |
+
username: Optional[str] = Field(None, description="Proxy username")
|
| 36 |
+
password: Optional[str] = Field(None, description="Proxy password")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class SearchSettings(BaseModel):
|
| 40 |
+
engine: str = Field(default="Google", description="Search engine the llm to use")
|
| 41 |
+
fallback_engines: List[str] = Field(
|
| 42 |
+
default_factory=lambda: ["DuckDuckGo", "Baidu", "Bing"],
|
| 43 |
+
description="Fallback search engines to try if the primary engine fails",
|
| 44 |
+
)
|
| 45 |
+
retry_delay: int = Field(
|
| 46 |
+
default=60,
|
| 47 |
+
description="Seconds to wait before retrying all engines again after they all fail",
|
| 48 |
+
)
|
| 49 |
+
max_retries: int = Field(
|
| 50 |
+
default=3,
|
| 51 |
+
description="Maximum number of times to retry all engines when all fail",
|
| 52 |
+
)
|
| 53 |
+
lang: str = Field(
|
| 54 |
+
default="en",
|
| 55 |
+
description="Language code for search results (e.g., en, zh, fr)",
|
| 56 |
+
)
|
| 57 |
+
country: str = Field(
|
| 58 |
+
default="us",
|
| 59 |
+
description="Country code for search results (e.g., us, cn, uk)",
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class RunflowSettings(BaseModel):
|
| 64 |
+
use_data_analysis_agent: bool = Field(
|
| 65 |
+
default=False, description="Enable data analysis agent in run flow"
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class BrowserSettings(BaseModel):
|
| 70 |
+
headless: bool = Field(False, description="Whether to run browser in headless mode")
|
| 71 |
+
disable_security: bool = Field(
|
| 72 |
+
True, description="Disable browser security features"
|
| 73 |
+
)
|
| 74 |
+
extra_chromium_args: List[str] = Field(
|
| 75 |
+
default_factory=list, description="Extra arguments to pass to the browser"
|
| 76 |
+
)
|
| 77 |
+
chrome_instance_path: Optional[str] = Field(
|
| 78 |
+
None, description="Path to a Chrome instance to use"
|
| 79 |
+
)
|
| 80 |
+
wss_url: Optional[str] = Field(
|
| 81 |
+
None, description="Connect to a browser instance via WebSocket"
|
| 82 |
+
)
|
| 83 |
+
cdp_url: Optional[str] = Field(
|
| 84 |
+
None, description="Connect to a browser instance via CDP"
|
| 85 |
+
)
|
| 86 |
+
proxy: Optional[ProxySettings] = Field(
|
| 87 |
+
None, description="Proxy settings for the browser"
|
| 88 |
+
)
|
| 89 |
+
max_content_length: int = Field(
|
| 90 |
+
2000, description="Maximum length for content retrieval operations"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class SandboxSettings(BaseModel):
|
| 95 |
+
"""Configuration for the execution sandbox"""
|
| 96 |
+
|
| 97 |
+
use_sandbox: bool = Field(False, description="Whether to use the sandbox")
|
| 98 |
+
image: str = Field("python:3.12-slim", description="Base image")
|
| 99 |
+
work_dir: str = Field("/workspace", description="Container working directory")
|
| 100 |
+
memory_limit: str = Field("512m", description="Memory limit")
|
| 101 |
+
cpu_limit: float = Field(1.0, description="CPU limit")
|
| 102 |
+
timeout: int = Field(300, description="Default command timeout (seconds)")
|
| 103 |
+
network_enabled: bool = Field(
|
| 104 |
+
False, description="Whether network access is allowed"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class DaytonaSettings(BaseModel):
|
| 109 |
+
daytona_api_key: str
|
| 110 |
+
daytona_server_url: Optional[str] = Field(
|
| 111 |
+
"https://app.daytona.io/api", description=""
|
| 112 |
+
)
|
| 113 |
+
daytona_target: Optional[str] = Field("us", description="enum ['eu', 'us']")
|
| 114 |
+
sandbox_image_name: Optional[str] = Field("whitezxj/sandbox:0.1.0", description="")
|
| 115 |
+
sandbox_entrypoint: Optional[str] = Field(
|
| 116 |
+
"/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf",
|
| 117 |
+
description="",
|
| 118 |
+
)
|
| 119 |
+
# sandbox_id: Optional[str] = Field(
|
| 120 |
+
# None, description="ID of the daytona sandbox to use, if any"
|
| 121 |
+
# )
|
| 122 |
+
VNC_password: Optional[str] = Field(
|
| 123 |
+
"123456", description="VNC password for the vnc service in sandbox"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class MCPServerConfig(BaseModel):
|
| 128 |
+
"""Configuration for a single MCP server"""
|
| 129 |
+
|
| 130 |
+
type: str = Field(..., description="Server connection type (sse or stdio)")
|
| 131 |
+
url: Optional[str] = Field(None, description="Server URL for SSE connections")
|
| 132 |
+
command: Optional[str] = Field(None, description="Command for stdio connections")
|
| 133 |
+
args: List[str] = Field(
|
| 134 |
+
default_factory=list, description="Arguments for stdio command"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class MCPSettings(BaseModel):
|
| 139 |
+
"""Configuration for MCP (Model Context Protocol)"""
|
| 140 |
+
|
| 141 |
+
server_reference: str = Field(
|
| 142 |
+
"app.mcp.server", description="Module reference for the MCP server"
|
| 143 |
+
)
|
| 144 |
+
servers: Dict[str, MCPServerConfig] = Field(
|
| 145 |
+
default_factory=dict, description="MCP server configurations"
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
@classmethod
|
| 149 |
+
def load_server_config(cls) -> Dict[str, MCPServerConfig]:
|
| 150 |
+
"""Load MCP server configuration from JSON file"""
|
| 151 |
+
config_path = PROJECT_ROOT / "config" / "mcp.json"
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
config_file = config_path if config_path.exists() else None
|
| 155 |
+
if not config_file:
|
| 156 |
+
return {}
|
| 157 |
+
|
| 158 |
+
with config_file.open() as f:
|
| 159 |
+
data = json.load(f)
|
| 160 |
+
servers = {}
|
| 161 |
+
|
| 162 |
+
for server_id, server_config in data.get("mcpServers", {}).items():
|
| 163 |
+
servers[server_id] = MCPServerConfig(
|
| 164 |
+
type=server_config["type"],
|
| 165 |
+
url=server_config.get("url"),
|
| 166 |
+
command=server_config.get("command"),
|
| 167 |
+
args=server_config.get("args", []),
|
| 168 |
+
)
|
| 169 |
+
return servers
|
| 170 |
+
except Exception as e:
|
| 171 |
+
raise ValueError(f"Failed to load MCP server config: {e}")
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class AppConfig(BaseModel):
|
| 175 |
+
llm: Dict[str, LLMSettings]
|
| 176 |
+
sandbox: Optional[SandboxSettings] = Field(
|
| 177 |
+
None, description="Sandbox configuration"
|
| 178 |
+
)
|
| 179 |
+
browser_config: Optional[BrowserSettings] = Field(
|
| 180 |
+
None, description="Browser configuration"
|
| 181 |
+
)
|
| 182 |
+
search_config: Optional[SearchSettings] = Field(
|
| 183 |
+
None, description="Search configuration"
|
| 184 |
+
)
|
| 185 |
+
mcp_config: Optional[MCPSettings] = Field(None, description="MCP configuration")
|
| 186 |
+
run_flow_config: Optional[RunflowSettings] = Field(
|
| 187 |
+
None, description="Run flow configuration"
|
| 188 |
+
)
|
| 189 |
+
daytona_config: Optional[DaytonaSettings] = Field(
|
| 190 |
+
None, description="Daytona configuration"
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
class Config:
|
| 194 |
+
arbitrary_types_allowed = True
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class Config:
|
| 198 |
+
_instance = None
|
| 199 |
+
_lock = threading.Lock()
|
| 200 |
+
_initialized = False
|
| 201 |
+
|
| 202 |
+
def __new__(cls):
|
| 203 |
+
if cls._instance is None:
|
| 204 |
+
with cls._lock:
|
| 205 |
+
if cls._instance is None:
|
| 206 |
+
cls._instance = super().__new__(cls)
|
| 207 |
+
return cls._instance
|
| 208 |
+
|
| 209 |
+
def __init__(self):
|
| 210 |
+
if not self._initialized:
|
| 211 |
+
with self._lock:
|
| 212 |
+
if not self._initialized:
|
| 213 |
+
self._config = None
|
| 214 |
+
self._load_initial_config()
|
| 215 |
+
self._initialized = True
|
| 216 |
+
|
| 217 |
+
@staticmethod
|
| 218 |
+
def _get_config_path() -> Path:
|
| 219 |
+
root = PROJECT_ROOT
|
| 220 |
+
config_path = root / "config" / "config.toml"
|
| 221 |
+
if config_path.exists():
|
| 222 |
+
return config_path
|
| 223 |
+
example_path = root / "config" / "config.example.toml"
|
| 224 |
+
if example_path.exists():
|
| 225 |
+
return example_path
|
| 226 |
+
raise FileNotFoundError("No configuration file found in config directory")
|
| 227 |
+
|
| 228 |
+
def _load_config(self) -> dict:
|
| 229 |
+
config_path = self._get_config_path()
|
| 230 |
+
with config_path.open("rb") as f:
|
| 231 |
+
return tomllib.load(f)
|
| 232 |
+
|
| 233 |
+
def _load_initial_config(self):
|
| 234 |
+
raw_config = self._load_config()
|
| 235 |
+
base_llm = raw_config.get("llm", {})
|
| 236 |
+
llm_overrides = {
|
| 237 |
+
k: v for k, v in raw_config.get("llm", {}).items() if isinstance(v, dict)
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
default_settings = {
|
| 241 |
+
"model": base_llm.get("model"),
|
| 242 |
+
"base_url": base_llm.get("base_url"),
|
| 243 |
+
"api_key": base_llm.get("api_key"),
|
| 244 |
+
"max_tokens": base_llm.get("max_tokens", 4096),
|
| 245 |
+
"max_input_tokens": base_llm.get("max_input_tokens"),
|
| 246 |
+
"temperature": base_llm.get("temperature", 1.0),
|
| 247 |
+
"api_type": base_llm.get("api_type", ""),
|
| 248 |
+
"api_version": base_llm.get("api_version", ""),
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
# handle browser config.
|
| 252 |
+
browser_config = raw_config.get("browser", {})
|
| 253 |
+
browser_settings = None
|
| 254 |
+
|
| 255 |
+
if browser_config:
|
| 256 |
+
# handle proxy settings.
|
| 257 |
+
proxy_config = browser_config.get("proxy", {})
|
| 258 |
+
proxy_settings = None
|
| 259 |
+
|
| 260 |
+
if proxy_config and proxy_config.get("server"):
|
| 261 |
+
proxy_settings = ProxySettings(
|
| 262 |
+
**{
|
| 263 |
+
k: v
|
| 264 |
+
for k, v in proxy_config.items()
|
| 265 |
+
if k in ["server", "username", "password"] and v
|
| 266 |
+
}
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# filter valid browser config parameters.
|
| 270 |
+
valid_browser_params = {
|
| 271 |
+
k: v
|
| 272 |
+
for k, v in browser_config.items()
|
| 273 |
+
if k in BrowserSettings.__annotations__ and v is not None
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
# if there is proxy settings, add it to the parameters.
|
| 277 |
+
if proxy_settings:
|
| 278 |
+
valid_browser_params["proxy"] = proxy_settings
|
| 279 |
+
|
| 280 |
+
# only create BrowserSettings when there are valid parameters.
|
| 281 |
+
if valid_browser_params:
|
| 282 |
+
browser_settings = BrowserSettings(**valid_browser_params)
|
| 283 |
+
|
| 284 |
+
search_config = raw_config.get("search", {})
|
| 285 |
+
search_settings = None
|
| 286 |
+
if search_config:
|
| 287 |
+
search_settings = SearchSettings(**search_config)
|
| 288 |
+
sandbox_config = raw_config.get("sandbox", {})
|
| 289 |
+
if sandbox_config:
|
| 290 |
+
sandbox_settings = SandboxSettings(**sandbox_config)
|
| 291 |
+
else:
|
| 292 |
+
sandbox_settings = SandboxSettings()
|
| 293 |
+
daytona_config = raw_config.get("daytona", {})
|
| 294 |
+
if daytona_config:
|
| 295 |
+
daytona_settings = DaytonaSettings(**daytona_config)
|
| 296 |
+
else:
|
| 297 |
+
daytona_settings = DaytonaSettings()
|
| 298 |
+
|
| 299 |
+
mcp_config = raw_config.get("mcp", {})
|
| 300 |
+
mcp_settings = None
|
| 301 |
+
if mcp_config:
|
| 302 |
+
# Load server configurations from JSON
|
| 303 |
+
mcp_config["servers"] = MCPSettings.load_server_config()
|
| 304 |
+
mcp_settings = MCPSettings(**mcp_config)
|
| 305 |
+
else:
|
| 306 |
+
mcp_settings = MCPSettings(servers=MCPSettings.load_server_config())
|
| 307 |
+
|
| 308 |
+
run_flow_config = raw_config.get("runflow")
|
| 309 |
+
if run_flow_config:
|
| 310 |
+
run_flow_settings = RunflowSettings(**run_flow_config)
|
| 311 |
+
else:
|
| 312 |
+
run_flow_settings = RunflowSettings()
|
| 313 |
+
config_dict = {
|
| 314 |
+
"llm": {
|
| 315 |
+
"default": default_settings,
|
| 316 |
+
**{
|
| 317 |
+
name: {**default_settings, **override_config}
|
| 318 |
+
for name, override_config in llm_overrides.items()
|
| 319 |
+
},
|
| 320 |
+
},
|
| 321 |
+
"sandbox": sandbox_settings,
|
| 322 |
+
"browser_config": browser_settings,
|
| 323 |
+
"search_config": search_settings,
|
| 324 |
+
"mcp_config": mcp_settings,
|
| 325 |
+
"run_flow_config": run_flow_settings,
|
| 326 |
+
"daytona_config": daytona_settings,
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
self._config = AppConfig(**config_dict)
|
| 330 |
+
|
| 331 |
+
@property
|
| 332 |
+
def llm(self) -> Dict[str, LLMSettings]:
|
| 333 |
+
return self._config.llm
|
| 334 |
+
|
| 335 |
+
@property
|
| 336 |
+
def sandbox(self) -> SandboxSettings:
|
| 337 |
+
return self._config.sandbox
|
| 338 |
+
|
| 339 |
+
@property
|
| 340 |
+
def daytona(self) -> DaytonaSettings:
|
| 341 |
+
return self._config.daytona_config
|
| 342 |
+
|
| 343 |
+
@property
|
| 344 |
+
def browser_config(self) -> Optional[BrowserSettings]:
|
| 345 |
+
return self._config.browser_config
|
| 346 |
+
|
| 347 |
+
@property
|
| 348 |
+
def search_config(self) -> Optional[SearchSettings]:
|
| 349 |
+
return self._config.search_config
|
| 350 |
+
|
| 351 |
+
@property
|
| 352 |
+
def mcp_config(self) -> MCPSettings:
|
| 353 |
+
"""Get the MCP configuration"""
|
| 354 |
+
return self._config.mcp_config
|
| 355 |
+
|
| 356 |
+
@property
|
| 357 |
+
def run_flow_config(self) -> RunflowSettings:
|
| 358 |
+
"""Get the Run Flow configuration"""
|
| 359 |
+
return self._config.run_flow_config
|
| 360 |
+
|
| 361 |
+
@property
|
| 362 |
+
def workspace_root(self) -> Path:
|
| 363 |
+
"""Get the workspace root directory"""
|
| 364 |
+
return WORKSPACE_ROOT
|
| 365 |
+
|
| 366 |
+
@property
|
| 367 |
+
def root_path(self) -> Path:
|
| 368 |
+
"""Get the root path of the application"""
|
| 369 |
+
return PROJECT_ROOT
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
config = Config()
|
app/huggingface_models.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app/logger.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
from loguru import logger as _logger
|
| 5 |
+
|
| 6 |
+
from app.config import PROJECT_ROOT
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_print_level = "INFO"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def define_log_level(print_level="INFO", logfile_level="DEBUG", name: str = None):
|
| 13 |
+
"""Adjust the log level to above level"""
|
| 14 |
+
global _print_level
|
| 15 |
+
_print_level = print_level
|
| 16 |
+
|
| 17 |
+
current_date = datetime.now()
|
| 18 |
+
formatted_date = current_date.strftime("%Y%m%d%H%M%S")
|
| 19 |
+
log_name = (
|
| 20 |
+
f"{name}_{formatted_date}" if name else formatted_date
|
| 21 |
+
) # name a log with prefix name
|
| 22 |
+
|
| 23 |
+
_logger.remove()
|
| 24 |
+
_logger.add(sys.stderr, level=print_level)
|
| 25 |
+
_logger.add(PROJECT_ROOT / f"logs/{log_name}.log", level=logfile_level)
|
| 26 |
+
return _logger
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
logger = define_log_level()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
logger.info("Starting application")
|
| 34 |
+
logger.debug("Debug message")
|
| 35 |
+
logger.warning("Warning message")
|
| 36 |
+
logger.error("Error message")
|
| 37 |
+
logger.critical("Critical message")
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
raise ValueError("Test error")
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.exception(f"An error occurred: {e}")
|
app/production_config.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Complete Configuration for OpenManus Production Deployment
|
| 3 |
+
Includes: All model configurations, agent settings, category mappings, and service configurations
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from typing import Dict, List, Optional, Any
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from enum import Enum
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class ModelConfig:
|
| 14 |
+
"""Configuration for individual AI models"""
|
| 15 |
+
|
| 16 |
+
name: str
|
| 17 |
+
category: str
|
| 18 |
+
api_endpoint: str
|
| 19 |
+
max_tokens: int = 4096
|
| 20 |
+
temperature: float = 0.7
|
| 21 |
+
supported_formats: List[str] = None
|
| 22 |
+
special_parameters: Dict[str, Any] = None
|
| 23 |
+
rate_limit: int = 100 # requests per minute
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class CategoryConfig:
|
| 27 |
+
"""Configuration for model categories"""
|
| 28 |
+
|
| 29 |
+
# Core AI Models - Text Generation (Qwen, DeepSeek, etc.)
|
| 30 |
+
TEXT_GENERATION_MODELS = {
|
| 31 |
+
# Qwen Models (35 models)
|
| 32 |
+
"qwen/qwen-2.5-72b-instruct": ModelConfig(
|
| 33 |
+
name="Qwen 2.5 72B Instruct",
|
| 34 |
+
category="text-generation",
|
| 35 |
+
api_endpoint="https://api-inference.huggingface.co/models/Qwen/Qwen2.5-72B-Instruct",
|
| 36 |
+
max_tokens=8192,
|
| 37 |
+
temperature=0.7,
|
| 38 |
+
),
|
| 39 |
+
"qwen/qwen-2.5-32b-instruct": ModelConfig(
|
| 40 |
+
name="Qwen 2.5 32B Instruct",
|
| 41 |
+
category="text-generation",
|
| 42 |
+
api_endpoint="https://api-inference.huggingface.co/models/Qwen/Qwen2.5-32B-Instruct",
|
| 43 |
+
max_tokens=8192,
|
| 44 |
+
),
|
| 45 |
+
"qwen/qwen-2.5-14b-instruct": ModelConfig(
|
| 46 |
+
name="Qwen 2.5 14B Instruct",
|
| 47 |
+
category="text-generation",
|
| 48 |
+
api_endpoint="https://api-inference.huggingface.co/models/Qwen/Qwen2.5-14B-Instruct",
|
| 49 |
+
max_tokens=8192,
|
| 50 |
+
),
|
| 51 |
+
"qwen/qwen-2.5-7b-instruct": ModelConfig(
|
| 52 |
+
name="Qwen 2.5 7B Instruct",
|
| 53 |
+
category="text-generation",
|
| 54 |
+
api_endpoint="https://api-inference.huggingface.co/models/Qwen/Qwen2.5-7B-Instruct",
|
| 55 |
+
),
|
| 56 |
+
"qwen/qwen-2.5-3b-instruct": ModelConfig(
|
| 57 |
+
name="Qwen 2.5 3B Instruct",
|
| 58 |
+
category="text-generation",
|
| 59 |
+
api_endpoint="https://api-inference.huggingface.co/models/Qwen/Qwen2.5-3B-Instruct",
|
| 60 |
+
),
|
| 61 |
+
"qwen/qwen-2.5-1.5b-instruct": ModelConfig(
|
| 62 |
+
name="Qwen 2.5 1.5B Instruct",
|
| 63 |
+
category="text-generation",
|
| 64 |
+
api_endpoint="https://api-inference.huggingface.co/models/Qwen/Qwen2.5-1.5B-Instruct",
|
| 65 |
+
),
|
| 66 |
+
"qwen/qwen-2.5-0.5b-instruct": ModelConfig(
|
| 67 |
+
name="Qwen 2.5 0.5B Instruct",
|
| 68 |
+
category="text-generation",
|
| 69 |
+
api_endpoint="https://api-inference.huggingface.co/models/Qwen/Qwen2.5-0.5B-Instruct",
|
| 70 |
+
),
|
| 71 |
+
# ... (Add all 35 Qwen models)
|
| 72 |
+
# DeepSeek Models (17 models)
|
| 73 |
+
"deepseek-ai/deepseek-coder-33b-instruct": ModelConfig(
|
| 74 |
+
name="DeepSeek Coder 33B Instruct",
|
| 75 |
+
category="code-generation",
|
| 76 |
+
api_endpoint="https://api-inference.huggingface.co/models/deepseek-ai/deepseek-coder-33b-instruct",
|
| 77 |
+
max_tokens=8192,
|
| 78 |
+
special_parameters={"code_focused": True},
|
| 79 |
+
),
|
| 80 |
+
"deepseek-ai/deepseek-coder-6.7b-instruct": ModelConfig(
|
| 81 |
+
name="DeepSeek Coder 6.7B Instruct",
|
| 82 |
+
category="code-generation",
|
| 83 |
+
api_endpoint="https://api-inference.huggingface.co/models/deepseek-ai/deepseek-coder-6.7b-instruct",
|
| 84 |
+
),
|
| 85 |
+
# ... (Add all 17 DeepSeek models)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# Image Editing Models (10 models)
|
| 89 |
+
IMAGE_EDITING_MODELS = {
|
| 90 |
+
"stabilityai/stable-diffusion-xl-refiner-1.0": ModelConfig(
|
| 91 |
+
name="SDXL Refiner 1.0",
|
| 92 |
+
category="image-editing",
|
| 93 |
+
api_endpoint="https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-refiner-1.0",
|
| 94 |
+
supported_formats=["image/png", "image/jpeg"],
|
| 95 |
+
),
|
| 96 |
+
"runwayml/stable-diffusion-inpainting": ModelConfig(
|
| 97 |
+
name="Stable Diffusion Inpainting",
|
| 98 |
+
category="image-inpainting",
|
| 99 |
+
api_endpoint="https://api-inference.huggingface.co/models/runwayml/stable-diffusion-inpainting",
|
| 100 |
+
supported_formats=["image/png", "image/jpeg"],
|
| 101 |
+
),
|
| 102 |
+
# ... (Add all 10 image editing models)
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
# TTS/STT Models (15 models)
|
| 106 |
+
SPEECH_MODELS = {
|
| 107 |
+
"microsoft/speecht5_tts": ModelConfig(
|
| 108 |
+
name="SpeechT5 TTS",
|
| 109 |
+
category="text-to-speech",
|
| 110 |
+
api_endpoint="https://api-inference.huggingface.co/models/microsoft/speecht5_tts",
|
| 111 |
+
supported_formats=["audio/wav", "audio/mp3"],
|
| 112 |
+
),
|
| 113 |
+
"openai/whisper-large-v3": ModelConfig(
|
| 114 |
+
name="Whisper Large v3",
|
| 115 |
+
category="automatic-speech-recognition",
|
| 116 |
+
api_endpoint="https://api-inference.huggingface.co/models/openai/whisper-large-v3",
|
| 117 |
+
supported_formats=["audio/wav", "audio/mp3", "audio/flac"],
|
| 118 |
+
),
|
| 119 |
+
# ... (Add all 15 speech models)
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
# Face Swap Models (6 models)
|
| 123 |
+
FACE_SWAP_MODELS = {
|
| 124 |
+
"deepinsight/insightface": ModelConfig(
|
| 125 |
+
name="InsightFace",
|
| 126 |
+
category="face-swap",
|
| 127 |
+
api_endpoint="https://api-inference.huggingface.co/models/deepinsight/insightface",
|
| 128 |
+
supported_formats=["image/png", "image/jpeg"],
|
| 129 |
+
),
|
| 130 |
+
# ... (Add all 6 face swap models)
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
# Talking Avatar Models (9 models)
|
| 134 |
+
AVATAR_MODELS = {
|
| 135 |
+
"microsoft/DiT-XL-2-512": ModelConfig(
|
| 136 |
+
name="DiT Avatar Generator",
|
| 137 |
+
category="talking-avatar",
|
| 138 |
+
api_endpoint="https://api-inference.huggingface.co/models/microsoft/DiT-XL-2-512",
|
| 139 |
+
supported_formats=["video/mp4", "image/png"],
|
| 140 |
+
),
|
| 141 |
+
# ... (Add all 9 avatar models)
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
# Arabic-English Interactive Models (12 models)
|
| 145 |
+
ARABIC_ENGLISH_MODELS = {
|
| 146 |
+
"aubmindlab/bert-base-arabertv02": ModelConfig(
|
| 147 |
+
name="AraBERT v02",
|
| 148 |
+
category="arabic-text",
|
| 149 |
+
api_endpoint="https://api-inference.huggingface.co/models/aubmindlab/bert-base-arabertv02",
|
| 150 |
+
special_parameters={"language": "ar-en"},
|
| 151 |
+
),
|
| 152 |
+
"UBC-NLP/MARBERT": ModelConfig(
|
| 153 |
+
name="MARBERT",
|
| 154 |
+
category="arabic-text",
|
| 155 |
+
api_endpoint="https://api-inference.huggingface.co/models/UBC-NLP/MARBERT",
|
| 156 |
+
special_parameters={"language": "ar-en"},
|
| 157 |
+
),
|
| 158 |
+
# ... (Add all 12 Arabic-English models)
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class AgentConfig:
|
| 163 |
+
"""Configuration for AI Agents"""
|
| 164 |
+
|
| 165 |
+
# Manus Agent Configuration
|
| 166 |
+
MANUS_AGENT = {
|
| 167 |
+
"name": "Manus",
|
| 168 |
+
"description": "Versatile AI agent with 200+ models",
|
| 169 |
+
"max_steps": 20,
|
| 170 |
+
"max_observe": 10000,
|
| 171 |
+
"system_prompt_template": """You are Manus, an advanced AI agent with access to 200+ specialized models.
|
| 172 |
+
|
| 173 |
+
Available categories:
|
| 174 |
+
- Text Generation (Qwen, DeepSeek, etc.)
|
| 175 |
+
- Image Editing & Generation
|
| 176 |
+
- Speech (TTS/STT)
|
| 177 |
+
- Face Swap & Avatar Generation
|
| 178 |
+
- Arabic-English Interactive Models
|
| 179 |
+
- Code Generation & Review
|
| 180 |
+
- Multimodal AI
|
| 181 |
+
- Document Processing
|
| 182 |
+
- 3D Generation
|
| 183 |
+
- Video Processing
|
| 184 |
+
|
| 185 |
+
User workspace: {directory}""",
|
| 186 |
+
"tools": [
|
| 187 |
+
"PythonExecute",
|
| 188 |
+
"BrowserUseTool",
|
| 189 |
+
"StrReplaceEditor",
|
| 190 |
+
"AskHuman",
|
| 191 |
+
"Terminate",
|
| 192 |
+
"HuggingFaceModels",
|
| 193 |
+
],
|
| 194 |
+
"model_preferences": {
|
| 195 |
+
"text": "qwen/qwen-2.5-72b-instruct",
|
| 196 |
+
"code": "deepseek-ai/deepseek-coder-33b-instruct",
|
| 197 |
+
"image": "stabilityai/stable-diffusion-xl-refiner-1.0",
|
| 198 |
+
"speech": "microsoft/speecht5_tts",
|
| 199 |
+
"arabic": "aubmindlab/bert-base-arabertv02",
|
| 200 |
+
},
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class ServiceConfig:
|
| 205 |
+
"""Configuration for all services"""
|
| 206 |
+
|
| 207 |
+
# Cloudflare Services
|
| 208 |
+
CLOUDFLARE_CONFIG = {
|
| 209 |
+
"d1_database": {
|
| 210 |
+
"enabled": True,
|
| 211 |
+
"tables": ["users", "sessions", "agent_interactions", "model_usage"],
|
| 212 |
+
"auto_migrate": True,
|
| 213 |
+
},
|
| 214 |
+
"r2_storage": {
|
| 215 |
+
"enabled": True,
|
| 216 |
+
"buckets": ["user-files", "generated-content", "model-cache"],
|
| 217 |
+
"max_file_size": "100MB",
|
| 218 |
+
},
|
| 219 |
+
"kv_storage": {
|
| 220 |
+
"enabled": True,
|
| 221 |
+
"namespaces": ["sessions", "model-cache", "user-preferences"],
|
| 222 |
+
"ttl": 86400, # 24 hours
|
| 223 |
+
},
|
| 224 |
+
"durable_objects": {
|
| 225 |
+
"enabled": True,
|
| 226 |
+
"classes": ["ChatSession", "ModelRouter", "UserContext"],
|
| 227 |
+
},
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
# Authentication Configuration
|
| 231 |
+
AUTH_CONFIG = {
|
| 232 |
+
"method": "mobile_password",
|
| 233 |
+
"password_min_length": 8,
|
| 234 |
+
"session_duration": 86400, # 24 hours
|
| 235 |
+
"max_concurrent_sessions": 5,
|
| 236 |
+
"mobile_validation": {
|
| 237 |
+
"international": True,
|
| 238 |
+
"formats": ["+1234567890", "01234567890"],
|
| 239 |
+
},
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
# Model Usage Configuration
|
| 243 |
+
MODEL_CONFIG = {
|
| 244 |
+
"rate_limits": {
|
| 245 |
+
"free_tier": 100, # requests per day
|
| 246 |
+
"premium_tier": 1000,
|
| 247 |
+
"enterprise_tier": 10000,
|
| 248 |
+
},
|
| 249 |
+
"fallback_models": {
|
| 250 |
+
"text": ["qwen/qwen-2.5-7b-instruct", "qwen/qwen-2.5-3b-instruct"],
|
| 251 |
+
"image": ["runwayml/stable-diffusion-v1-5"],
|
| 252 |
+
"code": ["deepseek-ai/deepseek-coder-6.7b-instruct"],
|
| 253 |
+
},
|
| 254 |
+
"cache_settings": {"enabled": True, "ttl": 3600, "max_size": "1GB"}, # 1 hour
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class EnvironmentConfig:
|
| 259 |
+
"""Environment-specific configurations"""
|
| 260 |
+
|
| 261 |
+
@staticmethod
|
| 262 |
+
def get_production_config():
|
| 263 |
+
"""Get production environment configuration"""
|
| 264 |
+
return {
|
| 265 |
+
"environment": "production",
|
| 266 |
+
"debug": False,
|
| 267 |
+
"log_level": "INFO",
|
| 268 |
+
"server": {"host": "0.0.0.0", "port": 7860, "workers": 4},
|
| 269 |
+
"database": {"type": "sqlite", "url": "auth.db", "pool_size": 10},
|
| 270 |
+
"security": {
|
| 271 |
+
"secret_key": os.getenv("SECRET_KEY", "your-secret-key"),
|
| 272 |
+
"cors_origins": ["*"],
|
| 273 |
+
"rate_limiting": True,
|
| 274 |
+
},
|
| 275 |
+
"monitoring": {"metrics": True, "logging": True, "health_checks": True},
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
@staticmethod
|
| 279 |
+
def get_development_config():
|
| 280 |
+
"""Get development environment configuration"""
|
| 281 |
+
return {
|
| 282 |
+
"environment": "development",
|
| 283 |
+
"debug": True,
|
| 284 |
+
"log_level": "DEBUG",
|
| 285 |
+
"server": {"host": "127.0.0.1", "port": 7860, "workers": 1},
|
| 286 |
+
"database": {"type": "sqlite", "url": "auth_dev.db", "pool_size": 2},
|
| 287 |
+
"security": {
|
| 288 |
+
"secret_key": "dev-secret-key",
|
| 289 |
+
"cors_origins": ["http://localhost:*"],
|
| 290 |
+
"rate_limiting": False,
|
| 291 |
+
},
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
# Global configuration instance
|
| 296 |
+
class OpenManusConfig:
|
| 297 |
+
"""Main configuration class for OpenManus"""
|
| 298 |
+
|
| 299 |
+
def __init__(self, environment: str = "production"):
|
| 300 |
+
self.environment = environment
|
| 301 |
+
self.categories = CategoryConfig()
|
| 302 |
+
self.agent = AgentConfig()
|
| 303 |
+
self.services = ServiceConfig()
|
| 304 |
+
|
| 305 |
+
if environment == "production":
|
| 306 |
+
self.env_config = EnvironmentConfig.get_production_config()
|
| 307 |
+
else:
|
| 308 |
+
self.env_config = EnvironmentConfig.get_development_config()
|
| 309 |
+
|
| 310 |
+
def get_model_config(self, model_id: str) -> Optional[ModelConfig]:
|
| 311 |
+
"""Get configuration for a specific model"""
|
| 312 |
+
all_models = {
|
| 313 |
+
**self.categories.TEXT_GENERATION_MODELS,
|
| 314 |
+
**self.categories.IMAGE_EDITING_MODELS,
|
| 315 |
+
**self.categories.SPEECH_MODELS,
|
| 316 |
+
**self.categories.FACE_SWAP_MODELS,
|
| 317 |
+
**self.categories.AVATAR_MODELS,
|
| 318 |
+
**self.categories.ARABIC_ENGLISH_MODELS,
|
| 319 |
+
}
|
| 320 |
+
return all_models.get(model_id)
|
| 321 |
+
|
| 322 |
+
def get_category_models(self, category: str) -> Dict[str, ModelConfig]:
|
| 323 |
+
"""Get all models in a category"""
|
| 324 |
+
if category == "text-generation":
|
| 325 |
+
return self.categories.TEXT_GENERATION_MODELS
|
| 326 |
+
elif category == "image-editing":
|
| 327 |
+
return self.categories.IMAGE_EDITING_MODELS
|
| 328 |
+
elif category in ["text-to-speech", "automatic-speech-recognition"]:
|
| 329 |
+
return self.categories.SPEECH_MODELS
|
| 330 |
+
elif category == "face-swap":
|
| 331 |
+
return self.categories.FACE_SWAP_MODELS
|
| 332 |
+
elif category == "talking-avatar":
|
| 333 |
+
return self.categories.AVATAR_MODELS
|
| 334 |
+
elif category == "arabic-text":
|
| 335 |
+
return self.categories.ARABIC_ENGLISH_MODELS
|
| 336 |
+
else:
|
| 337 |
+
return {}
|
| 338 |
+
|
| 339 |
+
def validate_config(self) -> bool:
|
| 340 |
+
"""Validate the configuration"""
|
| 341 |
+
try:
|
| 342 |
+
# Check required environment variables
|
| 343 |
+
required_env = (
|
| 344 |
+
["CLOUDFLARE_API_TOKEN", "HF_TOKEN"]
|
| 345 |
+
if self.environment == "production"
|
| 346 |
+
else []
|
| 347 |
+
)
|
| 348 |
+
missing_env = [var for var in required_env if not os.getenv(var)]
|
| 349 |
+
|
| 350 |
+
if missing_env:
|
| 351 |
+
print(f"Missing required environment variables: {missing_env}")
|
| 352 |
+
return False
|
| 353 |
+
|
| 354 |
+
print(f"Configuration validated for {self.environment} environment")
|
| 355 |
+
return True
|
| 356 |
+
|
| 357 |
+
except Exception as e:
|
| 358 |
+
print(f"Configuration validation failed: {e}")
|
| 359 |
+
return False
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
# Create global config instance
|
| 363 |
+
config = OpenManusConfig(environment=os.getenv("ENVIRONMENT", "production"))
|
app/prompt/__init__.py
ADDED
|
File without changes
|
app/prompt/manus.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SYSTEM_PROMPT = (
|
| 2 |
+
"You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, web browsing, or human interaction (only for extreme cases), you can handle it all."
|
| 3 |
+
"The initial directory is: {directory}"
|
| 4 |
+
)
|
| 5 |
+
|
| 6 |
+
NEXT_STEP_PROMPT = """
|
| 7 |
+
Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps.
|
| 8 |
+
|
| 9 |
+
If you want to stop the interaction at any point, use the `terminate` tool/function call.
|
| 10 |
+
"""
|
app/schema.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from typing import Any, List, Literal, Optional, Union
|
| 3 |
+
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Role(str, Enum):
|
| 8 |
+
"""Message role options"""
|
| 9 |
+
|
| 10 |
+
SYSTEM = "system"
|
| 11 |
+
USER = "user"
|
| 12 |
+
ASSISTANT = "assistant"
|
| 13 |
+
TOOL = "tool"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
ROLE_VALUES = tuple(role.value for role in Role)
|
| 17 |
+
ROLE_TYPE = Literal[ROLE_VALUES] # type: ignore
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ToolChoice(str, Enum):
|
| 21 |
+
"""Tool choice options"""
|
| 22 |
+
|
| 23 |
+
NONE = "none"
|
| 24 |
+
AUTO = "auto"
|
| 25 |
+
REQUIRED = "required"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
TOOL_CHOICE_VALUES = tuple(choice.value for choice in ToolChoice)
|
| 29 |
+
TOOL_CHOICE_TYPE = Literal[TOOL_CHOICE_VALUES] # type: ignore
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class AgentState(str, Enum):
|
| 33 |
+
"""Agent execution states"""
|
| 34 |
+
|
| 35 |
+
IDLE = "IDLE"
|
| 36 |
+
RUNNING = "RUNNING"
|
| 37 |
+
FINISHED = "FINISHED"
|
| 38 |
+
ERROR = "ERROR"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Function(BaseModel):
|
| 42 |
+
name: str
|
| 43 |
+
arguments: str
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class ToolCall(BaseModel):
|
| 47 |
+
"""Represents a tool/function call in a message"""
|
| 48 |
+
|
| 49 |
+
id: str
|
| 50 |
+
type: str = "function"
|
| 51 |
+
function: Function
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class Message(BaseModel):
|
| 55 |
+
"""Represents a chat message in the conversation"""
|
| 56 |
+
|
| 57 |
+
role: ROLE_TYPE = Field(...) # type: ignore
|
| 58 |
+
content: Optional[str] = Field(default=None)
|
| 59 |
+
tool_calls: Optional[List[ToolCall]] = Field(default=None)
|
| 60 |
+
name: Optional[str] = Field(default=None)
|
| 61 |
+
tool_call_id: Optional[str] = Field(default=None)
|
| 62 |
+
base64_image: Optional[str] = Field(default=None)
|
| 63 |
+
|
| 64 |
+
def __add__(self, other) -> List["Message"]:
|
| 65 |
+
"""支持 Message + list 或 Message + Message 的操作"""
|
| 66 |
+
if isinstance(other, list):
|
| 67 |
+
return [self] + other
|
| 68 |
+
elif isinstance(other, Message):
|
| 69 |
+
return [self, other]
|
| 70 |
+
else:
|
| 71 |
+
raise TypeError(
|
| 72 |
+
f"unsupported operand type(s) for +: '{type(self).__name__}' and '{type(other).__name__}'"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
def __radd__(self, other) -> List["Message"]:
|
| 76 |
+
"""支持 list + Message 的操作"""
|
| 77 |
+
if isinstance(other, list):
|
| 78 |
+
return other + [self]
|
| 79 |
+
else:
|
| 80 |
+
raise TypeError(
|
| 81 |
+
f"unsupported operand type(s) for +: '{type(other).__name__}' and '{type(self).__name__}'"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
def to_dict(self) -> dict:
|
| 85 |
+
"""Convert message to dictionary format"""
|
| 86 |
+
message = {"role": self.role}
|
| 87 |
+
if self.content is not None:
|
| 88 |
+
message["content"] = self.content
|
| 89 |
+
if self.tool_calls is not None:
|
| 90 |
+
message["tool_calls"] = [tool_call.dict() for tool_call in self.tool_calls]
|
| 91 |
+
if self.name is not None:
|
| 92 |
+
message["name"] = self.name
|
| 93 |
+
if self.tool_call_id is not None:
|
| 94 |
+
message["tool_call_id"] = self.tool_call_id
|
| 95 |
+
if self.base64_image is not None:
|
| 96 |
+
message["base64_image"] = self.base64_image
|
| 97 |
+
return message
|
| 98 |
+
|
| 99 |
+
@classmethod
|
| 100 |
+
def user_message(
|
| 101 |
+
cls, content: str, base64_image: Optional[str] = None
|
| 102 |
+
) -> "Message":
|
| 103 |
+
"""Create a user message"""
|
| 104 |
+
return cls(role=Role.USER, content=content, base64_image=base64_image)
|
| 105 |
+
|
| 106 |
+
@classmethod
|
| 107 |
+
def system_message(cls, content: str) -> "Message":
|
| 108 |
+
"""Create a system message"""
|
| 109 |
+
return cls(role=Role.SYSTEM, content=content)
|
| 110 |
+
|
| 111 |
+
@classmethod
|
| 112 |
+
def assistant_message(
|
| 113 |
+
cls, content: Optional[str] = None, base64_image: Optional[str] = None
|
| 114 |
+
) -> "Message":
|
| 115 |
+
"""Create an assistant message"""
|
| 116 |
+
return cls(role=Role.ASSISTANT, content=content, base64_image=base64_image)
|
| 117 |
+
|
| 118 |
+
@classmethod
|
| 119 |
+
def tool_message(
|
| 120 |
+
cls, content: str, name, tool_call_id: str, base64_image: Optional[str] = None
|
| 121 |
+
) -> "Message":
|
| 122 |
+
"""Create a tool message"""
|
| 123 |
+
return cls(
|
| 124 |
+
role=Role.TOOL,
|
| 125 |
+
content=content,
|
| 126 |
+
name=name,
|
| 127 |
+
tool_call_id=tool_call_id,
|
| 128 |
+
base64_image=base64_image,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
@classmethod
|
| 132 |
+
def from_tool_calls(
|
| 133 |
+
cls,
|
| 134 |
+
tool_calls: List[Any],
|
| 135 |
+
content: Union[str, List[str]] = "",
|
| 136 |
+
base64_image: Optional[str] = None,
|
| 137 |
+
**kwargs,
|
| 138 |
+
):
|
| 139 |
+
"""Create ToolCallsMessage from raw tool calls.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
tool_calls: Raw tool calls from LLM
|
| 143 |
+
content: Optional message content
|
| 144 |
+
base64_image: Optional base64 encoded image
|
| 145 |
+
"""
|
| 146 |
+
formatted_calls = [
|
| 147 |
+
{"id": call.id, "function": call.function.model_dump(), "type": "function"}
|
| 148 |
+
for call in tool_calls
|
| 149 |
+
]
|
| 150 |
+
return cls(
|
| 151 |
+
role=Role.ASSISTANT,
|
| 152 |
+
content=content,
|
| 153 |
+
tool_calls=formatted_calls,
|
| 154 |
+
base64_image=base64_image,
|
| 155 |
+
**kwargs,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class Memory(BaseModel):
|
| 160 |
+
messages: List[Message] = Field(default_factory=list)
|
| 161 |
+
max_messages: int = Field(default=100)
|
| 162 |
+
|
| 163 |
+
def add_message(self, message: Message) -> None:
|
| 164 |
+
"""Add a message to memory"""
|
| 165 |
+
self.messages.append(message)
|
| 166 |
+
# Optional: Implement message limit
|
| 167 |
+
if len(self.messages) > self.max_messages:
|
| 168 |
+
self.messages = self.messages[-self.max_messages :]
|
| 169 |
+
|
| 170 |
+
def add_messages(self, messages: List[Message]) -> None:
|
| 171 |
+
"""Add multiple messages to memory"""
|
| 172 |
+
self.messages.extend(messages)
|
| 173 |
+
# Optional: Implement message limit
|
| 174 |
+
if len(self.messages) > self.max_messages:
|
| 175 |
+
self.messages = self.messages[-self.max_messages :]
|
| 176 |
+
|
| 177 |
+
def clear(self) -> None:
|
| 178 |
+
"""Clear all messages"""
|
| 179 |
+
self.messages.clear()
|
| 180 |
+
|
| 181 |
+
def get_recent_messages(self, n: int) -> List[Message]:
|
| 182 |
+
"""Get n most recent messages"""
|
| 183 |
+
return self.messages[-n:]
|
| 184 |
+
|
| 185 |
+
def to_dict_list(self) -> List[dict]:
|
| 186 |
+
"""Convert messages to list of dicts"""
|
| 187 |
+
return [msg.to_dict() for msg in self.messages]
|
app/tool/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.tool.base import BaseTool
|
| 2 |
+
from app.tool.bash import Bash
|
| 3 |
+
from app.tool.browser_use_tool import BrowserUseTool
|
| 4 |
+
from app.tool.crawl4ai import Crawl4aiTool
|
| 5 |
+
from app.tool.create_chat_completion import CreateChatCompletion
|
| 6 |
+
from app.tool.planning import PlanningTool
|
| 7 |
+
from app.tool.str_replace_editor import StrReplaceEditor
|
| 8 |
+
from app.tool.terminate import Terminate
|
| 9 |
+
from app.tool.tool_collection import ToolCollection
|
| 10 |
+
from app.tool.web_search import WebSearch
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"BaseTool",
|
| 15 |
+
"Bash",
|
| 16 |
+
"BrowserUseTool",
|
| 17 |
+
"Terminate",
|
| 18 |
+
"StrReplaceEditor",
|
| 19 |
+
"WebSearch",
|
| 20 |
+
"ToolCollection",
|
| 21 |
+
"CreateChatCompletion",
|
| 22 |
+
"PlanningTool",
|
| 23 |
+
"Crawl4aiTool",
|
| 24 |
+
]
|
app/tool/ask_human.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.tool import BaseTool
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class AskHuman(BaseTool):
|
| 5 |
+
"""Add a tool to ask human for help."""
|
| 6 |
+
|
| 7 |
+
name: str = "ask_human"
|
| 8 |
+
description: str = "Use this tool to ask human for help."
|
| 9 |
+
parameters: str = {
|
| 10 |
+
"type": "object",
|
| 11 |
+
"properties": {
|
| 12 |
+
"inquire": {
|
| 13 |
+
"type": "string",
|
| 14 |
+
"description": "The question you want to ask human.",
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"required": ["inquire"],
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
async def execute(self, inquire: str) -> str:
|
| 21 |
+
return input(f"""Bot: {inquire}\n\nYou: """).strip()
|
app/tool/base.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from abc import ABC, abstractmethod
|
| 3 |
+
from typing import Any, Dict, Optional, Union
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
|
| 7 |
+
from app.utils.logger import logger
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# class BaseTool(ABC, BaseModel):
|
| 11 |
+
# name: str
|
| 12 |
+
# description: str
|
| 13 |
+
# parameters: Optional[dict] = None
|
| 14 |
+
|
| 15 |
+
# class Config:
|
| 16 |
+
# arbitrary_types_allowed = True
|
| 17 |
+
|
| 18 |
+
# async def __call__(self, **kwargs) -> Any:
|
| 19 |
+
# """Execute the tool with given parameters."""
|
| 20 |
+
# return await self.execute(**kwargs)
|
| 21 |
+
|
| 22 |
+
# @abstractmethod
|
| 23 |
+
# async def execute(self, **kwargs) -> Any:
|
| 24 |
+
# """Execute the tool with given parameters."""
|
| 25 |
+
|
| 26 |
+
# def to_param(self) -> Dict:
|
| 27 |
+
# """Convert tool to function call format."""
|
| 28 |
+
# return {
|
| 29 |
+
# "type": "function",
|
| 30 |
+
# "function": {
|
| 31 |
+
# "name": self.name,
|
| 32 |
+
# "description": self.description,
|
| 33 |
+
# "parameters": self.parameters,
|
| 34 |
+
# },
|
| 35 |
+
# }
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class ToolResult(BaseModel):
|
| 39 |
+
"""Represents the result of a tool execution."""
|
| 40 |
+
|
| 41 |
+
output: Any = Field(default=None)
|
| 42 |
+
error: Optional[str] = Field(default=None)
|
| 43 |
+
base64_image: Optional[str] = Field(default=None)
|
| 44 |
+
system: Optional[str] = Field(default=None)
|
| 45 |
+
|
| 46 |
+
class Config:
|
| 47 |
+
arbitrary_types_allowed = True
|
| 48 |
+
|
| 49 |
+
def __bool__(self):
|
| 50 |
+
return any(getattr(self, field) for field in self.__fields__)
|
| 51 |
+
|
| 52 |
+
def __add__(self, other: "ToolResult"):
|
| 53 |
+
def combine_fields(
|
| 54 |
+
field: Optional[str], other_field: Optional[str], concatenate: bool = True
|
| 55 |
+
):
|
| 56 |
+
if field and other_field:
|
| 57 |
+
if concatenate:
|
| 58 |
+
return field + other_field
|
| 59 |
+
raise ValueError("Cannot combine tool results")
|
| 60 |
+
return field or other_field
|
| 61 |
+
|
| 62 |
+
return ToolResult(
|
| 63 |
+
output=combine_fields(self.output, other.output),
|
| 64 |
+
error=combine_fields(self.error, other.error),
|
| 65 |
+
base64_image=combine_fields(self.base64_image, other.base64_image, False),
|
| 66 |
+
system=combine_fields(self.system, other.system),
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
def __str__(self):
|
| 70 |
+
return f"Error: {self.error}" if self.error else self.output
|
| 71 |
+
|
| 72 |
+
def replace(self, **kwargs):
|
| 73 |
+
"""Returns a new ToolResult with the given fields replaced."""
|
| 74 |
+
# return self.copy(update=kwargs)
|
| 75 |
+
return type(self)(**{**self.dict(), **kwargs})
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class BaseTool(ABC, BaseModel):
|
| 79 |
+
"""Consolidated base class for all tools combining BaseModel and Tool functionality.
|
| 80 |
+
|
| 81 |
+
Provides:
|
| 82 |
+
- Pydantic model validation
|
| 83 |
+
- Schema registration
|
| 84 |
+
- Standardized result handling
|
| 85 |
+
- Abstract execution interface
|
| 86 |
+
|
| 87 |
+
Attributes:
|
| 88 |
+
name (str): Tool name
|
| 89 |
+
description (str): Tool description
|
| 90 |
+
parameters (dict): Tool parameters schema
|
| 91 |
+
_schemas (Dict[str, List[ToolSchema]]): Registered method schemas
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
name: str
|
| 95 |
+
description: str
|
| 96 |
+
parameters: Optional[dict] = None
|
| 97 |
+
# _schemas: Dict[str, List[ToolSchema]] = {}
|
| 98 |
+
|
| 99 |
+
class Config:
|
| 100 |
+
arbitrary_types_allowed = True
|
| 101 |
+
underscore_attrs_are_private = False
|
| 102 |
+
|
| 103 |
+
# def __init__(self, **data):
|
| 104 |
+
# """Initialize tool with model validation and schema registration."""
|
| 105 |
+
# super().__init__(**data)
|
| 106 |
+
# logger.debug(f"Initializing tool class: {self.__class__.__name__}")
|
| 107 |
+
# self._register_schemas()
|
| 108 |
+
|
| 109 |
+
# def _register_schemas(self):
|
| 110 |
+
# """Register schemas from all decorated methods."""
|
| 111 |
+
# for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
|
| 112 |
+
# if hasattr(method, 'tool_schemas'):
|
| 113 |
+
# self._schemas[name] = method.tool_schemas
|
| 114 |
+
# logger.debug(f"Registered schemas for method '{name}' in {self.__class__.__name__}")
|
| 115 |
+
|
| 116 |
+
async def __call__(self, **kwargs) -> Any:
|
| 117 |
+
"""Execute the tool with given parameters."""
|
| 118 |
+
return await self.execute(**kwargs)
|
| 119 |
+
|
| 120 |
+
@abstractmethod
|
| 121 |
+
async def execute(self, **kwargs) -> Any:
|
| 122 |
+
"""Execute the tool with given parameters."""
|
| 123 |
+
|
| 124 |
+
def to_param(self) -> Dict:
|
| 125 |
+
"""Convert tool to function call format.
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Dictionary with tool metadata in OpenAI function calling format
|
| 129 |
+
"""
|
| 130 |
+
return {
|
| 131 |
+
"type": "function",
|
| 132 |
+
"function": {
|
| 133 |
+
"name": self.name,
|
| 134 |
+
"description": self.description,
|
| 135 |
+
"parameters": self.parameters,
|
| 136 |
+
},
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
# def get_schemas(self) -> Dict[str, List[ToolSchema]]:
|
| 140 |
+
# """Get all registered tool schemas.
|
| 141 |
+
|
| 142 |
+
# Returns:
|
| 143 |
+
# Dict mapping method names to their schema definitions
|
| 144 |
+
# """
|
| 145 |
+
# return self._schemas
|
| 146 |
+
|
| 147 |
+
def success_response(self, data: Union[Dict[str, Any], str]) -> ToolResult:
|
| 148 |
+
"""Create a successful tool result.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
data: Result data (dictionary or string)
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
ToolResult with success=True and formatted output
|
| 155 |
+
"""
|
| 156 |
+
if isinstance(data, str):
|
| 157 |
+
text = data
|
| 158 |
+
else:
|
| 159 |
+
text = json.dumps(data, indent=2)
|
| 160 |
+
logger.debug(f"Created success response for {self.__class__.__name__}")
|
| 161 |
+
return ToolResult(output=text)
|
| 162 |
+
|
| 163 |
+
def fail_response(self, msg: str) -> ToolResult:
|
| 164 |
+
"""Create a failed tool result.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
msg: Error message describing the failure
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
ToolResult with success=False and error message
|
| 171 |
+
"""
|
| 172 |
+
logger.debug(f"Tool {self.__class__.__name__} returned failed result: {msg}")
|
| 173 |
+
return ToolResult(error=msg)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
class CLIResult(ToolResult):
|
| 177 |
+
"""A ToolResult that can be rendered as a CLI output."""
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class ToolFailure(ToolResult):
|
| 181 |
+
"""A ToolResult that represents a failure."""
|
app/tool/python_execute.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import multiprocessing
|
| 2 |
+
import sys
|
| 3 |
+
from io import StringIO
|
| 4 |
+
from typing import Dict
|
| 5 |
+
|
| 6 |
+
from app.tool.base import BaseTool
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class PythonExecute(BaseTool):
|
| 10 |
+
"""A tool for executing Python code with timeout and safety restrictions."""
|
| 11 |
+
|
| 12 |
+
name: str = "python_execute"
|
| 13 |
+
description: str = "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results."
|
| 14 |
+
parameters: dict = {
|
| 15 |
+
"type": "object",
|
| 16 |
+
"properties": {
|
| 17 |
+
"code": {
|
| 18 |
+
"type": "string",
|
| 19 |
+
"description": "The Python code to execute.",
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
"required": ["code"],
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
def _run_code(self, code: str, result_dict: dict, safe_globals: dict) -> None:
|
| 26 |
+
original_stdout = sys.stdout
|
| 27 |
+
try:
|
| 28 |
+
output_buffer = StringIO()
|
| 29 |
+
sys.stdout = output_buffer
|
| 30 |
+
exec(code, safe_globals, safe_globals)
|
| 31 |
+
result_dict["observation"] = output_buffer.getvalue()
|
| 32 |
+
result_dict["success"] = True
|
| 33 |
+
except Exception as e:
|
| 34 |
+
result_dict["observation"] = str(e)
|
| 35 |
+
result_dict["success"] = False
|
| 36 |
+
finally:
|
| 37 |
+
sys.stdout = original_stdout
|
| 38 |
+
|
| 39 |
+
async def execute(
|
| 40 |
+
self,
|
| 41 |
+
code: str,
|
| 42 |
+
timeout: int = 5,
|
| 43 |
+
) -> Dict:
|
| 44 |
+
"""
|
| 45 |
+
Executes the provided Python code with a timeout.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
code (str): The Python code to execute.
|
| 49 |
+
timeout (int): Execution timeout in seconds.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
Dict: Contains 'output' with execution output or error message and 'success' status.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
with multiprocessing.Manager() as manager:
|
| 56 |
+
result = manager.dict({"observation": "", "success": False})
|
| 57 |
+
if isinstance(__builtins__, dict):
|
| 58 |
+
safe_globals = {"__builtins__": __builtins__}
|
| 59 |
+
else:
|
| 60 |
+
safe_globals = {"__builtins__": __builtins__.__dict__.copy()}
|
| 61 |
+
proc = multiprocessing.Process(
|
| 62 |
+
target=self._run_code, args=(code, result, safe_globals)
|
| 63 |
+
)
|
| 64 |
+
proc.start()
|
| 65 |
+
proc.join(timeout)
|
| 66 |
+
|
| 67 |
+
# timeout process
|
| 68 |
+
if proc.is_alive():
|
| 69 |
+
proc.terminate()
|
| 70 |
+
proc.join(1)
|
| 71 |
+
return {
|
| 72 |
+
"observation": f"Execution timeout after {timeout} seconds",
|
| 73 |
+
"success": False,
|
| 74 |
+
}
|
| 75 |
+
return dict(result)
|
app/tool/str_replace_editor.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""File and directory manipulation tool with sandbox support."""
|
| 2 |
+
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any, DefaultDict, List, Literal, Optional, get_args
|
| 6 |
+
|
| 7 |
+
from app.config import config
|
| 8 |
+
from app.exceptions import ToolError
|
| 9 |
+
from app.tool import BaseTool
|
| 10 |
+
from app.tool.base import CLIResult, ToolResult
|
| 11 |
+
from app.tool.file_operators import (
|
| 12 |
+
FileOperator,
|
| 13 |
+
LocalFileOperator,
|
| 14 |
+
PathLike,
|
| 15 |
+
SandboxFileOperator,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
Command = Literal[
|
| 20 |
+
"view",
|
| 21 |
+
"create",
|
| 22 |
+
"str_replace",
|
| 23 |
+
"insert",
|
| 24 |
+
"undo_edit",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
# Constants
|
| 28 |
+
SNIPPET_LINES: int = 4
|
| 29 |
+
MAX_RESPONSE_LEN: int = 16000
|
| 30 |
+
TRUNCATED_MESSAGE: str = (
|
| 31 |
+
"<response clipped><NOTE>To save on context only part of this file has been shown to you. "
|
| 32 |
+
"You should retry this tool after you have searched inside the file with `grep -n` "
|
| 33 |
+
"in order to find the line numbers of what you are looking for.</NOTE>"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Tool description
|
| 37 |
+
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files
|
| 38 |
+
* State is persistent across command calls and discussions with the user
|
| 39 |
+
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
|
| 40 |
+
* The `create` command cannot be used if the specified `path` already exists as a file
|
| 41 |
+
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
|
| 42 |
+
* The `undo_edit` command will revert the last edit made to the file at `path`
|
| 43 |
+
|
| 44 |
+
Notes for using the `str_replace` command:
|
| 45 |
+
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
|
| 46 |
+
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
|
| 47 |
+
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def maybe_truncate(
|
| 52 |
+
content: str, truncate_after: Optional[int] = MAX_RESPONSE_LEN
|
| 53 |
+
) -> str:
|
| 54 |
+
"""Truncate content and append a notice if content exceeds the specified length."""
|
| 55 |
+
if not truncate_after or len(content) <= truncate_after:
|
| 56 |
+
return content
|
| 57 |
+
return content[:truncate_after] + TRUNCATED_MESSAGE
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class StrReplaceEditor(BaseTool):
|
| 61 |
+
"""A tool for viewing, creating, and editing files with sandbox support."""
|
| 62 |
+
|
| 63 |
+
name: str = "str_replace_editor"
|
| 64 |
+
description: str = _STR_REPLACE_EDITOR_DESCRIPTION
|
| 65 |
+
parameters: dict = {
|
| 66 |
+
"type": "object",
|
| 67 |
+
"properties": {
|
| 68 |
+
"command": {
|
| 69 |
+
"description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.",
|
| 70 |
+
"enum": ["view", "create", "str_replace", "insert", "undo_edit"],
|
| 71 |
+
"type": "string",
|
| 72 |
+
},
|
| 73 |
+
"path": {
|
| 74 |
+
"description": "Absolute path to file or directory.",
|
| 75 |
+
"type": "string",
|
| 76 |
+
},
|
| 77 |
+
"file_text": {
|
| 78 |
+
"description": "Required parameter of `create` command, with the content of the file to be created.",
|
| 79 |
+
"type": "string",
|
| 80 |
+
},
|
| 81 |
+
"old_str": {
|
| 82 |
+
"description": "Required parameter of `str_replace` command containing the string in `path` to replace.",
|
| 83 |
+
"type": "string",
|
| 84 |
+
},
|
| 85 |
+
"new_str": {
|
| 86 |
+
"description": "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.",
|
| 87 |
+
"type": "string",
|
| 88 |
+
},
|
| 89 |
+
"insert_line": {
|
| 90 |
+
"description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.",
|
| 91 |
+
"type": "integer",
|
| 92 |
+
},
|
| 93 |
+
"view_range": {
|
| 94 |
+
"description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
|
| 95 |
+
"items": {"type": "integer"},
|
| 96 |
+
"type": "array",
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
"required": ["command", "path"],
|
| 100 |
+
}
|
| 101 |
+
_file_history: DefaultDict[PathLike, List[str]] = defaultdict(list)
|
| 102 |
+
_local_operator: LocalFileOperator = LocalFileOperator()
|
| 103 |
+
_sandbox_operator: SandboxFileOperator = SandboxFileOperator()
|
| 104 |
+
|
| 105 |
+
# def _get_operator(self, use_sandbox: bool) -> FileOperator:
|
| 106 |
+
def _get_operator(self) -> FileOperator:
|
| 107 |
+
"""Get the appropriate file operator based on execution mode."""
|
| 108 |
+
return (
|
| 109 |
+
self._sandbox_operator
|
| 110 |
+
if config.sandbox.use_sandbox
|
| 111 |
+
else self._local_operator
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
async def execute(
|
| 115 |
+
self,
|
| 116 |
+
*,
|
| 117 |
+
command: Command,
|
| 118 |
+
path: str,
|
| 119 |
+
file_text: str | None = None,
|
| 120 |
+
view_range: list[int] | None = None,
|
| 121 |
+
old_str: str | None = None,
|
| 122 |
+
new_str: str | None = None,
|
| 123 |
+
insert_line: int | None = None,
|
| 124 |
+
**kwargs: Any,
|
| 125 |
+
) -> str:
|
| 126 |
+
"""Execute a file operation command."""
|
| 127 |
+
# Get the appropriate file operator
|
| 128 |
+
operator = self._get_operator()
|
| 129 |
+
|
| 130 |
+
# Validate path and command combination
|
| 131 |
+
await self.validate_path(command, Path(path), operator)
|
| 132 |
+
|
| 133 |
+
# Execute the appropriate command
|
| 134 |
+
if command == "view":
|
| 135 |
+
result = await self.view(path, view_range, operator)
|
| 136 |
+
elif command == "create":
|
| 137 |
+
if file_text is None:
|
| 138 |
+
raise ToolError("Parameter `file_text` is required for command: create")
|
| 139 |
+
await operator.write_file(path, file_text)
|
| 140 |
+
self._file_history[path].append(file_text)
|
| 141 |
+
result = ToolResult(output=f"File created successfully at: {path}")
|
| 142 |
+
elif command == "str_replace":
|
| 143 |
+
if old_str is None:
|
| 144 |
+
raise ToolError(
|
| 145 |
+
"Parameter `old_str` is required for command: str_replace"
|
| 146 |
+
)
|
| 147 |
+
result = await self.str_replace(path, old_str, new_str, operator)
|
| 148 |
+
elif command == "insert":
|
| 149 |
+
if insert_line is None:
|
| 150 |
+
raise ToolError(
|
| 151 |
+
"Parameter `insert_line` is required for command: insert"
|
| 152 |
+
)
|
| 153 |
+
if new_str is None:
|
| 154 |
+
raise ToolError("Parameter `new_str` is required for command: insert")
|
| 155 |
+
result = await self.insert(path, insert_line, new_str, operator)
|
| 156 |
+
elif command == "undo_edit":
|
| 157 |
+
result = await self.undo_edit(path, operator)
|
| 158 |
+
else:
|
| 159 |
+
# This should be caught by type checking, but we include it for safety
|
| 160 |
+
raise ToolError(
|
| 161 |
+
f'Unrecognized command {command}. The allowed commands for the {self.name} tool are: {", ".join(get_args(Command))}'
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
return str(result)
|
| 165 |
+
|
| 166 |
+
async def validate_path(
|
| 167 |
+
self, command: str, path: Path, operator: FileOperator
|
| 168 |
+
) -> None:
|
| 169 |
+
"""Validate path and command combination based on execution environment."""
|
| 170 |
+
# Check if path is absolute
|
| 171 |
+
if not path.is_absolute():
|
| 172 |
+
raise ToolError(f"The path {path} is not an absolute path")
|
| 173 |
+
|
| 174 |
+
# Only check if path exists for non-create commands
|
| 175 |
+
if command != "create":
|
| 176 |
+
if not await operator.exists(path):
|
| 177 |
+
raise ToolError(
|
| 178 |
+
f"The path {path} does not exist. Please provide a valid path."
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Check if path is a directory
|
| 182 |
+
is_dir = await operator.is_directory(path)
|
| 183 |
+
if is_dir and command != "view":
|
| 184 |
+
raise ToolError(
|
| 185 |
+
f"The path {path} is a directory and only the `view` command can be used on directories"
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Check if file exists for create command
|
| 189 |
+
elif command == "create":
|
| 190 |
+
exists = await operator.exists(path)
|
| 191 |
+
if exists:
|
| 192 |
+
raise ToolError(
|
| 193 |
+
f"File already exists at: {path}. Cannot overwrite files using command `create`."
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
async def view(
|
| 197 |
+
self,
|
| 198 |
+
path: PathLike,
|
| 199 |
+
view_range: Optional[List[int]] = None,
|
| 200 |
+
operator: FileOperator = None,
|
| 201 |
+
) -> CLIResult:
|
| 202 |
+
"""Display file or directory content."""
|
| 203 |
+
# Determine if path is a directory
|
| 204 |
+
is_dir = await operator.is_directory(path)
|
| 205 |
+
|
| 206 |
+
if is_dir:
|
| 207 |
+
# Directory handling
|
| 208 |
+
if view_range:
|
| 209 |
+
raise ToolError(
|
| 210 |
+
"The `view_range` parameter is not allowed when `path` points to a directory."
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
return await self._view_directory(path, operator)
|
| 214 |
+
else:
|
| 215 |
+
# File handling
|
| 216 |
+
return await self._view_file(path, operator, view_range)
|
| 217 |
+
|
| 218 |
+
@staticmethod
|
| 219 |
+
async def _view_directory(path: PathLike, operator: FileOperator) -> CLIResult:
|
| 220 |
+
"""Display directory contents."""
|
| 221 |
+
find_cmd = f"find {path} -maxdepth 2 -not -path '*/\\.*'"
|
| 222 |
+
|
| 223 |
+
# Execute command using the operator
|
| 224 |
+
returncode, stdout, stderr = await operator.run_command(find_cmd)
|
| 225 |
+
|
| 226 |
+
if not stderr:
|
| 227 |
+
stdout = (
|
| 228 |
+
f"Here's the files and directories up to 2 levels deep in {path}, "
|
| 229 |
+
f"excluding hidden items:\n{stdout}\n"
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
return CLIResult(output=stdout, error=stderr)
|
| 233 |
+
|
| 234 |
+
async def _view_file(
|
| 235 |
+
self,
|
| 236 |
+
path: PathLike,
|
| 237 |
+
operator: FileOperator,
|
| 238 |
+
view_range: Optional[List[int]] = None,
|
| 239 |
+
) -> CLIResult:
|
| 240 |
+
"""Display file content, optionally within a specified line range."""
|
| 241 |
+
# Read file content
|
| 242 |
+
file_content = await operator.read_file(path)
|
| 243 |
+
init_line = 1
|
| 244 |
+
|
| 245 |
+
# Apply view range if specified
|
| 246 |
+
if view_range:
|
| 247 |
+
if len(view_range) != 2 or not all(isinstance(i, int) for i in view_range):
|
| 248 |
+
raise ToolError(
|
| 249 |
+
"Invalid `view_range`. It should be a list of two integers."
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
file_lines = file_content.split("\n")
|
| 253 |
+
n_lines_file = len(file_lines)
|
| 254 |
+
init_line, final_line = view_range
|
| 255 |
+
|
| 256 |
+
# Validate view range
|
| 257 |
+
if init_line < 1 or init_line > n_lines_file:
|
| 258 |
+
raise ToolError(
|
| 259 |
+
f"Invalid `view_range`: {view_range}. Its first element `{init_line}` should be "
|
| 260 |
+
f"within the range of lines of the file: {[1, n_lines_file]}"
|
| 261 |
+
)
|
| 262 |
+
if final_line > n_lines_file:
|
| 263 |
+
raise ToolError(
|
| 264 |
+
f"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be "
|
| 265 |
+
f"smaller than the number of lines in the file: `{n_lines_file}`"
|
| 266 |
+
)
|
| 267 |
+
if final_line != -1 and final_line < init_line:
|
| 268 |
+
raise ToolError(
|
| 269 |
+
f"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be "
|
| 270 |
+
f"larger or equal than its first `{init_line}`"
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# Apply range
|
| 274 |
+
if final_line == -1:
|
| 275 |
+
file_content = "\n".join(file_lines[init_line - 1 :])
|
| 276 |
+
else:
|
| 277 |
+
file_content = "\n".join(file_lines[init_line - 1 : final_line])
|
| 278 |
+
|
| 279 |
+
# Format and return result
|
| 280 |
+
return CLIResult(
|
| 281 |
+
output=self._make_output(file_content, str(path), init_line=init_line)
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
async def str_replace(
|
| 285 |
+
self,
|
| 286 |
+
path: PathLike,
|
| 287 |
+
old_str: str,
|
| 288 |
+
new_str: Optional[str] = None,
|
| 289 |
+
operator: FileOperator = None,
|
| 290 |
+
) -> CLIResult:
|
| 291 |
+
"""Replace a unique string in a file with a new string."""
|
| 292 |
+
# Read file content and expand tabs
|
| 293 |
+
file_content = (await operator.read_file(path)).expandtabs()
|
| 294 |
+
old_str = old_str.expandtabs()
|
| 295 |
+
new_str = new_str.expandtabs() if new_str is not None else ""
|
| 296 |
+
|
| 297 |
+
# Check if old_str is unique in the file
|
| 298 |
+
occurrences = file_content.count(old_str)
|
| 299 |
+
if occurrences == 0:
|
| 300 |
+
raise ToolError(
|
| 301 |
+
f"No replacement was performed, old_str `{old_str}` did not appear verbatim in {path}."
|
| 302 |
+
)
|
| 303 |
+
elif occurrences > 1:
|
| 304 |
+
# Find line numbers of occurrences
|
| 305 |
+
file_content_lines = file_content.split("\n")
|
| 306 |
+
lines = [
|
| 307 |
+
idx + 1
|
| 308 |
+
for idx, line in enumerate(file_content_lines)
|
| 309 |
+
if old_str in line
|
| 310 |
+
]
|
| 311 |
+
raise ToolError(
|
| 312 |
+
f"No replacement was performed. Multiple occurrences of old_str `{old_str}` "
|
| 313 |
+
f"in lines {lines}. Please ensure it is unique"
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
# Replace old_str with new_str
|
| 317 |
+
new_file_content = file_content.replace(old_str, new_str)
|
| 318 |
+
|
| 319 |
+
# Write the new content to the file
|
| 320 |
+
await operator.write_file(path, new_file_content)
|
| 321 |
+
|
| 322 |
+
# Save the original content to history
|
| 323 |
+
self._file_history[path].append(file_content)
|
| 324 |
+
|
| 325 |
+
# Create a snippet of the edited section
|
| 326 |
+
replacement_line = file_content.split(old_str)[0].count("\n")
|
| 327 |
+
start_line = max(0, replacement_line - SNIPPET_LINES)
|
| 328 |
+
end_line = replacement_line + SNIPPET_LINES + new_str.count("\n")
|
| 329 |
+
snippet = "\n".join(new_file_content.split("\n")[start_line : end_line + 1])
|
| 330 |
+
|
| 331 |
+
# Prepare the success message
|
| 332 |
+
success_msg = f"The file {path} has been edited. "
|
| 333 |
+
success_msg += self._make_output(
|
| 334 |
+
snippet, f"a snippet of {path}", start_line + 1
|
| 335 |
+
)
|
| 336 |
+
success_msg += "Review the changes and make sure they are as expected. Edit the file again if necessary."
|
| 337 |
+
|
| 338 |
+
return CLIResult(output=success_msg)
|
| 339 |
+
|
| 340 |
+
async def insert(
|
| 341 |
+
self,
|
| 342 |
+
path: PathLike,
|
| 343 |
+
insert_line: int,
|
| 344 |
+
new_str: str,
|
| 345 |
+
operator: FileOperator = None,
|
| 346 |
+
) -> CLIResult:
|
| 347 |
+
"""Insert text at a specific line in a file."""
|
| 348 |
+
# Read and prepare content
|
| 349 |
+
file_text = (await operator.read_file(path)).expandtabs()
|
| 350 |
+
new_str = new_str.expandtabs()
|
| 351 |
+
file_text_lines = file_text.split("\n")
|
| 352 |
+
n_lines_file = len(file_text_lines)
|
| 353 |
+
|
| 354 |
+
# Validate insert_line
|
| 355 |
+
if insert_line < 0 or insert_line > n_lines_file:
|
| 356 |
+
raise ToolError(
|
| 357 |
+
f"Invalid `insert_line` parameter: {insert_line}. It should be within "
|
| 358 |
+
f"the range of lines of the file: {[0, n_lines_file]}"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
# Perform insertion
|
| 362 |
+
new_str_lines = new_str.split("\n")
|
| 363 |
+
new_file_text_lines = (
|
| 364 |
+
file_text_lines[:insert_line]
|
| 365 |
+
+ new_str_lines
|
| 366 |
+
+ file_text_lines[insert_line:]
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# Create a snippet for preview
|
| 370 |
+
snippet_lines = (
|
| 371 |
+
file_text_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]
|
| 372 |
+
+ new_str_lines
|
| 373 |
+
+ file_text_lines[insert_line : insert_line + SNIPPET_LINES]
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
# Join lines and write to file
|
| 377 |
+
new_file_text = "\n".join(new_file_text_lines)
|
| 378 |
+
snippet = "\n".join(snippet_lines)
|
| 379 |
+
|
| 380 |
+
await operator.write_file(path, new_file_text)
|
| 381 |
+
self._file_history[path].append(file_text)
|
| 382 |
+
|
| 383 |
+
# Prepare success message
|
| 384 |
+
success_msg = f"The file {path} has been edited. "
|
| 385 |
+
success_msg += self._make_output(
|
| 386 |
+
snippet,
|
| 387 |
+
"a snippet of the edited file",
|
| 388 |
+
max(1, insert_line - SNIPPET_LINES + 1),
|
| 389 |
+
)
|
| 390 |
+
success_msg += "Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary."
|
| 391 |
+
|
| 392 |
+
return CLIResult(output=success_msg)
|
| 393 |
+
|
| 394 |
+
async def undo_edit(
|
| 395 |
+
self, path: PathLike, operator: FileOperator = None
|
| 396 |
+
) -> CLIResult:
|
| 397 |
+
"""Revert the last edit made to a file."""
|
| 398 |
+
if not self._file_history[path]:
|
| 399 |
+
raise ToolError(f"No edit history found for {path}.")
|
| 400 |
+
|
| 401 |
+
old_text = self._file_history[path].pop()
|
| 402 |
+
await operator.write_file(path, old_text)
|
| 403 |
+
|
| 404 |
+
return CLIResult(
|
| 405 |
+
output=f"Last edit to {path} undone successfully. {self._make_output(old_text, str(path))}"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
def _make_output(
|
| 409 |
+
self,
|
| 410 |
+
file_content: str,
|
| 411 |
+
file_descriptor: str,
|
| 412 |
+
init_line: int = 1,
|
| 413 |
+
expand_tabs: bool = True,
|
| 414 |
+
) -> str:
|
| 415 |
+
"""Format file content for display with line numbers."""
|
| 416 |
+
file_content = maybe_truncate(file_content)
|
| 417 |
+
if expand_tabs:
|
| 418 |
+
file_content = file_content.expandtabs()
|
| 419 |
+
|
| 420 |
+
# Add line numbers to each line
|
| 421 |
+
file_content = "\n".join(
|
| 422 |
+
[
|
| 423 |
+
f"{i + init_line:6}\t{line}"
|
| 424 |
+
for i, line in enumerate(file_content.split("\n"))
|
| 425 |
+
]
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
return (
|
| 429 |
+
f"Here's the result of running `cat -n` on {file_descriptor}:\n"
|
| 430 |
+
+ file_content
|
| 431 |
+
+ "\n"
|
| 432 |
+
)
|
app/tool/terminate.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.tool.base import BaseTool
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.
|
| 5 |
+
When you have finished all the tasks, call this tool to end the work."""
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Terminate(BaseTool):
|
| 9 |
+
name: str = "terminate"
|
| 10 |
+
description: str = _TERMINATE_DESCRIPTION
|
| 11 |
+
parameters: dict = {
|
| 12 |
+
"type": "object",
|
| 13 |
+
"properties": {
|
| 14 |
+
"status": {
|
| 15 |
+
"type": "string",
|
| 16 |
+
"description": "The finish status of the interaction.",
|
| 17 |
+
"enum": ["success", "failure"],
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"required": ["status"],
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async def execute(self, status: str) -> str:
|
| 24 |
+
"""Finish the current execution"""
|
| 25 |
+
return f"The interaction has been completed with status: {status}"
|
app/tool/tool_collection.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Collection classes for managing multiple tools."""
|
| 2 |
+
from typing import Any, Dict, List
|
| 3 |
+
|
| 4 |
+
from app.exceptions import ToolError
|
| 5 |
+
from app.logger import logger
|
| 6 |
+
from app.tool.base import BaseTool, ToolFailure, ToolResult
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ToolCollection:
|
| 10 |
+
"""A collection of defined tools."""
|
| 11 |
+
|
| 12 |
+
class Config:
|
| 13 |
+
arbitrary_types_allowed = True
|
| 14 |
+
|
| 15 |
+
def __init__(self, *tools: BaseTool):
|
| 16 |
+
self.tools = tools
|
| 17 |
+
self.tool_map = {tool.name: tool for tool in tools}
|
| 18 |
+
|
| 19 |
+
def __iter__(self):
|
| 20 |
+
return iter(self.tools)
|
| 21 |
+
|
| 22 |
+
def to_params(self) -> List[Dict[str, Any]]:
|
| 23 |
+
return [tool.to_param() for tool in self.tools]
|
| 24 |
+
|
| 25 |
+
async def execute(
|
| 26 |
+
self, *, name: str, tool_input: Dict[str, Any] = None
|
| 27 |
+
) -> ToolResult:
|
| 28 |
+
tool = self.tool_map.get(name)
|
| 29 |
+
if not tool:
|
| 30 |
+
return ToolFailure(error=f"Tool {name} is invalid")
|
| 31 |
+
try:
|
| 32 |
+
result = await tool(**tool_input)
|
| 33 |
+
return result
|
| 34 |
+
except ToolError as e:
|
| 35 |
+
return ToolFailure(error=e.message)
|
| 36 |
+
|
| 37 |
+
async def execute_all(self) -> List[ToolResult]:
|
| 38 |
+
"""Execute all tools in the collection sequentially."""
|
| 39 |
+
results = []
|
| 40 |
+
for tool in self.tools:
|
| 41 |
+
try:
|
| 42 |
+
result = await tool()
|
| 43 |
+
results.append(result)
|
| 44 |
+
except ToolError as e:
|
| 45 |
+
results.append(ToolFailure(error=e.message))
|
| 46 |
+
return results
|
| 47 |
+
|
| 48 |
+
def get_tool(self, name: str) -> BaseTool:
|
| 49 |
+
return self.tool_map.get(name)
|
| 50 |
+
|
| 51 |
+
def add_tool(self, tool: BaseTool):
|
| 52 |
+
"""Add a single tool to the collection.
|
| 53 |
+
|
| 54 |
+
If a tool with the same name already exists, it will be skipped and a warning will be logged.
|
| 55 |
+
"""
|
| 56 |
+
if tool.name in self.tool_map:
|
| 57 |
+
logger.warning(f"Tool {tool.name} already exists in collection, skipping")
|
| 58 |
+
return self
|
| 59 |
+
|
| 60 |
+
self.tools += (tool,)
|
| 61 |
+
self.tool_map[tool.name] = tool
|
| 62 |
+
return self
|
| 63 |
+
|
| 64 |
+
def add_tools(self, *tools: BaseTool):
|
| 65 |
+
"""Add multiple tools to the collection.
|
| 66 |
+
|
| 67 |
+
If any tool has a name conflict with an existing tool, it will be skipped and a warning will be logged.
|
| 68 |
+
"""
|
| 69 |
+
for tool in tools:
|
| 70 |
+
self.add_tool(tool)
|
| 71 |
+
return self
|
app/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Utility functions and constants for agent tools
|
app/utils/logger.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
import structlog
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
ENV_MODE = os.getenv("ENV_MODE", "LOCAL")
|
| 8 |
+
|
| 9 |
+
renderer = [structlog.processors.JSONRenderer()]
|
| 10 |
+
if ENV_MODE.lower() == "local".lower():
|
| 11 |
+
renderer = [structlog.dev.ConsoleRenderer()]
|
| 12 |
+
|
| 13 |
+
structlog.configure(
|
| 14 |
+
processors=[
|
| 15 |
+
structlog.stdlib.add_log_level,
|
| 16 |
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
| 17 |
+
structlog.processors.dict_tracebacks,
|
| 18 |
+
structlog.processors.CallsiteParameterAdder(
|
| 19 |
+
{
|
| 20 |
+
structlog.processors.CallsiteParameter.FILENAME,
|
| 21 |
+
structlog.processors.CallsiteParameter.FUNC_NAME,
|
| 22 |
+
structlog.processors.CallsiteParameter.LINENO,
|
| 23 |
+
}
|
| 24 |
+
),
|
| 25 |
+
structlog.processors.TimeStamper(fmt="iso"),
|
| 26 |
+
structlog.contextvars.merge_contextvars,
|
| 27 |
+
*renderer,
|
| 28 |
+
],
|
| 29 |
+
cache_logger_on_first_use=True,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
logger: structlog.stdlib.BoundLogger = structlog.get_logger(level=logging.DEBUG)
|
app_complete.py
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import sqlite3
|
| 5 |
+
import hashlib
|
| 6 |
+
import datetime
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# Cloudflare configuration
|
| 10 |
+
CLOUDFLARE_CONFIG = {
|
| 11 |
+
"api_token": os.getenv("CLOUDFLARE_API_TOKEN", ""),
|
| 12 |
+
"account_id": os.getenv("CLOUDFLARE_ACCOUNT_ID", ""),
|
| 13 |
+
"d1_database_id": os.getenv("CLOUDFLARE_D1_DATABASE_ID", ""),
|
| 14 |
+
"r2_bucket_name": os.getenv("CLOUDFLARE_R2_BUCKET_NAME", ""),
|
| 15 |
+
"kv_namespace_id": os.getenv("CLOUDFLARE_KV_NAMESPACE_ID", ""),
|
| 16 |
+
"durable_objects_id": os.getenv("CLOUDFLARE_DURABLE_OBJECTS_ID", ""),
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
# AI Model Categories with 200+ models
|
| 20 |
+
AI_MODELS = {
|
| 21 |
+
"Text Generation": {
|
| 22 |
+
"Qwen Models": [
|
| 23 |
+
"Qwen/Qwen2.5-72B-Instruct",
|
| 24 |
+
"Qwen/Qwen2.5-32B-Instruct",
|
| 25 |
+
"Qwen/Qwen2.5-14B-Instruct",
|
| 26 |
+
"Qwen/Qwen2.5-7B-Instruct",
|
| 27 |
+
"Qwen/Qwen2.5-3B-Instruct",
|
| 28 |
+
"Qwen/Qwen2.5-1.5B-Instruct",
|
| 29 |
+
"Qwen/Qwen2.5-0.5B-Instruct",
|
| 30 |
+
"Qwen/Qwen2-72B-Instruct",
|
| 31 |
+
"Qwen/Qwen2-57B-A14B-Instruct",
|
| 32 |
+
"Qwen/Qwen2-7B-Instruct",
|
| 33 |
+
"Qwen/Qwen2-1.5B-Instruct",
|
| 34 |
+
"Qwen/Qwen2-0.5B-Instruct",
|
| 35 |
+
"Qwen/Qwen1.5-110B-Chat",
|
| 36 |
+
"Qwen/Qwen1.5-72B-Chat",
|
| 37 |
+
"Qwen/Qwen1.5-32B-Chat",
|
| 38 |
+
"Qwen/Qwen1.5-14B-Chat",
|
| 39 |
+
"Qwen/Qwen1.5-7B-Chat",
|
| 40 |
+
"Qwen/Qwen1.5-4B-Chat",
|
| 41 |
+
"Qwen/Qwen1.5-1.8B-Chat",
|
| 42 |
+
"Qwen/Qwen1.5-0.5B-Chat",
|
| 43 |
+
"Qwen/CodeQwen1.5-7B-Chat",
|
| 44 |
+
"Qwen/Qwen2.5-Math-72B-Instruct",
|
| 45 |
+
"Qwen/Qwen2.5-Math-7B-Instruct",
|
| 46 |
+
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 47 |
+
"Qwen/Qwen2.5-Coder-14B-Instruct",
|
| 48 |
+
"Qwen/Qwen2.5-Coder-7B-Instruct",
|
| 49 |
+
"Qwen/Qwen2.5-Coder-3B-Instruct",
|
| 50 |
+
"Qwen/Qwen2.5-Coder-1.5B-Instruct",
|
| 51 |
+
"Qwen/Qwen2.5-Coder-0.5B-Instruct",
|
| 52 |
+
"Qwen/QwQ-32B-Preview",
|
| 53 |
+
"Qwen/Qwen2-VL-72B-Instruct",
|
| 54 |
+
"Qwen/Qwen2-VL-7B-Instruct",
|
| 55 |
+
"Qwen/Qwen2-VL-2B-Instruct",
|
| 56 |
+
"Qwen/Qwen2-Audio-7B-Instruct",
|
| 57 |
+
"Qwen/Qwen-Agent-Chat",
|
| 58 |
+
"Qwen/Qwen-VL-Chat",
|
| 59 |
+
],
|
| 60 |
+
"DeepSeek Models": [
|
| 61 |
+
"deepseek-ai/deepseek-llm-67b-chat",
|
| 62 |
+
"deepseek-ai/deepseek-llm-7b-chat",
|
| 63 |
+
"deepseek-ai/deepseek-coder-33b-instruct",
|
| 64 |
+
"deepseek-ai/deepseek-coder-7b-instruct",
|
| 65 |
+
"deepseek-ai/deepseek-coder-6.7b-instruct",
|
| 66 |
+
"deepseek-ai/deepseek-coder-1.3b-instruct",
|
| 67 |
+
"deepseek-ai/DeepSeek-V2-Chat",
|
| 68 |
+
"deepseek-ai/DeepSeek-V2-Lite-Chat",
|
| 69 |
+
"deepseek-ai/deepseek-math-7b-instruct",
|
| 70 |
+
"deepseek-ai/deepseek-moe-16b-chat",
|
| 71 |
+
"deepseek-ai/deepseek-vl-7b-chat",
|
| 72 |
+
"deepseek-ai/deepseek-vl-1.3b-chat",
|
| 73 |
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
|
| 74 |
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B",
|
| 75 |
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
|
| 76 |
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-8B",
|
| 77 |
+
"deepseek-ai/DeepSeek-Reasoner-R1",
|
| 78 |
+
],
|
| 79 |
+
},
|
| 80 |
+
"Image Processing": {
|
| 81 |
+
"Image Generation": [
|
| 82 |
+
"black-forest-labs/FLUX.1-dev",
|
| 83 |
+
"black-forest-labs/FLUX.1-schnell",
|
| 84 |
+
"black-forest-labs/FLUX.1-pro",
|
| 85 |
+
"runwayml/stable-diffusion-v1-5",
|
| 86 |
+
"stabilityai/stable-diffusion-xl-base-1.0",
|
| 87 |
+
"stabilityai/stable-diffusion-3-medium-diffusers",
|
| 88 |
+
"stabilityai/sd-turbo",
|
| 89 |
+
"kandinsky-community/kandinsky-2-2-decoder",
|
| 90 |
+
"playgroundai/playground-v2.5-1024px-aesthetic",
|
| 91 |
+
"midjourney/midjourney-v6",
|
| 92 |
+
],
|
| 93 |
+
"Image Editing": [
|
| 94 |
+
"timbrooks/instruct-pix2pix",
|
| 95 |
+
"runwayml/stable-diffusion-inpainting",
|
| 96 |
+
"stabilityai/stable-diffusion-xl-refiner-1.0",
|
| 97 |
+
"lllyasviel/control_v11p_sd15_inpaint",
|
| 98 |
+
"SG161222/RealVisXL_V4.0",
|
| 99 |
+
"ByteDance/SDXL-Lightning",
|
| 100 |
+
"segmind/SSD-1B",
|
| 101 |
+
"segmind/Segmind-Vega",
|
| 102 |
+
"playgroundai/playground-v2-1024px-aesthetic",
|
| 103 |
+
"stabilityai/stable-cascade",
|
| 104 |
+
],
|
| 105 |
+
"Face Processing": [
|
| 106 |
+
"InsightFace/inswapper_128.onnx",
|
| 107 |
+
"deepinsight/insightface",
|
| 108 |
+
"TencentARC/GFPGAN",
|
| 109 |
+
"sczhou/CodeFormer",
|
| 110 |
+
"xinntao/Real-ESRGAN",
|
| 111 |
+
"ESRGAN/ESRGAN",
|
| 112 |
+
],
|
| 113 |
+
},
|
| 114 |
+
"Audio Processing": {
|
| 115 |
+
"Text-to-Speech": [
|
| 116 |
+
"microsoft/speecht5_tts",
|
| 117 |
+
"facebook/mms-tts-eng",
|
| 118 |
+
"facebook/mms-tts-ara",
|
| 119 |
+
"coqui/XTTS-v2",
|
| 120 |
+
"suno/bark",
|
| 121 |
+
"parler-tts/parler-tts-large-v1",
|
| 122 |
+
"microsoft/DisTTS",
|
| 123 |
+
"facebook/fastspeech2-en-ljspeech",
|
| 124 |
+
"espnet/kan-bayashi_ljspeech_vits",
|
| 125 |
+
"facebook/tts_transformer-en-ljspeech",
|
| 126 |
+
"microsoft/SpeechT5",
|
| 127 |
+
"Voicemod/fastspeech2-en-male1",
|
| 128 |
+
"facebook/mms-tts-spa",
|
| 129 |
+
"facebook/mms-tts-fra",
|
| 130 |
+
"facebook/mms-tts-deu",
|
| 131 |
+
],
|
| 132 |
+
"Speech-to-Text": [
|
| 133 |
+
"openai/whisper-large-v3",
|
| 134 |
+
"openai/whisper-large-v2",
|
| 135 |
+
"openai/whisper-medium",
|
| 136 |
+
"openai/whisper-small",
|
| 137 |
+
"openai/whisper-base",
|
| 138 |
+
"openai/whisper-tiny",
|
| 139 |
+
"facebook/wav2vec2-large-960h",
|
| 140 |
+
"facebook/wav2vec2-base-960h",
|
| 141 |
+
"microsoft/unispeech-sat-large",
|
| 142 |
+
"nvidia/stt_en_conformer_ctc_large",
|
| 143 |
+
"speechbrain/asr-wav2vec2-commonvoice-en",
|
| 144 |
+
"facebook/mms-1b-all",
|
| 145 |
+
"facebook/seamless-m4t-v2-large",
|
| 146 |
+
"distil-whisper/distil-large-v3",
|
| 147 |
+
"distil-whisper/distil-medium.en",
|
| 148 |
+
],
|
| 149 |
+
},
|
| 150 |
+
"Multimodal AI": {
|
| 151 |
+
"Vision-Language": [
|
| 152 |
+
"microsoft/DialoGPT-large",
|
| 153 |
+
"microsoft/blip-image-captioning-large",
|
| 154 |
+
"microsoft/blip2-opt-6.7b",
|
| 155 |
+
"microsoft/blip2-flan-t5-xl",
|
| 156 |
+
"salesforce/blip-vqa-capfilt-large",
|
| 157 |
+
"dandelin/vilt-b32-finetuned-vqa",
|
| 158 |
+
"google/pix2struct-ai2d-base",
|
| 159 |
+
"microsoft/git-large-coco",
|
| 160 |
+
"microsoft/git-base-vqa",
|
| 161 |
+
"liuhaotian/llava-v1.6-34b",
|
| 162 |
+
"liuhaotian/llava-v1.6-vicuna-7b",
|
| 163 |
+
],
|
| 164 |
+
"Talking Avatars": [
|
| 165 |
+
"microsoft/SpeechT5-TTS-Avatar",
|
| 166 |
+
"Wav2Lip-HD",
|
| 167 |
+
"First-Order-Model",
|
| 168 |
+
"LipSync-Expert",
|
| 169 |
+
"DeepFaceLive",
|
| 170 |
+
"FaceSwapper-Live",
|
| 171 |
+
"RealTime-FaceRig",
|
| 172 |
+
"AI-Avatar-Generator",
|
| 173 |
+
"TalkingHead-3D",
|
| 174 |
+
],
|
| 175 |
+
},
|
| 176 |
+
"Arabic-English Models": [
|
| 177 |
+
"aubmindlab/bert-base-arabertv2",
|
| 178 |
+
"aubmindlab/aragpt2-base",
|
| 179 |
+
"aubmindlab/aragpt2-medium",
|
| 180 |
+
"CAMeL-Lab/bert-base-arabic-camelbert-mix",
|
| 181 |
+
"asafaya/bert-base-arabic",
|
| 182 |
+
"UBC-NLP/MARBERT",
|
| 183 |
+
"UBC-NLP/ARBERTv2",
|
| 184 |
+
"facebook/nllb-200-3.3B",
|
| 185 |
+
"facebook/m2m100_1.2B",
|
| 186 |
+
"Helsinki-NLP/opus-mt-ar-en",
|
| 187 |
+
"Helsinki-NLP/opus-mt-en-ar",
|
| 188 |
+
"microsoft/DialoGPT-medium-arabic",
|
| 189 |
+
],
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def init_database():
|
| 194 |
+
"""Initialize SQLite database for authentication"""
|
| 195 |
+
db_path = Path("openmanus.db")
|
| 196 |
+
conn = sqlite3.connect(db_path)
|
| 197 |
+
cursor = conn.cursor()
|
| 198 |
+
|
| 199 |
+
# Create users table
|
| 200 |
+
cursor.execute(
|
| 201 |
+
"""
|
| 202 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 203 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 204 |
+
mobile_number TEXT UNIQUE NOT NULL,
|
| 205 |
+
full_name TEXT NOT NULL,
|
| 206 |
+
password_hash TEXT NOT NULL,
|
| 207 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 208 |
+
last_login TIMESTAMP,
|
| 209 |
+
is_active BOOLEAN DEFAULT 1
|
| 210 |
+
)
|
| 211 |
+
"""
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Create sessions table
|
| 215 |
+
cursor.execute(
|
| 216 |
+
"""
|
| 217 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 218 |
+
id TEXT PRIMARY KEY,
|
| 219 |
+
user_id INTEGER NOT NULL,
|
| 220 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 221 |
+
expires_at TIMESTAMP NOT NULL,
|
| 222 |
+
ip_address TEXT,
|
| 223 |
+
user_agent TEXT,
|
| 224 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 225 |
+
)
|
| 226 |
+
"""
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# Create model usage table
|
| 230 |
+
cursor.execute(
|
| 231 |
+
"""
|
| 232 |
+
CREATE TABLE IF NOT EXISTS model_usage (
|
| 233 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 234 |
+
user_id INTEGER,
|
| 235 |
+
model_name TEXT NOT NULL,
|
| 236 |
+
category TEXT NOT NULL,
|
| 237 |
+
input_text TEXT,
|
| 238 |
+
output_text TEXT,
|
| 239 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 240 |
+
processing_time REAL,
|
| 241 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 242 |
+
)
|
| 243 |
+
"""
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
conn.commit()
|
| 247 |
+
conn.close()
|
| 248 |
+
return True
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def hash_password(password):
|
| 252 |
+
"""Hash password using SHA-256"""
|
| 253 |
+
return hashlib.sha256(password.encode()).hexdigest()
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def signup_user(mobile, name, password, confirm_password):
|
| 257 |
+
"""User registration with mobile number"""
|
| 258 |
+
if not all([mobile, name, password, confirm_password]):
|
| 259 |
+
return "❌ Please fill in all fields"
|
| 260 |
+
|
| 261 |
+
if password != confirm_password:
|
| 262 |
+
return "❌ Passwords do not match"
|
| 263 |
+
|
| 264 |
+
if len(password) < 6:
|
| 265 |
+
return "❌ Password must be at least 6 characters"
|
| 266 |
+
|
| 267 |
+
# Validate mobile number
|
| 268 |
+
if not mobile.replace("+", "").replace("-", "").replace(" ", "").isdigit():
|
| 269 |
+
return "❌ Please enter a valid mobile number"
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
conn = sqlite3.connect("openmanus.db")
|
| 273 |
+
cursor = conn.cursor()
|
| 274 |
+
|
| 275 |
+
# Check if mobile number already exists
|
| 276 |
+
cursor.execute("SELECT id FROM users WHERE mobile_number = ?", (mobile,))
|
| 277 |
+
if cursor.fetchone():
|
| 278 |
+
conn.close()
|
| 279 |
+
return "❌ Mobile number already registered"
|
| 280 |
+
|
| 281 |
+
# Create new user
|
| 282 |
+
password_hash = hash_password(password)
|
| 283 |
+
cursor.execute(
|
| 284 |
+
"""
|
| 285 |
+
INSERT INTO users (mobile_number, full_name, password_hash)
|
| 286 |
+
VALUES (?, ?, ?)
|
| 287 |
+
""",
|
| 288 |
+
(mobile, name, password_hash),
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
conn.commit()
|
| 292 |
+
conn.close()
|
| 293 |
+
|
| 294 |
+
return f"✅ Account created successfully for {name}! Welcome to OpenManus Platform."
|
| 295 |
+
|
| 296 |
+
except Exception as e:
|
| 297 |
+
return f"❌ Registration failed: {str(e)}"
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def login_user(mobile, password):
|
| 301 |
+
"""User authentication"""
|
| 302 |
+
if not mobile or not password:
|
| 303 |
+
return "❌ Please provide mobile number and password"
|
| 304 |
+
|
| 305 |
+
try:
|
| 306 |
+
conn = sqlite3.connect("openmanus.db")
|
| 307 |
+
cursor = conn.cursor()
|
| 308 |
+
|
| 309 |
+
# Verify credentials
|
| 310 |
+
password_hash = hash_password(password)
|
| 311 |
+
cursor.execute(
|
| 312 |
+
"""
|
| 313 |
+
SELECT id, full_name FROM users
|
| 314 |
+
WHERE mobile_number = ? AND password_hash = ? AND is_active = 1
|
| 315 |
+
""",
|
| 316 |
+
(mobile, password_hash),
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
user = cursor.fetchone()
|
| 320 |
+
if user:
|
| 321 |
+
# Update last login
|
| 322 |
+
cursor.execute(
|
| 323 |
+
"""
|
| 324 |
+
UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?
|
| 325 |
+
""",
|
| 326 |
+
(user[0],),
|
| 327 |
+
)
|
| 328 |
+
conn.commit()
|
| 329 |
+
conn.close()
|
| 330 |
+
|
| 331 |
+
return f"✅ Welcome back, {user[1]}! Login successful."
|
| 332 |
+
else:
|
| 333 |
+
conn.close()
|
| 334 |
+
return "❌ Invalid mobile number or password"
|
| 335 |
+
|
| 336 |
+
except Exception as e:
|
| 337 |
+
return f"❌ Login failed: {str(e)}"
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def use_ai_model(model_name, input_text, user_session="guest"):
|
| 341 |
+
"""Simulate AI model usage"""
|
| 342 |
+
if not input_text.strip():
|
| 343 |
+
return "Please enter some text for the AI model to process."
|
| 344 |
+
|
| 345 |
+
# Simulate model processing
|
| 346 |
+
response_templates = {
|
| 347 |
+
"text": f"🧠 {model_name} processed: '{input_text}'\n\n✨ AI Response: This is a simulated response from the {model_name} model. In production, this would connect to the actual model API.",
|
| 348 |
+
"image": f"🖼️ {model_name} would generate/edit an image based on: '{input_text}'\n\n📸 Output: Image processing complete (simulated)",
|
| 349 |
+
"audio": f"🎵 {model_name} audio processing for: '{input_text}'\n\n🔊 Output: Audio generated/processed (simulated)",
|
| 350 |
+
"multimodal": f"🤖 {model_name} multimodal processing: '{input_text}'\n\n🎯 Output: Combined AI analysis complete (simulated)",
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
# Determine response type based on model
|
| 354 |
+
if any(
|
| 355 |
+
x in model_name.lower()
|
| 356 |
+
for x in ["image", "flux", "diffusion", "face", "avatar"]
|
| 357 |
+
):
|
| 358 |
+
response_type = "image"
|
| 359 |
+
elif any(
|
| 360 |
+
x in model_name.lower()
|
| 361 |
+
for x in ["tts", "speech", "audio", "whisper", "wav2vec"]
|
| 362 |
+
):
|
| 363 |
+
response_type = "audio"
|
| 364 |
+
elif any(x in model_name.lower() for x in ["vl", "blip", "vision", "talking"]):
|
| 365 |
+
response_type = "multimodal"
|
| 366 |
+
else:
|
| 367 |
+
response_type = "text"
|
| 368 |
+
|
| 369 |
+
return response_templates[response_type]
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def get_cloudflare_status():
|
| 373 |
+
"""Get Cloudflare services status"""
|
| 374 |
+
services = []
|
| 375 |
+
|
| 376 |
+
if CLOUDFLARE_CONFIG["d1_database_id"]:
|
| 377 |
+
services.append("✅ D1 Database Connected")
|
| 378 |
+
else:
|
| 379 |
+
services.append("⚙️ D1 Database (Configure CLOUDFLARE_D1_DATABASE_ID)")
|
| 380 |
+
|
| 381 |
+
if CLOUDFLARE_CONFIG["r2_bucket_name"]:
|
| 382 |
+
services.append("✅ R2 Storage Connected")
|
| 383 |
+
else:
|
| 384 |
+
services.append("⚙️ R2 Storage (Configure CLOUDFLARE_R2_BUCKET_NAME)")
|
| 385 |
+
|
| 386 |
+
if CLOUDFLARE_CONFIG["kv_namespace_id"]:
|
| 387 |
+
services.append("✅ KV Cache Connected")
|
| 388 |
+
else:
|
| 389 |
+
services.append("⚙️ KV Cache (Configure CLOUDFLARE_KV_NAMESPACE_ID)")
|
| 390 |
+
|
| 391 |
+
if CLOUDFLARE_CONFIG["durable_objects_id"]:
|
| 392 |
+
services.append("✅ Durable Objects Connected")
|
| 393 |
+
else:
|
| 394 |
+
services.append("⚙️ Durable Objects (Configure CLOUDFLARE_DURABLE_OBJECTS_ID)")
|
| 395 |
+
|
| 396 |
+
return "\n".join(services)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
# Initialize database
|
| 400 |
+
init_database()
|
| 401 |
+
|
| 402 |
+
# Create Gradio interface
|
| 403 |
+
with gr.Blocks(
|
| 404 |
+
title="OpenManus - Complete AI Platform",
|
| 405 |
+
theme=gr.themes.Soft(),
|
| 406 |
+
css="""
|
| 407 |
+
.container { max-width: 1400px; margin: 0 auto; }
|
| 408 |
+
.header { text-align: center; padding: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 15px; margin-bottom: 25px; }
|
| 409 |
+
.section { background: white; padding: 25px; border-radius: 15px; margin: 15px 0; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
| 410 |
+
""",
|
| 411 |
+
) as app:
|
| 412 |
+
|
| 413 |
+
# Header
|
| 414 |
+
gr.HTML(
|
| 415 |
+
"""
|
| 416 |
+
<div class="header">
|
| 417 |
+
<h1>🤖 OpenManus - Complete AI Platform</h1>
|
| 418 |
+
<p><strong>Mobile Authentication + 200+ AI Models + Cloudflare Services</strong></p>
|
| 419 |
+
<p>🧠 Qwen & DeepSeek | 🖼️ Image Processing | 🎵 TTS/STT | 👤 Face Swap | 🌍 Arabic-English | ☁️ Cloud Integration</p>
|
| 420 |
+
</div>
|
| 421 |
+
"""
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
with gr.Row():
|
| 425 |
+
# Authentication Section
|
| 426 |
+
with gr.Column(scale=1, elem_classes="section"):
|
| 427 |
+
gr.Markdown("## 🔐 Authentication System")
|
| 428 |
+
|
| 429 |
+
with gr.Tab("Sign Up"):
|
| 430 |
+
gr.Markdown("### Create New Account")
|
| 431 |
+
signup_mobile = gr.Textbox(
|
| 432 |
+
label="Mobile Number",
|
| 433 |
+
placeholder="+1234567890",
|
| 434 |
+
info="Enter your mobile number with country code",
|
| 435 |
+
)
|
| 436 |
+
signup_name = gr.Textbox(
|
| 437 |
+
label="Full Name", placeholder="Your full name"
|
| 438 |
+
)
|
| 439 |
+
signup_password = gr.Textbox(
|
| 440 |
+
label="Password", type="password", info="Minimum 6 characters"
|
| 441 |
+
)
|
| 442 |
+
signup_confirm = gr.Textbox(label="Confirm Password", type="password")
|
| 443 |
+
signup_btn = gr.Button("Create Account", variant="primary")
|
| 444 |
+
signup_result = gr.Textbox(
|
| 445 |
+
label="Registration Status", interactive=False, lines=2
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
signup_btn.click(
|
| 449 |
+
signup_user,
|
| 450 |
+
[signup_mobile, signup_name, signup_password, signup_confirm],
|
| 451 |
+
signup_result,
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
with gr.Tab("Login"):
|
| 455 |
+
gr.Markdown("### Access Your Account")
|
| 456 |
+
login_mobile = gr.Textbox(
|
| 457 |
+
label="Mobile Number", placeholder="+1234567890"
|
| 458 |
+
)
|
| 459 |
+
login_password = gr.Textbox(label="Password", type="password")
|
| 460 |
+
login_btn = gr.Button("Login", variant="primary")
|
| 461 |
+
login_result = gr.Textbox(
|
| 462 |
+
label="Login Status", interactive=False, lines=2
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
login_btn.click(
|
| 466 |
+
login_user, [login_mobile, login_password], login_result
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
# AI Models Section
|
| 470 |
+
with gr.Column(scale=2, elem_classes="section"):
|
| 471 |
+
gr.Markdown("## 🤖 AI Models Hub (200+ Models)")
|
| 472 |
+
|
| 473 |
+
with gr.Tab("Text Generation"):
|
| 474 |
+
with gr.Row():
|
| 475 |
+
with gr.Column():
|
| 476 |
+
gr.Markdown("### Qwen Models (35 models)")
|
| 477 |
+
qwen_model = gr.Dropdown(
|
| 478 |
+
choices=AI_MODELS["Text Generation"]["Qwen Models"],
|
| 479 |
+
label="Select Qwen Model",
|
| 480 |
+
value="Qwen/Qwen2.5-72B-Instruct",
|
| 481 |
+
)
|
| 482 |
+
qwen_input = gr.Textbox(
|
| 483 |
+
label="Input Text",
|
| 484 |
+
placeholder="Enter your prompt for Qwen...",
|
| 485 |
+
lines=3,
|
| 486 |
+
)
|
| 487 |
+
qwen_btn = gr.Button("Generate with Qwen")
|
| 488 |
+
qwen_output = gr.Textbox(
|
| 489 |
+
label="Qwen Response", lines=5, interactive=False
|
| 490 |
+
)
|
| 491 |
+
qwen_btn.click(
|
| 492 |
+
use_ai_model, [qwen_model, qwen_input], qwen_output
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
with gr.Column():
|
| 496 |
+
gr.Markdown("### DeepSeek Models (17 models)")
|
| 497 |
+
deepseek_model = gr.Dropdown(
|
| 498 |
+
choices=AI_MODELS["Text Generation"]["DeepSeek Models"],
|
| 499 |
+
label="Select DeepSeek Model",
|
| 500 |
+
value="deepseek-ai/deepseek-llm-67b-chat",
|
| 501 |
+
)
|
| 502 |
+
deepseek_input = gr.Textbox(
|
| 503 |
+
label="Input Text",
|
| 504 |
+
placeholder="Enter your prompt for DeepSeek...",
|
| 505 |
+
lines=3,
|
| 506 |
+
)
|
| 507 |
+
deepseek_btn = gr.Button("Generate with DeepSeek")
|
| 508 |
+
deepseek_output = gr.Textbox(
|
| 509 |
+
label="DeepSeek Response", lines=5, interactive=False
|
| 510 |
+
)
|
| 511 |
+
deepseek_btn.click(
|
| 512 |
+
use_ai_model,
|
| 513 |
+
[deepseek_model, deepseek_input],
|
| 514 |
+
deepseek_output,
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
with gr.Tab("Image Processing"):
|
| 518 |
+
with gr.Row():
|
| 519 |
+
with gr.Column():
|
| 520 |
+
gr.Markdown("### Image Generation")
|
| 521 |
+
img_gen_model = gr.Dropdown(
|
| 522 |
+
choices=AI_MODELS["Image Processing"]["Image Generation"],
|
| 523 |
+
label="Select Image Model",
|
| 524 |
+
value="black-forest-labs/FLUX.1-dev",
|
| 525 |
+
)
|
| 526 |
+
img_prompt = gr.Textbox(
|
| 527 |
+
label="Image Prompt",
|
| 528 |
+
placeholder="Describe the image you want to generate...",
|
| 529 |
+
lines=2,
|
| 530 |
+
)
|
| 531 |
+
img_gen_btn = gr.Button("Generate Image")
|
| 532 |
+
img_gen_output = gr.Textbox(
|
| 533 |
+
label="Generation Status", lines=4, interactive=False
|
| 534 |
+
)
|
| 535 |
+
img_gen_btn.click(
|
| 536 |
+
use_ai_model, [img_gen_model, img_prompt], img_gen_output
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
with gr.Column():
|
| 540 |
+
gr.Markdown("### Face Processing & Editing")
|
| 541 |
+
face_model = gr.Dropdown(
|
| 542 |
+
choices=AI_MODELS["Image Processing"]["Face Processing"],
|
| 543 |
+
label="Select Face Model",
|
| 544 |
+
value="InsightFace/inswapper_128.onnx",
|
| 545 |
+
)
|
| 546 |
+
face_input = gr.Textbox(
|
| 547 |
+
label="Face Processing Task",
|
| 548 |
+
placeholder="Describe face swap or enhancement task...",
|
| 549 |
+
lines=2,
|
| 550 |
+
)
|
| 551 |
+
face_btn = gr.Button("Process Face")
|
| 552 |
+
face_output = gr.Textbox(
|
| 553 |
+
label="Processing Status", lines=4, interactive=False
|
| 554 |
+
)
|
| 555 |
+
face_btn.click(
|
| 556 |
+
use_ai_model, [face_model, face_input], face_output
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
with gr.Tab("Audio Processing"):
|
| 560 |
+
with gr.Row():
|
| 561 |
+
with gr.Column():
|
| 562 |
+
gr.Markdown("### Text-to-Speech (15 models)")
|
| 563 |
+
tts_model = gr.Dropdown(
|
| 564 |
+
choices=AI_MODELS["Audio Processing"]["Text-to-Speech"],
|
| 565 |
+
label="Select TTS Model",
|
| 566 |
+
value="microsoft/speecht5_tts",
|
| 567 |
+
)
|
| 568 |
+
tts_text = gr.Textbox(
|
| 569 |
+
label="Text to Speak",
|
| 570 |
+
placeholder="Enter text to convert to speech...",
|
| 571 |
+
lines=3,
|
| 572 |
+
)
|
| 573 |
+
tts_btn = gr.Button("Generate Speech")
|
| 574 |
+
tts_output = gr.Textbox(
|
| 575 |
+
label="TTS Status", lines=4, interactive=False
|
| 576 |
+
)
|
| 577 |
+
tts_btn.click(use_ai_model, [tts_model, tts_text], tts_output)
|
| 578 |
+
|
| 579 |
+
with gr.Column():
|
| 580 |
+
gr.Markdown("### Speech-to-Text (15 models)")
|
| 581 |
+
stt_model = gr.Dropdown(
|
| 582 |
+
choices=AI_MODELS["Audio Processing"]["Speech-to-Text"],
|
| 583 |
+
label="Select STT Model",
|
| 584 |
+
value="openai/whisper-large-v3",
|
| 585 |
+
)
|
| 586 |
+
stt_input = gr.Textbox(
|
| 587 |
+
label="Audio Description",
|
| 588 |
+
placeholder="Describe audio file to transcribe...",
|
| 589 |
+
lines=3,
|
| 590 |
+
)
|
| 591 |
+
stt_btn = gr.Button("Transcribe Audio")
|
| 592 |
+
stt_output = gr.Textbox(
|
| 593 |
+
label="STT Status", lines=4, interactive=False
|
| 594 |
+
)
|
| 595 |
+
stt_btn.click(use_ai_model, [stt_model, stt_input], stt_output)
|
| 596 |
+
|
| 597 |
+
with gr.Tab("Multimodal & Avatars"):
|
| 598 |
+
with gr.Row():
|
| 599 |
+
with gr.Column():
|
| 600 |
+
gr.Markdown("### Vision-Language Models")
|
| 601 |
+
vl_model = gr.Dropdown(
|
| 602 |
+
choices=AI_MODELS["Multimodal AI"]["Vision-Language"],
|
| 603 |
+
label="Select VL Model",
|
| 604 |
+
value="liuhaotian/llava-v1.6-34b",
|
| 605 |
+
)
|
| 606 |
+
vl_input = gr.Textbox(
|
| 607 |
+
label="Vision-Language Task",
|
| 608 |
+
placeholder="Describe image analysis or VQA task...",
|
| 609 |
+
lines=3,
|
| 610 |
+
)
|
| 611 |
+
vl_btn = gr.Button("Process with VL Model")
|
| 612 |
+
vl_output = gr.Textbox(
|
| 613 |
+
label="VL Response", lines=4, interactive=False
|
| 614 |
+
)
|
| 615 |
+
vl_btn.click(use_ai_model, [vl_model, vl_input], vl_output)
|
| 616 |
+
|
| 617 |
+
with gr.Column():
|
| 618 |
+
gr.Markdown("### Talking Avatars")
|
| 619 |
+
avatar_model = gr.Dropdown(
|
| 620 |
+
choices=AI_MODELS["Multimodal AI"]["Talking Avatars"],
|
| 621 |
+
label="Select Avatar Model",
|
| 622 |
+
value="Wav2Lip-HD",
|
| 623 |
+
)
|
| 624 |
+
avatar_input = gr.Textbox(
|
| 625 |
+
label="Avatar Generation Task",
|
| 626 |
+
placeholder="Describe talking avatar or lip-sync task...",
|
| 627 |
+
lines=3,
|
| 628 |
+
)
|
| 629 |
+
avatar_btn = gr.Button("Generate Avatar")
|
| 630 |
+
avatar_output = gr.Textbox(
|
| 631 |
+
label="Avatar Status", lines=4, interactive=False
|
| 632 |
+
)
|
| 633 |
+
avatar_btn.click(
|
| 634 |
+
use_ai_model, [avatar_model, avatar_input], avatar_output
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
with gr.Tab("Arabic-English"):
|
| 638 |
+
gr.Markdown("### Arabic-English Interactive Models (12 models)")
|
| 639 |
+
arabic_model = gr.Dropdown(
|
| 640 |
+
choices=AI_MODELS["Arabic-English Models"],
|
| 641 |
+
label="Select Arabic-English Model",
|
| 642 |
+
value="aubmindlab/bert-base-arabertv2",
|
| 643 |
+
)
|
| 644 |
+
arabic_input = gr.Textbox(
|
| 645 |
+
label="Text (Arabic or English)",
|
| 646 |
+
placeholder="أدخل النص باللغة العربية أو الإنجليزية / Enter text in Arabic or English...",
|
| 647 |
+
lines=4,
|
| 648 |
+
)
|
| 649 |
+
arabic_btn = gr.Button("Process Arabic-English")
|
| 650 |
+
arabic_output = gr.Textbox(
|
| 651 |
+
label="Processing Result", lines=6, interactive=False
|
| 652 |
+
)
|
| 653 |
+
arabic_btn.click(
|
| 654 |
+
use_ai_model, [arabic_model, arabic_input], arabic_output
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
# Services Status Section
|
| 658 |
+
with gr.Row():
|
| 659 |
+
with gr.Column(elem_classes="section"):
|
| 660 |
+
gr.Markdown("## ☁️ Cloudflare Services Integration")
|
| 661 |
+
|
| 662 |
+
with gr.Row():
|
| 663 |
+
with gr.Column():
|
| 664 |
+
gr.Markdown("### Services Status")
|
| 665 |
+
services_status = gr.Textbox(
|
| 666 |
+
label="Cloudflare Services",
|
| 667 |
+
value=get_cloudflare_status(),
|
| 668 |
+
lines=6,
|
| 669 |
+
interactive=False,
|
| 670 |
+
)
|
| 671 |
+
refresh_btn = gr.Button("Refresh Status")
|
| 672 |
+
refresh_btn.click(
|
| 673 |
+
lambda: get_cloudflare_status(), outputs=services_status
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
with gr.Column():
|
| 677 |
+
gr.Markdown("### Configuration")
|
| 678 |
+
gr.HTML(
|
| 679 |
+
"""
|
| 680 |
+
<div style="background: #f0f8ff; padding: 15px; border-radius: 10px;">
|
| 681 |
+
<h4>Environment Variables:</h4>
|
| 682 |
+
<ul>
|
| 683 |
+
<li><code>CLOUDFLARE_API_TOKEN</code> - API authentication</li>
|
| 684 |
+
<li><code>CLOUDFLARE_ACCOUNT_ID</code> - Account identifier</li>
|
| 685 |
+
<li><code>CLOUDFLARE_D1_DATABASE_ID</code> - D1 database</li>
|
| 686 |
+
<li><code>CLOUDFLARE_R2_BUCKET_NAME</code> - R2 storage</li>
|
| 687 |
+
<li><code>CLOUDFLARE_KV_NAMESPACE_ID</code> - KV cache</li>
|
| 688 |
+
<li><code>CLOUDFLARE_DURABLE_OBJECTS_ID</code> - Durable objects</li>
|
| 689 |
+
</ul>
|
| 690 |
+
</div>
|
| 691 |
+
"""
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
# Footer Status
|
| 695 |
+
gr.HTML(
|
| 696 |
+
"""
|
| 697 |
+
<div style="background: linear-gradient(45deg, #f0f8ff 0%, #e6f3ff 100%); padding: 20px; border-radius: 15px; margin-top: 25px; text-align: center;">
|
| 698 |
+
<h3>📊 Platform Status</h3>
|
| 699 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 15px 0;">
|
| 700 |
+
<div>✅ <strong>Authentication:</strong> Active</div>
|
| 701 |
+
<div>🧠 <strong>AI Models:</strong> 200+ Ready</div>
|
| 702 |
+
<div>🖼️ <strong>Image Processing:</strong> Available</div>
|
| 703 |
+
<div>🎵 <strong>Audio AI:</strong> Enabled</div>
|
| 704 |
+
<div>👤 <strong>Face/Avatar:</strong> Ready</div>
|
| 705 |
+
<div>🌍 <strong>Arabic-English:</strong> Supported</div>
|
| 706 |
+
<div>☁️ <strong>Cloudflare:</strong> Configurable</div>
|
| 707 |
+
<div>🚀 <strong>Platform:</strong> Production Ready</div>
|
| 708 |
+
</div>
|
| 709 |
+
<p><em>Complete AI Platform successfully deployed on HuggingFace Spaces with Docker!</em></p>
|
| 710 |
+
</div>
|
| 711 |
+
"""
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
if __name__ == "__main__":
|
| 715 |
+
app.launch(server_name="0.0.0.0", server_port=7860)
|
config/config.example.toml
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Global LLM configuration
|
| 2 |
+
[llm]
|
| 3 |
+
model = "claude-3-7-sonnet-20250219" # The LLM model to use
|
| 4 |
+
base_url = "https://api.anthropic.com/v1/" # API endpoint URL
|
| 5 |
+
api_key = "YOUR_API_KEY" # Your API key
|
| 6 |
+
max_tokens = 8192 # Maximum number of tokens in the response
|
| 7 |
+
temperature = 0.0 # Controls randomness
|
| 8 |
+
|
| 9 |
+
# [llm] # Amazon Bedrock
|
| 10 |
+
# api_type = "aws" # Required
|
| 11 |
+
# model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0" # Bedrock supported modelID
|
| 12 |
+
# base_url = "bedrock-runtime.us-west-2.amazonaws.com" # Not used now
|
| 13 |
+
# max_tokens = 8192
|
| 14 |
+
# temperature = 1.0
|
| 15 |
+
# api_key = "bear" # Required but not used for Bedrock
|
| 16 |
+
|
| 17 |
+
# [llm] #AZURE OPENAI:
|
| 18 |
+
# api_type= 'azure'
|
| 19 |
+
# model = "YOUR_MODEL_NAME" #"gpt-4o-mini"
|
| 20 |
+
# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}"
|
| 21 |
+
# api_key = "AZURE API KEY"
|
| 22 |
+
# max_tokens = 8096
|
| 23 |
+
# temperature = 0.0
|
| 24 |
+
# api_version="AZURE API VERSION" #"2024-08-01-preview"
|
| 25 |
+
|
| 26 |
+
# [llm] #OLLAMA:
|
| 27 |
+
# api_type = 'ollama'
|
| 28 |
+
# model = "llama3.2"
|
| 29 |
+
# base_url = "http://localhost:11434/v1"
|
| 30 |
+
# api_key = "ollama"
|
| 31 |
+
# max_tokens = 4096
|
| 32 |
+
# temperature = 0.0
|
| 33 |
+
|
| 34 |
+
# Optional configuration for specific LLM models
|
| 35 |
+
[llm.vision]
|
| 36 |
+
model = "claude-3-7-sonnet-20250219" # The vision model to use
|
| 37 |
+
base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model
|
| 38 |
+
api_key = "YOUR_API_KEY" # Your API key for vision model
|
| 39 |
+
max_tokens = 8192 # Maximum number of tokens in the response
|
| 40 |
+
temperature = 0.0 # Controls randomness for vision model
|
| 41 |
+
|
| 42 |
+
# [llm.vision] #OLLAMA VISION:
|
| 43 |
+
# api_type = 'ollama'
|
| 44 |
+
# model = "llama3.2-vision"
|
| 45 |
+
# base_url = "http://localhost:11434/v1"
|
| 46 |
+
# api_key = "ollama"
|
| 47 |
+
# max_tokens = 4096
|
| 48 |
+
# temperature = 0.0
|
| 49 |
+
|
| 50 |
+
# Optional configuration for specific browser configuration
|
| 51 |
+
# [browser]
|
| 52 |
+
# Whether to run browser in headless mode (default: false)
|
| 53 |
+
#headless = false
|
| 54 |
+
# Disable browser security features (default: true)
|
| 55 |
+
#disable_security = true
|
| 56 |
+
# Extra arguments to pass to the browser
|
| 57 |
+
#extra_chromium_args = []
|
| 58 |
+
# Path to a Chrome instance to use to connect to your normal browser
|
| 59 |
+
# e.g. '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
| 60 |
+
#chrome_instance_path = ""
|
| 61 |
+
# Connect to a browser instance via WebSocket
|
| 62 |
+
#wss_url = ""
|
| 63 |
+
# Connect to a browser instance via CDP
|
| 64 |
+
#cdp_url = ""
|
| 65 |
+
|
| 66 |
+
# Optional configuration, Proxy settings for the browser
|
| 67 |
+
# [browser.proxy]
|
| 68 |
+
# server = "http://proxy-server:port"
|
| 69 |
+
# username = "proxy-username"
|
| 70 |
+
# password = "proxy-password"
|
| 71 |
+
|
| 72 |
+
# Optional configuration, Search settings.
|
| 73 |
+
# [search]
|
| 74 |
+
# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo" or "Bing".
|
| 75 |
+
#engine = "Google"
|
| 76 |
+
# Fallback engine order. Default is ["DuckDuckGo", "Baidu", "Bing"] - will try in this order after primary engine fails.
|
| 77 |
+
#fallback_engines = ["DuckDuckGo", "Baidu", "Bing"]
|
| 78 |
+
# Seconds to wait before retrying all engines again when they all fail due to rate limits. Default is 60.
|
| 79 |
+
#retry_delay = 60
|
| 80 |
+
# Maximum number of times to retry all engines when all fail. Default is 3.
|
| 81 |
+
#max_retries = 3
|
| 82 |
+
# Language code for search results. Options: "en" (English), "zh" (Chinese), etc.
|
| 83 |
+
#lang = "en"
|
| 84 |
+
# Country code for search results. Options: "us" (United States), "cn" (China), etc.
|
| 85 |
+
#country = "us"
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
## Sandbox configuration
|
| 89 |
+
#[sandbox]
|
| 90 |
+
#use_sandbox = false
|
| 91 |
+
#image = "python:3.12-slim"
|
| 92 |
+
#work_dir = "/workspace"
|
| 93 |
+
#memory_limit = "1g" # 512m
|
| 94 |
+
#cpu_limit = 2.0
|
| 95 |
+
#timeout = 300
|
| 96 |
+
#network_enabled = true
|
| 97 |
+
|
| 98 |
+
# MCP (Model Context Protocol) configuration
|
| 99 |
+
[mcp]
|
| 100 |
+
server_reference = "app.mcp.server" # default server module reference
|
| 101 |
+
|
| 102 |
+
# Optional Runflow configuration
|
| 103 |
+
# Your can add additional agents into run-flow workflow to solve different-type tasks.
|
| 104 |
+
[runflow]
|
| 105 |
+
use_data_analysis_agent = false # The Data Analysi Agent to solve various data analysis tasks
|
docker-commands.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenManus Docker Commands
|
| 2 |
+
# Build and run the OpenManus platform locally
|
| 3 |
+
|
| 4 |
+
# 1. Build the Docker image locally
|
| 5 |
+
docker build -t openmanus-local .
|
| 6 |
+
|
| 7 |
+
# 2. Run the container locally (recommended)
|
| 8 |
+
docker run -it -p 7860:7860 --platform=linux/amd64 openmanus-local
|
| 9 |
+
|
| 10 |
+
# 3. Run with environment variables (for Cloudflare integration)
|
| 11 |
+
docker run -it -p 7860:7860 --platform=linux/amd64 \
|
| 12 |
+
-e CLOUDFLARE_API_TOKEN="your-token" \
|
| 13 |
+
-e CLOUDFLARE_ACCOUNT_ID="your-account-id" \
|
| 14 |
+
-e CLOUDFLARE_D1_DATABASE_ID="your-d1-id" \
|
| 15 |
+
-e CLOUDFLARE_R2_BUCKET_NAME="your-r2-bucket" \
|
| 16 |
+
-e CLOUDFLARE_KV_NAMESPACE_ID="your-kv-id" \
|
| 17 |
+
-e CLOUDFLARE_DURABLE_OBJECTS_ID="your-durable-objects-id" \
|
| 18 |
+
openmanus-local
|
| 19 |
+
|
| 20 |
+
# 4. Run with volume mounts (for persistent data)
|
| 21 |
+
docker run -it -p 7860:7860 --platform=linux/amd64 \
|
| 22 |
+
-v "${PWD}/data:/home/user/app/data" \
|
| 23 |
+
-v "${PWD}/logs:/home/user/app/logs" \
|
| 24 |
+
openmanus-local
|
| 25 |
+
|
| 26 |
+
# 5. Run in background (daemon mode)
|
| 27 |
+
docker run -d -p 7860:7860 --platform=linux/amd64 --name openmanus openmanus-local
|
| 28 |
+
|
| 29 |
+
# 6. Run from HuggingFace registry - NEW HHH SPACE
|
| 30 |
+
docker run -it -p 7860:7860 --platform=linux/amd64 \
|
| 31 |
+
registry.hf.space/speedofmastery-hhh:latest
|
| 32 |
+
|
| 33 |
+
# 6b. Run from old agnt space (backup)
|
| 34 |
+
docker run -it -p 7860:7860 --platform=linux/amd64 \
|
| 35 |
+
registry.hf.space/speedofmastery-agnt:latest
|
| 36 |
+
|
| 37 |
+
# 7. Debug mode with shell access
|
| 38 |
+
docker run -it --platform=linux/amd64 openmanus-local /bin/bash
|
| 39 |
+
|
| 40 |
+
# 8. Check running containers
|
| 41 |
+
docker ps
|
| 42 |
+
|
| 43 |
+
# 9. Stop the container
|
| 44 |
+
docker stop openmanus
|
| 45 |
+
|
| 46 |
+
# 10. Remove the container
|
| 47 |
+
docker rm openmanus
|
| 48 |
+
|
| 49 |
+
# Access the platform at: http://localhost:7860
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0
|
requirements_backup.txt
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pydantic~=2.10.6
|
| 2 |
+
openai~=1.66.3
|
| 3 |
+
tenacity~=9.0.0
|
| 4 |
+
pyyaml~=6.0.2
|
| 5 |
+
loguru~=0.7.3
|
| 6 |
+
numpy
|
| 7 |
+
datasets~=3.4.1
|
| 8 |
+
fastapi~=0.115.11
|
| 9 |
+
tiktoken~=0.9.0
|
| 10 |
+
|
| 11 |
+
html2text~=2024.2.26
|
| 12 |
+
gymnasium~=1.1.1
|
| 13 |
+
pillow~=11.1.0
|
| 14 |
+
browsergym~=0.13.3
|
| 15 |
+
uvicorn~=0.34.0
|
| 16 |
+
unidiff~=0.7.5
|
| 17 |
+
browser-use~=0.1.40
|
| 18 |
+
googlesearch-python~=1.3.0
|
| 19 |
+
baidusearch~=1.0.3
|
| 20 |
+
duckduckgo_search~=7.5.3
|
| 21 |
+
|
| 22 |
+
aiofiles~=24.1.0
|
| 23 |
+
pydantic_core~=2.27.2
|
| 24 |
+
colorama~=0.4.6
|
| 25 |
+
playwright~=1.51.0
|
| 26 |
+
|
| 27 |
+
docker~=7.1.0
|
| 28 |
+
pytest~=8.3.5
|
| 29 |
+
pytest-asyncio~=0.25.3
|
| 30 |
+
|
| 31 |
+
mcp~=1.5.0
|
| 32 |
+
httpx>=0.27.0
|
| 33 |
+
tomli>=2.0.0
|
| 34 |
+
|
| 35 |
+
boto3~=1.37.18
|
| 36 |
+
|
| 37 |
+
requests~=2.32.3
|
| 38 |
+
beautifulsoup4~=4.13.3
|
| 39 |
+
crawl4ai~=0.6.3
|
| 40 |
+
|
| 41 |
+
huggingface-hub~=0.29.2
|
| 42 |
+
transformers~=4.46.0
|
| 43 |
+
torch>=2.0.0
|
| 44 |
+
gradio>=4.0,<5.0
|
| 45 |
+
setuptools~=75.8.0
|
| 46 |
+
|
| 47 |
+
# Authentication dependencies
|
| 48 |
+
bcrypt~=4.0.1
|
| 49 |
+
|
| 50 |
+
# Cloudflare integrations - Complete stack
|
| 51 |
+
aiohttp~=3.9.0
|
| 52 |
+
websockets~=12.0
|
| 53 |
+
httpx~=0.25.0
|
| 54 |
+
cloudflare~=2.20.1
|
| 55 |
+
cffi~=1.16.0
|
| 56 |
+
cryptography~=42.0.0
|
| 57 |
+
|
| 58 |
+
# Additional backend dependencies
|
| 59 |
+
asyncio-mqtt~=0.16.2
|
| 60 |
+
aiodns~=3.2.0
|
| 61 |
+
cchardet~=2.1.7
|
| 62 |
+
charset-normalizer~=3.4.0
|
| 63 |
+
python-dotenv~=1.0.0
|
| 64 |
+
|
| 65 |
+
# Essential dependencies for production deployment
|
| 66 |
+
pandas>=1.5.0
|
| 67 |
+
numpy>=1.21.0
|
| 68 |
+
Pillow>=8.0.0
|
| 69 |
+
python-multipart>=0.0.5
|
| 70 |
+
email-validator>=1.1.0
|
| 71 |
+
passlib>=1.7.4
|
| 72 |
+
python-jose>=3.3.0
|
| 73 |
+
|
| 74 |
+
# Additional dependencies for Hugging Face Spaces and models integration
|
| 75 |
+
gradio~=4.44.0
|
| 76 |
+
spaces~=0.19.4
|
requirements_fixed.txt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
pydantic>=2.0,<3.0
|
| 3 |
+
fastapi>=0.100.0
|
| 4 |
+
uvicorn>=0.20.0
|
| 5 |
+
gradio>=4.0,<5.0
|
| 6 |
+
|
| 7 |
+
# AI and ML
|
| 8 |
+
huggingface-hub>=0.20.0
|
| 9 |
+
transformers>=4.30.0
|
| 10 |
+
torch>=2.0.0
|
| 11 |
+
datasets>=2.0.0
|
| 12 |
+
numpy>=1.21.0
|
| 13 |
+
pillow>=8.0.0
|
| 14 |
+
|
| 15 |
+
# Authentication and security
|
| 16 |
+
bcrypt>=4.0.0
|
| 17 |
+
python-multipart>=0.0.5
|
| 18 |
+
python-jose>=3.3.0
|
| 19 |
+
passlib>=1.7.4
|
| 20 |
+
|
| 21 |
+
# Async and HTTP
|
| 22 |
+
aiohttp>=3.8.0
|
| 23 |
+
aiofiles>=22.0.0
|
| 24 |
+
httpx>=0.24.0
|
| 25 |
+
websockets>=10.0
|
| 26 |
+
|
| 27 |
+
# Data processing
|
| 28 |
+
pandas>=1.5.0
|
| 29 |
+
pyyaml>=6.0.0
|
| 30 |
+
requests>=2.28.0
|
| 31 |
+
beautifulsoup4>=4.11.0
|
| 32 |
+
|
| 33 |
+
# Logging and utilities
|
| 34 |
+
loguru>=0.7.0
|
| 35 |
+
python-dotenv>=1.0.0
|
| 36 |
+
tenacity>=8.0.0
|
| 37 |
+
colorama>=0.4.0
|
| 38 |
+
|
| 39 |
+
# Cloudflare (optional)
|
| 40 |
+
cloudflare>=2.0.0
|
| 41 |
+
cryptography>=40.0.0
|
| 42 |
+
|
| 43 |
+
# Additional utilities
|
| 44 |
+
setuptools>=60.0.0
|
requirements_new.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core framework - compatible versions
|
| 2 |
+
fastapi>=0.100.0,<0.116.0
|
| 3 |
+
uvicorn>=0.20.0,<0.35.0
|
| 4 |
+
pydantic>=2.0,<3.0
|
| 5 |
+
starlette>=0.35.0,<0.42.0
|
| 6 |
+
|
| 7 |
+
# Gradio and UI - stable version
|
| 8 |
+
gradio>=4.0,<5.0
|
| 9 |
+
|
| 10 |
+
# HTTP and networking - core only
|
| 11 |
+
httpx>=0.25.0,<0.29.0
|
| 12 |
+
requests>=2.28.0,<2.33.0
|
| 13 |
+
|
| 14 |
+
# Database - minimal setup
|
| 15 |
+
bcrypt>=4.0.0,<5.0.0
|
| 16 |
+
|
| 17 |
+
# AI essentials - lightweight
|
| 18 |
+
openai>=1.0.0,<2.0.0
|
| 19 |
+
huggingface-hub>=0.20.0,<1.0.0
|
| 20 |
+
|
| 21 |
+
# Basic utilities
|
| 22 |
+
python-dotenv>=1.0.0,<2.0.0
|
| 23 |
+
pyyaml>=6.0,<7.0
|
| 24 |
+
python-multipart>=0.0.6
|
| 25 |
+
|
| 26 |
+
# System utilities
|
| 27 |
+
pillow>=10.0.0
|
| 28 |
+
numpy>=1.20.0
|
| 29 |
+
|
| 30 |
+
# Additional basics
|
| 31 |
+
typing-extensions>=4.0.0
|
| 32 |
+
loguru>=0.7.0
|
| 33 |
+
tenacity>=8.0.0
|
schema.sql
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- OpenManus Database Schema for Cloudflare D1
|
| 2 |
+
|
| 3 |
+
-- Users table to store user information
|
| 4 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 5 |
+
id TEXT PRIMARY KEY,
|
| 6 |
+
mobile_number TEXT UNIQUE NOT NULL,
|
| 7 |
+
full_name TEXT NOT NULL,
|
| 8 |
+
password_hash TEXT NOT NULL,
|
| 9 |
+
avatar_url TEXT,
|
| 10 |
+
preferences TEXT, -- JSON string for user preferences
|
| 11 |
+
is_active BOOLEAN DEFAULT TRUE,
|
| 12 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 13 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 14 |
+
);
|
| 15 |
+
|
| 16 |
+
-- Sessions table to store user sessions
|
| 17 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 18 |
+
id TEXT PRIMARY KEY,
|
| 19 |
+
user_id TEXT NOT NULL,
|
| 20 |
+
title TEXT,
|
| 21 |
+
metadata TEXT, -- JSON string for session metadata
|
| 22 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 23 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 24 |
+
expires_at DATETIME,
|
| 25 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
-- Conversations table to store chat messages
|
| 29 |
+
CREATE TABLE IF NOT EXISTS conversations (
|
| 30 |
+
id TEXT PRIMARY KEY,
|
| 31 |
+
session_id TEXT NOT NULL,
|
| 32 |
+
role TEXT NOT NULL, -- 'user', 'assistant', 'system'
|
| 33 |
+
content TEXT NOT NULL,
|
| 34 |
+
metadata TEXT, -- JSON string for message metadata (files, tools used, etc.)
|
| 35 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 36 |
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
| 37 |
+
);
|
| 38 |
+
|
| 39 |
+
-- Files table to store uploaded file information
|
| 40 |
+
CREATE TABLE IF NOT EXISTS files (
|
| 41 |
+
id TEXT PRIMARY KEY,
|
| 42 |
+
user_id TEXT NOT NULL,
|
| 43 |
+
session_id TEXT,
|
| 44 |
+
filename TEXT NOT NULL,
|
| 45 |
+
content_type TEXT,
|
| 46 |
+
size INTEGER,
|
| 47 |
+
r2_key TEXT NOT NULL, -- Key in R2 storage
|
| 48 |
+
bucket TEXT NOT NULL, -- Which R2 bucket
|
| 49 |
+
metadata TEXT, -- JSON string for file metadata
|
| 50 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 51 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 52 |
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL
|
| 53 |
+
);
|
| 54 |
+
|
| 55 |
+
-- Agents table to store agent configurations
|
| 56 |
+
CREATE TABLE IF NOT EXISTS agents (
|
| 57 |
+
id TEXT PRIMARY KEY,
|
| 58 |
+
user_id TEXT NOT NULL,
|
| 59 |
+
name TEXT NOT NULL,
|
| 60 |
+
description TEXT,
|
| 61 |
+
system_prompt TEXT,
|
| 62 |
+
model TEXT,
|
| 63 |
+
tools TEXT, -- JSON array of enabled tools
|
| 64 |
+
config TEXT, -- JSON configuration object
|
| 65 |
+
is_active BOOLEAN DEFAULT TRUE,
|
| 66 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 67 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 68 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
-- Agent sessions table for durable object session tracking
|
| 72 |
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
| 73 |
+
id TEXT PRIMARY KEY,
|
| 74 |
+
agent_id TEXT NOT NULL,
|
| 75 |
+
user_id TEXT NOT NULL,
|
| 76 |
+
session_id TEXT NOT NULL,
|
| 77 |
+
durable_object_id TEXT, -- ID of the corresponding durable object
|
| 78 |
+
status TEXT DEFAULT 'active', -- 'active', 'paused', 'completed', 'error'
|
| 79 |
+
metadata TEXT, -- JSON string for session state
|
| 80 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 81 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 82 |
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
|
| 83 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 84 |
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
| 85 |
+
);
|
| 86 |
+
|
| 87 |
+
-- Usage tracking table for monitoring and analytics
|
| 88 |
+
CREATE TABLE IF NOT EXISTS usage_logs (
|
| 89 |
+
id TEXT PRIMARY KEY,
|
| 90 |
+
user_id TEXT NOT NULL,
|
| 91 |
+
session_id TEXT,
|
| 92 |
+
agent_id TEXT,
|
| 93 |
+
action TEXT NOT NULL, -- 'chat', 'upload', 'tool_use', etc.
|
| 94 |
+
resource_type TEXT, -- 'd1', 'r2', 'kv', 'durable_object'
|
| 95 |
+
resource_id TEXT,
|
| 96 |
+
tokens_used INTEGER DEFAULT 0,
|
| 97 |
+
duration_ms INTEGER DEFAULT 0,
|
| 98 |
+
cost_cents INTEGER DEFAULT 0,
|
| 99 |
+
metadata TEXT, -- JSON string for additional details
|
| 100 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 101 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 102 |
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL,
|
| 103 |
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL
|
| 104 |
+
);
|
| 105 |
+
|
| 106 |
+
-- Create indexes for better performance
|
| 107 |
+
CREATE INDEX IF NOT EXISTS idx_users_mobile_number ON users(mobile_number);
|
| 108 |
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
| 109 |
+
CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at);
|
| 110 |
+
CREATE INDEX IF NOT EXISTS idx_conversations_session_id ON conversations(session_id);
|
| 111 |
+
CREATE INDEX IF NOT EXISTS idx_conversations_created_at ON conversations(created_at);
|
| 112 |
+
CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id);
|
| 113 |
+
CREATE INDEX IF NOT EXISTS idx_files_session_id ON files(session_id);
|
| 114 |
+
CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at);
|
| 115 |
+
CREATE INDEX IF NOT EXISTS idx_agents_user_id ON agents(user_id);
|
| 116 |
+
CREATE INDEX IF NOT EXISTS idx_agent_sessions_user_id ON agent_sessions(user_id);
|
| 117 |
+
CREATE INDEX IF NOT EXISTS idx_agent_sessions_session_id ON agent_sessions(session_id);
|
| 118 |
+
CREATE INDEX IF NOT EXISTS idx_usage_logs_user_id ON usage_logs(user_id);
|
| 119 |
+
CREATE INDEX IF NOT EXISTS idx_usage_logs_created_at ON usage_logs(created_at);
|
| 120 |
+
|
| 121 |
+
-- Insert a default system user for system-level operations
|
| 122 |
+
INSERT OR IGNORE INTO users (id, mobile_number, full_name, password_hash)
|
| 123 |
+
VALUES ('system', '0000000000', 'OpenManus System', 'system_hash');
|
| 124 |
+
|
| 125 |
+
-- Insert a default agent configuration
|
| 126 |
+
INSERT OR IGNORE INTO agents (id, user_id, name, description, system_prompt, model, tools)
|
| 127 |
+
VALUES (
|
| 128 |
+
'default-agent',
|
| 129 |
+
'system',
|
| 130 |
+
'OpenManus Assistant',
|
| 131 |
+
'Default OpenManus AI assistant with full capabilities',
|
| 132 |
+
'You are OpenManus, an intelligent AI assistant with access to various tools and services. You help users with a wide range of tasks including file management, data analysis, web browsing, and more. Always be helpful, accurate, and concise in your responses.',
|
| 133 |
+
'gpt-4-turbo-preview',
|
| 134 |
+
'["file_operations", "web_search", "data_analysis", "browser_use", "python_execute"]'
|
| 135 |
+
);
|
start.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# OpenManus Platform - Linux Startup Script
|
| 4 |
+
|
| 5 |
+
echo "🐧 Starting OpenManus Platform on Linux..."
|
| 6 |
+
|
| 7 |
+
# Create necessary directories
|
| 8 |
+
mkdir -p logs data cache
|
| 9 |
+
|
| 10 |
+
# Set proper permissions
|
| 11 |
+
chmod +x app.py
|
| 12 |
+
|
| 13 |
+
# Check Python and show system info
|
| 14 |
+
echo "� System Information:"
|
| 15 |
+
echo "Python version: $(python3 --version)"
|
| 16 |
+
echo "Working directory: $(pwd)"
|
| 17 |
+
echo "User: $(whoami)"
|
| 18 |
+
|
| 19 |
+
# List files to verify everything is in place
|
| 20 |
+
echo "� Files in directory:"
|
| 21 |
+
ls -la
|
| 22 |
+
|
| 23 |
+
# Start the application
|
| 24 |
+
echo "🚀 Launching OpenManus Platform..."
|
| 25 |
+
exec python3 app.py
|