Spaces:
Sleeping
Sleeping
Commit
·
4343907
0
Parent(s):
feat: initial HuggingFace Space deployment
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +65 -0
- .gitattributes +35 -0
- .github/workflows/ci-cd-pipeline.yml +470 -0
- .github/workflows/deploy-huggingface.yml +151 -0
- .gitignore +220 -0
- .gitleaks.toml +26 -0
- .pre-commit-config.yaml +38 -0
- DEPLOYMENT.md +389 -0
- Dockerfile +47 -0
- LICENSE +21 -0
- PROJEKT_VOLLSTAENDIGE_ANALYSE_2025-12-04.md +578 -0
- QUICKSTART.md +362 -0
- README.md +63 -0
- SECURITY_REMEDIATION_REQUIRED.md +198 -0
- SECURITY_SCAN_REPORT.md +294 -0
- SECURITY_SETUP_COMPLETE.md +240 -0
- backend/.dockerignore +75 -0
- backend/.env.example +230 -0
- backend/Dockerfile +74 -0
- backend/__init__.py +1 -0
- backend/agent.py +343 -0
- backend/agent_init_fix.py +45 -0
- backend/agent_manager.py +989 -0
- backend/agent_manager_database_enhanced.py +328 -0
- backend/agent_manager_enhanced.py +651 -0
- backend/agent_manager_fixed.py +978 -0
- backend/agent_manager_hybrid.py +575 -0
- backend/agent_manager_hybrid_fixed.py +494 -0
- backend/agent_schema.json +267 -0
- backend/agent_schema.py +784 -0
- backend/agent_templates.json +144 -0
- backend/agents/__init__.py +1 -0
- backend/agents/colossus_agent.py +377 -0
- backend/agents/colossus_saap_agent.py +384 -0
- backend/agents/openrouter_agent_enhanced.py +367 -0
- backend/agents/openrouter_saap_agent.py +301 -0
- backend/api/__init__.py +1 -0
- backend/api/agent_api.py +359 -0
- backend/api/agent_manager.py +419 -0
- backend/api/agents.py +820 -0
- backend/api/colossus_client.py +216 -0
- backend/api/cost_tracking.py +303 -0
- backend/api/hybrid_endpoints.py +389 -0
- backend/api/multi_agent_endpoints.py +408 -0
- backend/api/openrouter_client.py +397 -0
- backend/api/openrouter_endpoints.py +230 -0
- backend/config/__init__.py +1 -0
- backend/config/settings.py +482 -0
- backend/connection.py +415 -0
- backend/cost_efficiency_logger.py +478 -0
.env.example
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# SAAP Environment Configuration Template
|
| 3 |
+
# ==========================================
|
| 4 |
+
# Copy this file to .env and fill in your actual values
|
| 5 |
+
# NEVER commit .env to version control!
|
| 6 |
+
|
| 7 |
+
# ==========================================
|
| 8 |
+
# Application Settings
|
| 9 |
+
# ==========================================
|
| 10 |
+
ENVIRONMENT=development
|
| 11 |
+
DEBUG=true
|
| 12 |
+
LOG_LEVEL=INFO
|
| 13 |
+
SECRET_KEY=your-secret-key-change-in-production
|
| 14 |
+
|
| 15 |
+
# ==========================================
|
| 16 |
+
# Database Configuration (PostgreSQL)
|
| 17 |
+
# ==========================================
|
| 18 |
+
POSTGRES_DB=saap_db
|
| 19 |
+
POSTGRES_USER=saap_user
|
| 20 |
+
POSTGRES_PASSWORD=saap_password
|
| 21 |
+
POSTGRES_PORT=5432
|
| 22 |
+
DATABASE_URL=postgresql://saap_user:saap_password@postgres:5432/saap_db
|
| 23 |
+
|
| 24 |
+
# ==========================================
|
| 25 |
+
# API Keys (REQUIRED)
|
| 26 |
+
# ==========================================
|
| 27 |
+
# Colossus API Key (Free Provider - Fallback)
|
| 28 |
+
COLOSSUS_API_KEY=your-colossus-api-key-here
|
| 29 |
+
|
| 30 |
+
# OpenRouter API Key (Primary Provider - Cost-Efficient)
|
| 31 |
+
# Get your key from: https://openrouter.ai/keys
|
| 32 |
+
OPENROUTER_API_KEY=your-openrouter-api-key-here
|
| 33 |
+
|
| 34 |
+
# ==========================================
|
| 35 |
+
# CORS Settings
|
| 36 |
+
# ==========================================
|
| 37 |
+
# Comma-separated list of allowed origins
|
| 38 |
+
CORS_ORIGINS=http://localhost:5173,http://localhost:80,http://localhost:3000
|
| 39 |
+
|
| 40 |
+
# ==========================================
|
| 41 |
+
# Application Ports
|
| 42 |
+
# ==========================================
|
| 43 |
+
BACKEND_PORT=8000
|
| 44 |
+
FRONTEND_PORT=5173
|
| 45 |
+
|
| 46 |
+
# ==========================================
|
| 47 |
+
# Frontend Environment Variables
|
| 48 |
+
# ==========================================
|
| 49 |
+
VITE_API_BASE_URL=http://localhost:8000
|
| 50 |
+
VITE_WS_URL=ws://localhost:8000/ws
|
| 51 |
+
|
| 52 |
+
# ==========================================
|
| 53 |
+
# Production Deployment Settings
|
| 54 |
+
# ==========================================
|
| 55 |
+
# Data storage path for production volumes
|
| 56 |
+
DATA_PATH=./data
|
| 57 |
+
|
| 58 |
+
# Number of uvicorn workers (production)
|
| 59 |
+
WORKERS=4
|
| 60 |
+
|
| 61 |
+
# ==========================================
|
| 62 |
+
# Optional: Performance & Monitoring
|
| 63 |
+
# ==========================================
|
| 64 |
+
# SENTRY_DSN=your-sentry-dsn-here
|
| 65 |
+
# REDIS_URL=redis://localhost:6379/0
|
.gitattributes
ADDED
|
@@ -0,0 +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
|
.github/workflows/ci-cd-pipeline.yml
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: SAAP CI/CD Pipeline
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main, develop ]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [ main, develop ]
|
| 8 |
+
release:
|
| 9 |
+
types: [ published ]
|
| 10 |
+
|
| 11 |
+
env:
|
| 12 |
+
REGISTRY: ghcr.io
|
| 13 |
+
IMAGE_NAME_BACKEND: ${{ github.repository }}/backend
|
| 14 |
+
IMAGE_NAME_FRONTEND: ${{ github.repository }}/frontend
|
| 15 |
+
|
| 16 |
+
jobs:
|
| 17 |
+
# ==========================================
|
| 18 |
+
# JOB 1: Security Scanning
|
| 19 |
+
# ==========================================
|
| 20 |
+
security-scan:
|
| 21 |
+
name: Security Checks
|
| 22 |
+
runs-on: ubuntu-latest
|
| 23 |
+
steps:
|
| 24 |
+
- name: Checkout code
|
| 25 |
+
uses: actions/checkout@v4
|
| 26 |
+
with:
|
| 27 |
+
fetch-depth: 0
|
| 28 |
+
|
| 29 |
+
- name: Install Gitleaks
|
| 30 |
+
run: |
|
| 31 |
+
wget -q https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_linux_x64.tar.gz
|
| 32 |
+
tar -xzf gitleaks_8.18.4_linux_x64.tar.gz
|
| 33 |
+
sudo mv gitleaks /usr/local/bin/
|
| 34 |
+
gitleaks version
|
| 35 |
+
|
| 36 |
+
- name: Run Gitleaks Scan
|
| 37 |
+
run: |
|
| 38 |
+
gitleaks detect --source . --config .gitleaks.toml --verbose --no-git
|
| 39 |
+
|
| 40 |
+
- name: Python Security Check (Safety)
|
| 41 |
+
run: |
|
| 42 |
+
pip install safety
|
| 43 |
+
safety check --file requirements.txt --output text || true
|
| 44 |
+
|
| 45 |
+
- name: Node.js Security Check (npm audit)
|
| 46 |
+
working-directory: ./frontend
|
| 47 |
+
run: |
|
| 48 |
+
npm install
|
| 49 |
+
npm audit --audit-level=moderate || true
|
| 50 |
+
|
| 51 |
+
# ==========================================
|
| 52 |
+
# JOB 2: Backend Testing (Python/FastAPI)
|
| 53 |
+
# ==========================================
|
| 54 |
+
backend-tests:
|
| 55 |
+
name: Backend Tests (Python)
|
| 56 |
+
runs-on: ubuntu-latest
|
| 57 |
+
needs: security-scan
|
| 58 |
+
|
| 59 |
+
services:
|
| 60 |
+
postgres:
|
| 61 |
+
image: postgres:15-alpine
|
| 62 |
+
env:
|
| 63 |
+
POSTGRES_USER: saap_test
|
| 64 |
+
POSTGRES_PASSWORD: test_password
|
| 65 |
+
POSTGRES_DB: saap_test
|
| 66 |
+
ports:
|
| 67 |
+
- 5432:5432
|
| 68 |
+
options: >-
|
| 69 |
+
--health-cmd pg_isready
|
| 70 |
+
--health-interval 10s
|
| 71 |
+
--health-timeout 5s
|
| 72 |
+
--health-retries 5
|
| 73 |
+
|
| 74 |
+
steps:
|
| 75 |
+
- name: Checkout code
|
| 76 |
+
uses: actions/checkout@v4
|
| 77 |
+
|
| 78 |
+
- name: Set up Python 3.11
|
| 79 |
+
uses: actions/setup-python@v5
|
| 80 |
+
with:
|
| 81 |
+
python-version: '3.11'
|
| 82 |
+
cache: 'pip'
|
| 83 |
+
|
| 84 |
+
- name: Install dependencies
|
| 85 |
+
run: |
|
| 86 |
+
python -m pip install --upgrade pip
|
| 87 |
+
pip install -r requirements.txt
|
| 88 |
+
pip install pytest pytest-cov pytest-asyncio
|
| 89 |
+
|
| 90 |
+
- name: Run Backend Tests
|
| 91 |
+
env:
|
| 92 |
+
DATABASE_URL: postgresql://saap_test:test_password@localhost:5432/saap_test
|
| 93 |
+
PYTHONPATH: ${{ github.workspace }}/backend
|
| 94 |
+
run: |
|
| 95 |
+
pytest backend/ -v --cov=backend --cov-report=xml --cov-report=term
|
| 96 |
+
|
| 97 |
+
- name: Upload Coverage to Codecov
|
| 98 |
+
uses: codecov/codecov-action@v3
|
| 99 |
+
with:
|
| 100 |
+
files: ./coverage.xml
|
| 101 |
+
flags: backend
|
| 102 |
+
name: backend-coverage
|
| 103 |
+
|
| 104 |
+
# ==========================================
|
| 105 |
+
# JOB 3: Frontend Testing (Vue.js)
|
| 106 |
+
# ==========================================
|
| 107 |
+
frontend-tests:
|
| 108 |
+
name: Frontend Tests (Vue.js)
|
| 109 |
+
runs-on: ubuntu-latest
|
| 110 |
+
needs: security-scan
|
| 111 |
+
|
| 112 |
+
steps:
|
| 113 |
+
- name: Checkout code
|
| 114 |
+
uses: actions/checkout@v4
|
| 115 |
+
|
| 116 |
+
- name: Set up Node.js 20
|
| 117 |
+
uses: actions/setup-node@v4
|
| 118 |
+
with:
|
| 119 |
+
node-version: '20'
|
| 120 |
+
cache: 'npm'
|
| 121 |
+
cache-dependency-path: frontend/package-lock.json
|
| 122 |
+
|
| 123 |
+
- name: Install dependencies
|
| 124 |
+
working-directory: ./frontend
|
| 125 |
+
run: npm ci
|
| 126 |
+
|
| 127 |
+
- name: Run ESLint
|
| 128 |
+
working-directory: ./frontend
|
| 129 |
+
run: npm run lint || true
|
| 130 |
+
|
| 131 |
+
- name: Run Frontend Tests
|
| 132 |
+
working-directory: ./frontend
|
| 133 |
+
run: npm run test:unit || echo "No tests configured yet"
|
| 134 |
+
|
| 135 |
+
- name: Build Frontend
|
| 136 |
+
working-directory: ./frontend
|
| 137 |
+
run: npm run build
|
| 138 |
+
|
| 139 |
+
# ==========================================
|
| 140 |
+
# JOB 4: Build Docker Images
|
| 141 |
+
# ==========================================
|
| 142 |
+
build-docker-images:
|
| 143 |
+
name: Build Docker Images
|
| 144 |
+
runs-on: ubuntu-latest
|
| 145 |
+
needs: [backend-tests, frontend-tests]
|
| 146 |
+
permissions:
|
| 147 |
+
contents: read
|
| 148 |
+
packages: write
|
| 149 |
+
|
| 150 |
+
steps:
|
| 151 |
+
- name: Checkout code
|
| 152 |
+
uses: actions/checkout@v4
|
| 153 |
+
|
| 154 |
+
- name: Set up Docker Buildx
|
| 155 |
+
uses: docker/setup-buildx-action@v3
|
| 156 |
+
|
| 157 |
+
- name: Log in to GitHub Container Registry
|
| 158 |
+
uses: docker/login-action@v3
|
| 159 |
+
with:
|
| 160 |
+
registry: ${{ env.REGISTRY }}
|
| 161 |
+
username: ${{ github.actor }}
|
| 162 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 163 |
+
|
| 164 |
+
- name: Extract metadata (tags, labels) for Backend
|
| 165 |
+
id: meta-backend
|
| 166 |
+
uses: docker/metadata-action@v5
|
| 167 |
+
with:
|
| 168 |
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}
|
| 169 |
+
tags: |
|
| 170 |
+
type=ref,event=branch
|
| 171 |
+
type=ref,event=pr
|
| 172 |
+
type=semver,pattern={{version}}
|
| 173 |
+
type=semver,pattern={{major}}.{{minor}}
|
| 174 |
+
type=sha,prefix={{branch}}-
|
| 175 |
+
|
| 176 |
+
- name: Extract metadata (tags, labels) for Frontend
|
| 177 |
+
id: meta-frontend
|
| 178 |
+
uses: docker/metadata-action@v5
|
| 179 |
+
with:
|
| 180 |
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}
|
| 181 |
+
tags: |
|
| 182 |
+
type=ref,event=branch
|
| 183 |
+
type=ref,event=pr
|
| 184 |
+
type=semver,pattern={{version}}
|
| 185 |
+
type=semver,pattern={{major}}.{{minor}}
|
| 186 |
+
type=sha,prefix={{branch}}-
|
| 187 |
+
|
| 188 |
+
- name: Build and push Backend Docker image
|
| 189 |
+
uses: docker/build-push-action@v5
|
| 190 |
+
with:
|
| 191 |
+
context: ./backend
|
| 192 |
+
file: ./backend/Dockerfile
|
| 193 |
+
push: ${{ github.event_name != 'pull_request' }}
|
| 194 |
+
tags: ${{ steps.meta-backend.outputs.tags }}
|
| 195 |
+
labels: ${{ steps.meta-backend.outputs.labels }}
|
| 196 |
+
cache-from: type=gha
|
| 197 |
+
cache-to: type=gha,mode=max
|
| 198 |
+
|
| 199 |
+
- name: Build and push Frontend Docker image
|
| 200 |
+
uses: docker/build-push-action@v5
|
| 201 |
+
with:
|
| 202 |
+
context: ./frontend
|
| 203 |
+
file: ./frontend/Dockerfile
|
| 204 |
+
push: ${{ github.event_name != 'pull_request' }}
|
| 205 |
+
tags: ${{ steps.meta-frontend.outputs.tags }}
|
| 206 |
+
labels: ${{ steps.meta-frontend.outputs.labels }}
|
| 207 |
+
cache-from: type=gha
|
| 208 |
+
cache-to: type=gha,mode=max
|
| 209 |
+
|
| 210 |
+
# ==========================================
|
| 211 |
+
# JOB 5: Create Deployment Package
|
| 212 |
+
# ==========================================
|
| 213 |
+
create-deployment-package:
|
| 214 |
+
name: Create Local Deployment Package
|
| 215 |
+
runs-on: ubuntu-latest
|
| 216 |
+
needs: build-docker-images
|
| 217 |
+
if: github.event_name == 'release'
|
| 218 |
+
|
| 219 |
+
steps:
|
| 220 |
+
- name: Checkout code
|
| 221 |
+
uses: actions/checkout@v4
|
| 222 |
+
|
| 223 |
+
- name: Create deployment package
|
| 224 |
+
run: |
|
| 225 |
+
mkdir -p deployment-package
|
| 226 |
+
cp docker-compose.yml deployment-package/
|
| 227 |
+
cp docker-compose.prod.yml deployment-package/
|
| 228 |
+
cp .env.example deployment-package/.env
|
| 229 |
+
cp README.md deployment-package/
|
| 230 |
+
cp QUICKSTART.md deployment-package/
|
| 231 |
+
|
| 232 |
+
# Create deployment instructions
|
| 233 |
+
cat > deployment-package/DEPLOYMENT.md << 'EOF'
|
| 234 |
+
# SAAP Lokales Deployment
|
| 235 |
+
|
| 236 |
+
## Voraussetzungen
|
| 237 |
+
- Docker 24.0 oder höher
|
| 238 |
+
- Docker Compose 2.20 oder höher
|
| 239 |
+
- 4 GB RAM minimum
|
| 240 |
+
- 10 GB Festplattenspeicher
|
| 241 |
+
|
| 242 |
+
## Schnellstart
|
| 243 |
+
|
| 244 |
+
1. **Konfiguration anpassen:**
|
| 245 |
+
```bash
|
| 246 |
+
cp .env.example .env
|
| 247 |
+
# .env-Datei editieren und API-Keys eintragen
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
2. **SAAP starten:**
|
| 251 |
+
```bash
|
| 252 |
+
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
3. **Status überprüfen:**
|
| 256 |
+
```bash
|
| 257 |
+
docker-compose ps
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
4. **Anwendung öffnen:**
|
| 261 |
+
- Frontend: http://localhost:5173
|
| 262 |
+
- Backend API: http://localhost:8000
|
| 263 |
+
- API Docs: http://localhost:8000/docs
|
| 264 |
+
|
| 265 |
+
## Verwaltung
|
| 266 |
+
|
| 267 |
+
- **Logs anzeigen:** `docker-compose logs -f`
|
| 268 |
+
- **Neustart:** `docker-compose restart`
|
| 269 |
+
- **Stoppen:** `docker-compose down`
|
| 270 |
+
- **Updates:** `docker-compose pull && docker-compose up -d`
|
| 271 |
+
|
| 272 |
+
## Kostenvergleich: Lokal vs. Cloud
|
| 273 |
+
|
| 274 |
+
| Kriterium | Lokal (SAAP) | AWS/Azure Cloud |
|
| 275 |
+
|-----------|--------------|-----------------|
|
| 276 |
+
| Monatliche Kosten | €0 (nur Stromkosten) | €200-500+ |
|
| 277 |
+
| Datenschutz | Vollständig lokal | Externen Servern |
|
| 278 |
+
| Latenz | <10ms | 50-200ms |
|
| 279 |
+
| Skalierung | Manuell | Automatisch |
|
| 280 |
+
| Wartung | Selbst | Managed |
|
| 281 |
+
|
| 282 |
+
## Support
|
| 283 |
+
Bei Fragen: https://github.com/satwareAG/saap/issues
|
| 284 |
+
EOF
|
| 285 |
+
|
| 286 |
+
- name: Create archive
|
| 287 |
+
run: |
|
| 288 |
+
cd deployment-package
|
| 289 |
+
tar -czf ../saap-deployment-${{ github.ref_name }}.tar.gz .
|
| 290 |
+
|
| 291 |
+
- name: Upload deployment package
|
| 292 |
+
uses: actions/upload-artifact@v4
|
| 293 |
+
with:
|
| 294 |
+
name: saap-deployment-package
|
| 295 |
+
path: saap-deployment-${{ github.ref_name }}.tar.gz
|
| 296 |
+
|
| 297 |
+
- name: Upload to Release
|
| 298 |
+
uses: softprops/action-gh-release@v1
|
| 299 |
+
if: github.event_name == 'release'
|
| 300 |
+
with:
|
| 301 |
+
files: saap-deployment-${{ github.ref_name }}.tar.gz
|
| 302 |
+
env:
|
| 303 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 304 |
+
|
| 305 |
+
# ==========================================
|
| 306 |
+
# JOB 6: Deploy to HuggingFace Spaces
|
| 307 |
+
# ==========================================
|
| 308 |
+
deploy-huggingface:
|
| 309 |
+
name: Deploy to HuggingFace Spaces
|
| 310 |
+
runs-on: ubuntu-latest
|
| 311 |
+
needs: [backend-tests, frontend-tests]
|
| 312 |
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
| 313 |
+
|
| 314 |
+
steps:
|
| 315 |
+
- name: Checkout code
|
| 316 |
+
uses: actions/checkout@v4
|
| 317 |
+
with:
|
| 318 |
+
fetch-depth: 0
|
| 319 |
+
|
| 320 |
+
- name: Setup Python
|
| 321 |
+
uses: actions/setup-python@v5
|
| 322 |
+
with:
|
| 323 |
+
python-version: '3.11'
|
| 324 |
+
|
| 325 |
+
- name: Install HuggingFace Hub
|
| 326 |
+
run: pip install huggingface_hub
|
| 327 |
+
|
| 328 |
+
- name: Debug - Check HF_TOKEN
|
| 329 |
+
run: |
|
| 330 |
+
if [ -z "${{ secrets.HF_TOKEN }}" ]; then
|
| 331 |
+
echo "❌ ERROR: HF_TOKEN secret not configured"
|
| 332 |
+
echo "Please add HF_TOKEN to repository secrets"
|
| 333 |
+
exit 1
|
| 334 |
+
else
|
| 335 |
+
echo "✅ HF_TOKEN is configured"
|
| 336 |
+
echo "Token length: ${#HF_TOKEN}"
|
| 337 |
+
fi
|
| 338 |
+
env:
|
| 339 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 340 |
+
|
| 341 |
+
- name: Prepare deployment files
|
| 342 |
+
run: |
|
| 343 |
+
echo "📦 Preparing HuggingFace deployment files..."
|
| 344 |
+
|
| 345 |
+
# Create huggingface deployment directory
|
| 346 |
+
mkdir -p huggingface_deploy
|
| 347 |
+
|
| 348 |
+
# Copy all necessary files
|
| 349 |
+
cp -r huggingface/* huggingface_deploy/
|
| 350 |
+
cp -r backend huggingface_deploy/
|
| 351 |
+
cp -r frontend huggingface_deploy/
|
| 352 |
+
cp requirements.txt huggingface_deploy/
|
| 353 |
+
|
| 354 |
+
# Verify critical files exist
|
| 355 |
+
echo "📋 Verifying deployment files:"
|
| 356 |
+
ls -la huggingface_deploy/
|
| 357 |
+
|
| 358 |
+
if [ ! -f "huggingface_deploy/Dockerfile" ]; then
|
| 359 |
+
echo "❌ ERROR: Dockerfile missing"
|
| 360 |
+
exit 1
|
| 361 |
+
fi
|
| 362 |
+
|
| 363 |
+
if [ ! -f "huggingface_deploy/README.md" ]; then
|
| 364 |
+
echo "⚠️ WARNING: README.md missing - creating default"
|
| 365 |
+
cat > huggingface_deploy/README.md << 'EOF'
|
| 366 |
+
---
|
| 367 |
+
title: SAAP - satware AI Autonomous Agent Platform
|
| 368 |
+
emoji: 🤖
|
| 369 |
+
colorFrom: purple
|
| 370 |
+
colorTo: blue
|
| 371 |
+
sdk: docker
|
| 372 |
+
app_port: 7860
|
| 373 |
+
pinned: false
|
| 374 |
+
license: mit
|
| 375 |
+
---
|
| 376 |
+
|
| 377 |
+
# SAAP - satware® AI Autonomous Agent Platform
|
| 378 |
+
|
| 379 |
+
Local autonomous multi-agent system for specialized AI agents.
|
| 380 |
+
|
| 381 |
+
**Features:**
|
| 382 |
+
- Multi-agent coordination (Jane, John, Lara, Theo, Justus, Leon, Luna)
|
| 383 |
+
- Real-time WebSocket communication
|
| 384 |
+
- Cost-efficient hybrid provider support
|
| 385 |
+
- Privacy-first local deployment
|
| 386 |
+
EOF
|
| 387 |
+
fi
|
| 388 |
+
|
| 389 |
+
echo "✅ Deployment files prepared"
|
| 390 |
+
|
| 391 |
+
- name: Upload to HuggingFace Space
|
| 392 |
+
env:
|
| 393 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 394 |
+
run: |
|
| 395 |
+
echo "🚀 Deploying to HuggingFace Spaces..."
|
| 396 |
+
|
| 397 |
+
cd huggingface_deploy
|
| 398 |
+
|
| 399 |
+
python << 'DEPLOY_SCRIPT'
|
| 400 |
+
import os
|
| 401 |
+
import sys
|
| 402 |
+
from huggingface_hub import HfApi, create_repo
|
| 403 |
+
|
| 404 |
+
# Configuration
|
| 405 |
+
repo_id = "Hwandji/saap"
|
| 406 |
+
token = os.environ.get("HF_TOKEN")
|
| 407 |
+
|
| 408 |
+
if not token:
|
| 409 |
+
print("❌ HF_TOKEN not found")
|
| 410 |
+
sys.exit(1)
|
| 411 |
+
|
| 412 |
+
print(f"📤 Uploading to HuggingFace Space: {repo_id}")
|
| 413 |
+
|
| 414 |
+
try:
|
| 415 |
+
api = HfApi(token=token)
|
| 416 |
+
|
| 417 |
+
# Upload all files
|
| 418 |
+
api.upload_folder(
|
| 419 |
+
folder_path=".",
|
| 420 |
+
repo_id=repo_id,
|
| 421 |
+
repo_type="space",
|
| 422 |
+
commit_message=f"Deploy from GitHub Actions - {os.environ.get('GITHUB_SHA', 'unknown')[:7]}"
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
print("✅ Successfully uploaded to HuggingFace")
|
| 426 |
+
print(f"🌐 Space URL: https://huggingface.co/spaces/{repo_id}")
|
| 427 |
+
|
| 428 |
+
except Exception as e:
|
| 429 |
+
print(f"❌ Deployment failed: {e}")
|
| 430 |
+
sys.exit(1)
|
| 431 |
+
DEPLOY_SCRIPT
|
| 432 |
+
|
| 433 |
+
- name: Deployment summary
|
| 434 |
+
run: |
|
| 435 |
+
echo "📊 Deployment Summary"
|
| 436 |
+
echo "===================="
|
| 437 |
+
echo "✅ Files uploaded to HuggingFace Spaces"
|
| 438 |
+
echo "🌐 Space: https://huggingface.co/spaces/Hwandji/saap"
|
| 439 |
+
echo "⏳ Note: Space restart may take 2-3 minutes"
|
| 440 |
+
echo "🔍 Check logs at: https://huggingface.co/spaces/Hwandji/saap/logs"
|
| 441 |
+
|
| 442 |
+
# ==========================================
|
| 443 |
+
# JOB 7: Deployment Success Notification
|
| 444 |
+
# ==========================================
|
| 445 |
+
notify-deployment:
|
| 446 |
+
name: Deployment Status
|
| 447 |
+
runs-on: ubuntu-latest
|
| 448 |
+
needs: [build-docker-images, create-deployment-package, deploy-huggingface]
|
| 449 |
+
if: always()
|
| 450 |
+
|
| 451 |
+
steps:
|
| 452 |
+
- name: Check deployment status
|
| 453 |
+
run: |
|
| 454 |
+
if [ "${{ needs.build-docker-images.result }}" = "success" ]; then
|
| 455 |
+
echo "✅ Docker Images erfolgreich gebaut"
|
| 456 |
+
echo "📦 Images verfügbar in GitHub Container Registry"
|
| 457 |
+
echo "🚀 Deployment-Package bereit für lokale Installation"
|
| 458 |
+
else
|
| 459 |
+
echo "❌ Deployment fehlgeschlagen"
|
| 460 |
+
exit 1
|
| 461 |
+
fi
|
| 462 |
+
|
| 463 |
+
if [ "${{ needs.deploy-huggingface.result }}" = "success" ]; then
|
| 464 |
+
echo "✅ HuggingFace Deployment erfolgreich"
|
| 465 |
+
echo "🌐 Space: https://huggingface.co/spaces/Hwandji/saap"
|
| 466 |
+
elif [ "${{ needs.deploy-huggingface.result }}" = "skipped" ]; then
|
| 467 |
+
echo "⏭️ HuggingFace Deployment übersprungen (nicht main branch)"
|
| 468 |
+
else
|
| 469 |
+
echo "❌ HuggingFace Deployment fehlgeschlagen"
|
| 470 |
+
fi
|
.github/workflows/deploy-huggingface.yml
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to HuggingFace Spaces
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
inputs:
|
| 9 |
+
reason:
|
| 10 |
+
description: 'Deployment reason'
|
| 11 |
+
required: false
|
| 12 |
+
default: 'Manual deployment'
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
deploy:
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
|
| 18 |
+
steps:
|
| 19 |
+
- name: Checkout repository
|
| 20 |
+
uses: actions/checkout@v4
|
| 21 |
+
with:
|
| 22 |
+
lfs: true
|
| 23 |
+
fetch-depth: 0
|
| 24 |
+
|
| 25 |
+
- name: Setup Python
|
| 26 |
+
uses: actions/setup-python@v5
|
| 27 |
+
with:
|
| 28 |
+
python-version: '3.10'
|
| 29 |
+
|
| 30 |
+
- name: Install HuggingFace Hub
|
| 31 |
+
run: |
|
| 32 |
+
pip install --upgrade huggingface_hub[cli]
|
| 33 |
+
echo "✅ HuggingFace Hub installed"
|
| 34 |
+
|
| 35 |
+
- name: Debug - Check HF_TOKEN
|
| 36 |
+
env:
|
| 37 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 38 |
+
run: |
|
| 39 |
+
# Check if token is set (without revealing it)
|
| 40 |
+
if [ -z "$HF_TOKEN" ]; then
|
| 41 |
+
echo "❌ ERROR: HF_TOKEN is not set!"
|
| 42 |
+
exit 1
|
| 43 |
+
else
|
| 44 |
+
echo "✅ HF_TOKEN is set (length: ${#HF_TOKEN} chars)"
|
| 45 |
+
fi
|
| 46 |
+
|
| 47 |
+
- name: Prepare deployment files
|
| 48 |
+
run: |
|
| 49 |
+
echo "Preparing files for HuggingFace Spaces deployment..."
|
| 50 |
+
|
| 51 |
+
# Create temporary deployment directory
|
| 52 |
+
mkdir -p hf-deploy
|
| 53 |
+
|
| 54 |
+
# Copy all necessary files
|
| 55 |
+
cp -r backend/ hf-deploy/
|
| 56 |
+
cp -r frontend/ hf-deploy/
|
| 57 |
+
cp requirements.txt hf-deploy/
|
| 58 |
+
cp huggingface/Dockerfile hf-deploy/Dockerfile
|
| 59 |
+
cp huggingface/README.md hf-deploy/README.md
|
| 60 |
+
cp huggingface/nginx.conf hf-deploy/
|
| 61 |
+
cp huggingface/supervisord.conf hf-deploy/
|
| 62 |
+
|
| 63 |
+
# Copy .dockerignore if it exists
|
| 64 |
+
if [ -f "huggingface/.dockerignore" ]; then
|
| 65 |
+
cp huggingface/.dockerignore hf-deploy/.dockerignore
|
| 66 |
+
echo "✅ .dockerignore copied"
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
echo "✅ Files prepared in hf-deploy/"
|
| 70 |
+
|
| 71 |
+
- name: Upload to HuggingFace Space
|
| 72 |
+
env:
|
| 73 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 74 |
+
run: |
|
| 75 |
+
echo "Uploading to HuggingFace Space: Hwandji/saap"
|
| 76 |
+
|
| 77 |
+
# Create Python script for upload
|
| 78 |
+
cat > upload_to_hf.py << 'EOF'
|
| 79 |
+
import os
|
| 80 |
+
from pathlib import Path
|
| 81 |
+
from huggingface_hub import HfApi, login, create_repo
|
| 82 |
+
|
| 83 |
+
# Login with token
|
| 84 |
+
token = os.environ.get('HF_TOKEN')
|
| 85 |
+
if not token:
|
| 86 |
+
raise ValueError("HF_TOKEN not found in environment")
|
| 87 |
+
|
| 88 |
+
login(token=token)
|
| 89 |
+
print("✅ Successfully logged in to HuggingFace")
|
| 90 |
+
|
| 91 |
+
# Initialize API
|
| 92 |
+
api = HfApi()
|
| 93 |
+
|
| 94 |
+
# Create or get Space repository
|
| 95 |
+
repo_id = "Hwandji/saap"
|
| 96 |
+
print(f"Creating or accessing Space: {repo_id}...")
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
# Try to create the Space (will succeed if doesn't exist, harmless if exists)
|
| 100 |
+
create_repo(
|
| 101 |
+
repo_id=repo_id,
|
| 102 |
+
repo_type="space",
|
| 103 |
+
space_sdk="docker",
|
| 104 |
+
exist_ok=True, # Don't error if already exists
|
| 105 |
+
private=False
|
| 106 |
+
)
|
| 107 |
+
print(f"✅ Space {repo_id} is ready")
|
| 108 |
+
except Exception as e:
|
| 109 |
+
print(f"⚠️ Note: {e}")
|
| 110 |
+
print("Continuing with upload...")
|
| 111 |
+
|
| 112 |
+
# Upload directory
|
| 113 |
+
print(f"Uploading files to {repo_id}...")
|
| 114 |
+
api.upload_folder(
|
| 115 |
+
folder_path="./hf-deploy",
|
| 116 |
+
repo_id=repo_id,
|
| 117 |
+
repo_type="space",
|
| 118 |
+
commit_message="🚀 Deploy from GitHub Actions",
|
| 119 |
+
ignore_patterns=[".git", ".github", "__pycache__", "*.pyc"]
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
print("✅ Successfully deployed to HuggingFace Spaces")
|
| 123 |
+
print(f"🌐 Space URL: https://huggingface.co/spaces/{repo_id}")
|
| 124 |
+
print(f"🌐 App URL: https://{repo_id.replace('/', '-')}.hf.space")
|
| 125 |
+
EOF
|
| 126 |
+
|
| 127 |
+
# Run the upload script
|
| 128 |
+
python upload_to_hf.py
|
| 129 |
+
|
| 130 |
+
- name: Deployment summary
|
| 131 |
+
if: success()
|
| 132 |
+
run: |
|
| 133 |
+
echo "## 🎉 Deployment Successful!" >> $GITHUB_STEP_SUMMARY
|
| 134 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 135 |
+
echo "**Space URL:** https://huggingface.co/spaces/Hwandji/saap" >> $GITHUB_STEP_SUMMARY
|
| 136 |
+
echo "**App URL:** https://Hwandji-saap.hf.space" >> $GITHUB_STEP_SUMMARY
|
| 137 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 138 |
+
echo "⏱️ The space may take 2-3 minutes to build and start." >> $GITHUB_STEP_SUMMARY
|
| 139 |
+
|
| 140 |
+
- name: Notify on failure
|
| 141 |
+
if: failure()
|
| 142 |
+
run: |
|
| 143 |
+
echo "## ❌ Deployment Failed!" >> $GITHUB_STEP_SUMMARY
|
| 144 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 145 |
+
echo "Please check the logs above for error details." >> $GITHUB_STEP_SUMMARY
|
| 146 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 147 |
+
echo "**Common issues:**" >> $GITHUB_STEP_SUMMARY
|
| 148 |
+
echo "- HF_TOKEN not configured in repository secrets" >> $GITHUB_STEP_SUMMARY
|
| 149 |
+
echo "- Token lacks WRITE permissions for Spaces" >> $GITHUB_STEP_SUMMARY
|
| 150 |
+
echo "- Token creator is not a member of 'satware' organization" >> $GITHUB_STEP_SUMMARY
|
| 151 |
+
echo "- Space 'Hwandji/saap' does not exist or is not accessible" >> $GITHUB_STEP_SUMMARY
|
.gitignore
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# SAAP Project - Comprehensive .gitignore
|
| 3 |
+
# ==========================================
|
| 4 |
+
|
| 5 |
+
# ============ SECRETS & CREDENTIALS (CRITICAL) ============
|
| 6 |
+
# Never commit any files with secrets, API keys, or credentials
|
| 7 |
+
.env
|
| 8 |
+
.env.*
|
| 9 |
+
!.env.example
|
| 10 |
+
*.pem
|
| 11 |
+
*.key
|
| 12 |
+
*.p12
|
| 13 |
+
*.pfx
|
| 14 |
+
*.crt
|
| 15 |
+
*.cer
|
| 16 |
+
*.der
|
| 17 |
+
*secret*
|
| 18 |
+
*credential*
|
| 19 |
+
*password*
|
| 20 |
+
*token*
|
| 21 |
+
*.config.local.*
|
| 22 |
+
|
| 23 |
+
# SSH keys
|
| 24 |
+
id_rsa
|
| 25 |
+
id_dsa
|
| 26 |
+
id_ecdsa
|
| 27 |
+
id_ed25519
|
| 28 |
+
|
| 29 |
+
# ============ PYTHON ============
|
| 30 |
+
# Byte-compiled / optimized / DLL files
|
| 31 |
+
__pycache__/
|
| 32 |
+
*.py[cod]
|
| 33 |
+
*$py.class
|
| 34 |
+
|
| 35 |
+
# Virtual environments
|
| 36 |
+
venv/
|
| 37 |
+
env/
|
| 38 |
+
ENV/
|
| 39 |
+
saap-env/
|
| 40 |
+
.venv/
|
| 41 |
+
.Python
|
| 42 |
+
|
| 43 |
+
# Distribution / packaging
|
| 44 |
+
build/
|
| 45 |
+
dist/
|
| 46 |
+
*.egg-info/
|
| 47 |
+
.eggs/
|
| 48 |
+
*.egg
|
| 49 |
+
|
| 50 |
+
# PyInstaller
|
| 51 |
+
*.manifest
|
| 52 |
+
*.spec
|
| 53 |
+
|
| 54 |
+
# Unit test / coverage reports
|
| 55 |
+
htmlcov/
|
| 56 |
+
.tox/
|
| 57 |
+
.coverage
|
| 58 |
+
.coverage.*
|
| 59 |
+
.cache
|
| 60 |
+
nosetests.xml
|
| 61 |
+
coverage.xml
|
| 62 |
+
*.cover
|
| 63 |
+
.hypothesis/
|
| 64 |
+
.pytest_cache/
|
| 65 |
+
|
| 66 |
+
# Jupyter Notebook
|
| 67 |
+
.ipynb_checkpoints
|
| 68 |
+
|
| 69 |
+
# pyenv
|
| 70 |
+
.python-version
|
| 71 |
+
|
| 72 |
+
# Celery
|
| 73 |
+
celerybeat-schedule
|
| 74 |
+
|
| 75 |
+
# mypy
|
| 76 |
+
.mypy_cache/
|
| 77 |
+
.dmypy.json
|
| 78 |
+
dmypy.json
|
| 79 |
+
|
| 80 |
+
# ============ NODE.JS / JAVASCRIPT ============
|
| 81 |
+
# Dependencies
|
| 82 |
+
node_modules/
|
| 83 |
+
npm-debug.log*
|
| 84 |
+
yarn-debug.log*
|
| 85 |
+
yarn-error.log*
|
| 86 |
+
pnpm-debug.log*
|
| 87 |
+
lerna-debug.log*
|
| 88 |
+
|
| 89 |
+
# Build outputs
|
| 90 |
+
dist/
|
| 91 |
+
dist-ssr/
|
| 92 |
+
*.local
|
| 93 |
+
|
| 94 |
+
# Editor directories and files
|
| 95 |
+
.vscode/*
|
| 96 |
+
!.vscode/extensions.json
|
| 97 |
+
!.vscode/settings.json
|
| 98 |
+
.idea/
|
| 99 |
+
*.suo
|
| 100 |
+
*.ntvs*
|
| 101 |
+
*.njsproj
|
| 102 |
+
*.sln
|
| 103 |
+
*.sw?
|
| 104 |
+
|
| 105 |
+
# ============ JAVA ============
|
| 106 |
+
# Compiled class files
|
| 107 |
+
*.class
|
| 108 |
+
|
| 109 |
+
# Log file
|
| 110 |
+
*.log
|
| 111 |
+
|
| 112 |
+
# Package Files
|
| 113 |
+
*.jar
|
| 114 |
+
*.war
|
| 115 |
+
*.nar
|
| 116 |
+
*.ear
|
| 117 |
+
*.zip
|
| 118 |
+
*.tar.gz
|
| 119 |
+
*.rar
|
| 120 |
+
|
| 121 |
+
# Maven
|
| 122 |
+
target/
|
| 123 |
+
pom.xml.tag
|
| 124 |
+
pom.xml.releaseBackup
|
| 125 |
+
pom.xml.versionsBackup
|
| 126 |
+
pom.xml.next
|
| 127 |
+
release.properties
|
| 128 |
+
dependency-reduced-pom.xml
|
| 129 |
+
buildNumber.properties
|
| 130 |
+
.mvn/timing.properties
|
| 131 |
+
.mvn/wrapper/maven-wrapper.jar
|
| 132 |
+
|
| 133 |
+
# Gradle
|
| 134 |
+
.gradle
|
| 135 |
+
**/build/
|
| 136 |
+
!src/**/build/
|
| 137 |
+
gradle-app.setting
|
| 138 |
+
!gradle-wrapper.jar
|
| 139 |
+
!gradle-wrapper.properties
|
| 140 |
+
.gradletasknamecache
|
| 141 |
+
|
| 142 |
+
# IntelliJ IDEA
|
| 143 |
+
.idea/
|
| 144 |
+
*.iws
|
| 145 |
+
*.iml
|
| 146 |
+
*.ipr
|
| 147 |
+
out/
|
| 148 |
+
|
| 149 |
+
# ============ DOCKER ============
|
| 150 |
+
# Docker files that might contain secrets
|
| 151 |
+
docker-compose.override.yml
|
| 152 |
+
.dockerignore
|
| 153 |
+
|
| 154 |
+
# ============ DATABASE ============
|
| 155 |
+
# Database files
|
| 156 |
+
*.db
|
| 157 |
+
*.sqlite
|
| 158 |
+
*.sqlite3
|
| 159 |
+
*.sql
|
| 160 |
+
|
| 161 |
+
# PostgreSQL
|
| 162 |
+
postgresql_data/
|
| 163 |
+
|
| 164 |
+
# Redis
|
| 165 |
+
dump.rdb
|
| 166 |
+
|
| 167 |
+
# ============ OPERATING SYSTEM ============
|
| 168 |
+
# macOS
|
| 169 |
+
.DS_Store
|
| 170 |
+
.AppleDouble
|
| 171 |
+
.LSOverride
|
| 172 |
+
._*
|
| 173 |
+
|
| 174 |
+
# Linux
|
| 175 |
+
*~
|
| 176 |
+
.directory
|
| 177 |
+
.Trash-*
|
| 178 |
+
|
| 179 |
+
# Windows
|
| 180 |
+
Thumbs.db
|
| 181 |
+
Thumbs.db:encryptable
|
| 182 |
+
ehthumbs.db
|
| 183 |
+
ehthumbs_vista.db
|
| 184 |
+
Desktop.ini
|
| 185 |
+
$RECYCLE.BIN/
|
| 186 |
+
|
| 187 |
+
# ============ LOGS & TEMPORARY FILES ============
|
| 188 |
+
logs/
|
| 189 |
+
*.log
|
| 190 |
+
*.tmp
|
| 191 |
+
*.temp
|
| 192 |
+
*.swp
|
| 193 |
+
*.swo
|
| 194 |
+
*~
|
| 195 |
+
|
| 196 |
+
# ============ BACKUP FILES ============
|
| 197 |
+
# Exclude backup directories and files
|
| 198 |
+
*_backup/
|
| 199 |
+
*_backup.*
|
| 200 |
+
*.bak
|
| 201 |
+
*.backup
|
| 202 |
+
*.old
|
| 203 |
+
|
| 204 |
+
# ============ PROJECT-SPECIFIC ============
|
| 205 |
+
# SAAP specific excludes
|
| 206 |
+
# Exclude files with hardcoded secrets (from le-chantier migration)
|
| 207 |
+
main_complete_solution.py
|
| 208 |
+
fix_chat_errors.py
|
| 209 |
+
|
| 210 |
+
# Test data with potential sensitive information
|
| 211 |
+
test_data/
|
| 212 |
+
fixtures/
|
| 213 |
+
|
| 214 |
+
# AI model cache
|
| 215 |
+
.ai_cache/
|
| 216 |
+
models_cache/
|
| 217 |
+
|
| 218 |
+
# Performance profiling outputs
|
| 219 |
+
*.prof
|
| 220 |
+
*.pstats
|
.gitleaks.toml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gitleaks Configuration for SAAP
|
| 2 |
+
# Allows documentation files with example API keys
|
| 3 |
+
|
| 4 |
+
[allowlist]
|
| 5 |
+
description = "Allow example API keys in security documentation"
|
| 6 |
+
|
| 7 |
+
# Allow findings in documentation files
|
| 8 |
+
paths = [
|
| 9 |
+
'''SECURITY_SETUP_COMPLETE\.md''',
|
| 10 |
+
'''SECURITY_SCAN_REPORT\.md''',
|
| 11 |
+
'''SECURITY_REMEDIATION_REQUIRED\.md''',
|
| 12 |
+
'''README\.md''',
|
| 13 |
+
'''DEPLOYMENT\.md''',
|
| 14 |
+
'''TESTING_CICD\.md'''
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
# Allow example/placeholder API keys
|
| 18 |
+
regexes = [
|
| 19 |
+
'''(sk|msk)-dBoxml3krytIRLdjr35Lnw''', # Example key from docs
|
| 20 |
+
'''\{\{COLOSSUS_API_KEY\}\}''', # Template placeholder
|
| 21 |
+
'''\{\{OPENROUTER_API_KEY\}\}''', # Template placeholder
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
[extend]
|
| 25 |
+
# Use default Gitleaks rules
|
| 26 |
+
useDefault = true
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pre-commit hooks for SAAP - Security & Code Quality
|
| 2 |
+
# Installation: sudo pacman -S pre-commit && pre-commit install
|
| 3 |
+
# Manual run: pre-commit run --all-files
|
| 4 |
+
|
| 5 |
+
repos:
|
| 6 |
+
- repo: https://github.com/gitleaks/gitleaks
|
| 7 |
+
rev: v8.27.2
|
| 8 |
+
hooks:
|
| 9 |
+
- id: gitleaks
|
| 10 |
+
name: Gitleaks - Secret Detection
|
| 11 |
+
description: Scan for hardcoded secrets (API keys, passwords, tokens)
|
| 12 |
+
entry: gitleaks protect --staged --redact --verbose
|
| 13 |
+
language: system
|
| 14 |
+
pass_filenames: false
|
| 15 |
+
|
| 16 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 17 |
+
rev: v4.5.0
|
| 18 |
+
hooks:
|
| 19 |
+
- id: trailing-whitespace
|
| 20 |
+
name: Trim Trailing Whitespace
|
| 21 |
+
- id: end-of-file-fixer
|
| 22 |
+
name: Fix End of Files
|
| 23 |
+
- id: check-yaml
|
| 24 |
+
name: Check YAML Syntax
|
| 25 |
+
- id: check-json
|
| 26 |
+
name: Check JSON Syntax
|
| 27 |
+
- id: check-merge-conflict
|
| 28 |
+
name: Check for Merge Conflicts
|
| 29 |
+
- id: detect-private-key
|
| 30 |
+
name: Detect Private Keys
|
| 31 |
+
|
| 32 |
+
- repo: https://github.com/psf/black
|
| 33 |
+
rev: 23.12.1
|
| 34 |
+
hooks:
|
| 35 |
+
- id: black
|
| 36 |
+
name: Black - Python Formatter
|
| 37 |
+
language_version: python3
|
| 38 |
+
args: [--line-length=100]
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SAAP Deployment Guide
|
| 2 |
+
|
| 3 |
+
## 📋 Overview
|
| 4 |
+
|
| 5 |
+
This guide covers deploying SAAP (satware Autonomous Agent Platform) from development to production using Docker and GitHub Actions.
|
| 6 |
+
|
| 7 |
+
## 🚀 Deployment Strategies
|
| 8 |
+
|
| 9 |
+
### 1. Local Development
|
| 10 |
+
|
| 11 |
+
**Requirements:**
|
| 12 |
+
- Docker & Docker Compose
|
| 13 |
+
- Node.js 20+ (for frontend development)
|
| 14 |
+
- Python 3.10+ (for backend development)
|
| 15 |
+
|
| 16 |
+
**Setup:**
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
# Clone repository
|
| 20 |
+
git clone https://github.com/satwareAG/saap.git
|
| 21 |
+
cd saap
|
| 22 |
+
|
| 23 |
+
# Copy environment template
|
| 24 |
+
cp .env.example .env
|
| 25 |
+
|
| 26 |
+
# Edit .env with your API keys
|
| 27 |
+
nano .env
|
| 28 |
+
|
| 29 |
+
# Start development environment
|
| 30 |
+
docker-compose up -d
|
| 31 |
+
|
| 32 |
+
# Verify services
|
| 33 |
+
curl http://localhost:8000/health
|
| 34 |
+
curl http://localhost:5173
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
**Services:**
|
| 38 |
+
- Backend API: http://localhost:8000
|
| 39 |
+
- Frontend: http://localhost:5173
|
| 40 |
+
- API Docs: http://localhost:8000/docs
|
| 41 |
+
- PostgreSQL: localhost:5432
|
| 42 |
+
|
| 43 |
+
### 2. Production Deployment
|
| 44 |
+
|
| 45 |
+
**Production Configuration:**
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# Use production overlay
|
| 49 |
+
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
**Key Differences:**
|
| 53 |
+
- Optimized builds (no dev dependencies)
|
| 54 |
+
- Port 80 exposed (not 5173)
|
| 55 |
+
- Named volumes for data persistence
|
| 56 |
+
- Production CORS settings
|
| 57 |
+
- No hot reload
|
| 58 |
+
- Uvicorn workers: 4
|
| 59 |
+
|
| 60 |
+
## 🔐 Environment Variables
|
| 61 |
+
|
| 62 |
+
### Required Variables
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
# API Keys (MANDATORY)
|
| 66 |
+
COLOSSUS_API_KEY=your-colossus-key
|
| 67 |
+
OPENROUTER_API_KEY=your-openrouter-key
|
| 68 |
+
|
| 69 |
+
# Database
|
| 70 |
+
POSTGRES_DB=saap_db
|
| 71 |
+
POSTGRES_USER=saap_user
|
| 72 |
+
POSTGRES_PASSWORD=strong-password-here
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### Production Variables
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
# Security
|
| 79 |
+
ENVIRONMENT=production
|
| 80 |
+
DEBUG=false
|
| 81 |
+
LOG_LEVEL=WARNING
|
| 82 |
+
SECRET_KEY=generate-strong-secret
|
| 83 |
+
|
| 84 |
+
# CORS (whitelist domains)
|
| 85 |
+
CORS_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
|
| 86 |
+
|
| 87 |
+
# Performance
|
| 88 |
+
WORKERS=4
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## 🛠️ CI/CD Pipeline (GitHub Actions)
|
| 92 |
+
|
| 93 |
+
### Automated Workflow
|
| 94 |
+
|
| 95 |
+
**Triggers:**
|
| 96 |
+
- Push to `main` branch
|
| 97 |
+
- Push to `develop` branch
|
| 98 |
+
- Pull requests to `main`
|
| 99 |
+
|
| 100 |
+
**Stages:**
|
| 101 |
+
|
| 102 |
+
1. **Security Checks**
|
| 103 |
+
- Gitleaks secret scanning
|
| 104 |
+
- Dependency vulnerability scanning (npm audit)
|
| 105 |
+
|
| 106 |
+
2. **Linting & Type Checking**
|
| 107 |
+
- ESLint (frontend)
|
| 108 |
+
- Ruff (backend)
|
| 109 |
+
- TypeScript validation
|
| 110 |
+
|
| 111 |
+
3. **Testing**
|
| 112 |
+
- Unit tests
|
| 113 |
+
- Integration tests
|
| 114 |
+
- Coverage reporting
|
| 115 |
+
|
| 116 |
+
4. **Build**
|
| 117 |
+
- Multi-architecture Docker images (amd64, arm64)
|
| 118 |
+
- Optimized production builds
|
| 119 |
+
- Image tagging (commit SHA + latest)
|
| 120 |
+
|
| 121 |
+
5. **Push to Registry**
|
| 122 |
+
- GitHub Container Registry (ghcr.io)
|
| 123 |
+
- Automatic versioning
|
| 124 |
+
|
| 125 |
+
### Manual Deployment
|
| 126 |
+
|
| 127 |
+
**Deploy to production:**
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
# SSH into server
|
| 131 |
+
ssh [email protected]
|
| 132 |
+
|
| 133 |
+
# Pull latest images
|
| 134 |
+
docker pull ghcr.io/satwareag/saap/backend:latest
|
| 135 |
+
docker pull ghcr.io/satwareag/saap/frontend:latest
|
| 136 |
+
|
| 137 |
+
# Restart services
|
| 138 |
+
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## 📦 Container Registry
|
| 142 |
+
|
| 143 |
+
**Images:**
|
| 144 |
+
```
|
| 145 |
+
ghcr.io/satwareag/saap/backend:latest
|
| 146 |
+
ghcr.io/satwareag/saap/backend:<commit-sha>
|
| 147 |
+
ghcr.io/satwareag/saap/frontend:latest
|
| 148 |
+
ghcr.io/satwareag/saap/frontend:<commit-sha>
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
**Authentication:**
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
# GitHub Personal Access Token required
|
| 155 |
+
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## 🔍 Health Checks
|
| 159 |
+
|
| 160 |
+
### Backend Health Check
|
| 161 |
+
|
| 162 |
+
```bash
|
| 163 |
+
# Simple health check (Docker/Kubernetes)
|
| 164 |
+
curl http://localhost:8000/health
|
| 165 |
+
|
| 166 |
+
# Response
|
| 167 |
+
{"status":"healthy","timestamp":"2025-11-18T10:00:00"}
|
| 168 |
+
|
| 169 |
+
# Detailed health check
|
| 170 |
+
curl http://localhost:8000/api/v1/health
|
| 171 |
+
|
| 172 |
+
# Response
|
| 173 |
+
{
|
| 174 |
+
"status": "healthy",
|
| 175 |
+
"services": {
|
| 176 |
+
"agent_manager": "active",
|
| 177 |
+
"websocket": "active",
|
| 178 |
+
"colossus_api": "connected"
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
### Frontend Health Check
|
| 184 |
+
|
| 185 |
+
```bash
|
| 186 |
+
curl http://localhost/
|
| 187 |
+
# Returns Vue.js application
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
## 🗂️ Data Persistence
|
| 191 |
+
|
| 192 |
+
### Development
|
| 193 |
+
|
| 194 |
+
```yaml
|
| 195 |
+
volumes:
|
| 196 |
+
- ./backend/logs:/app/logs # Local logs
|
| 197 |
+
- ./data/postgres:/var/lib/postgresql/data # Local database
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### Production
|
| 201 |
+
|
| 202 |
+
```yaml
|
| 203 |
+
volumes:
|
| 204 |
+
postgres_data:
|
| 205 |
+
driver_opts:
|
| 206 |
+
device: /data/saap/postgres # Persistent storage
|
| 207 |
+
backend_logs:
|
| 208 |
+
driver_opts:
|
| 209 |
+
device: /data/saap/logs
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
**Backup Strategy:**
|
| 213 |
+
|
| 214 |
+
```bash
|
| 215 |
+
# Database backup
|
| 216 |
+
docker exec saap-postgres-1 pg_dump -U saap_user saap_db > backup.sql
|
| 217 |
+
|
| 218 |
+
# Restore
|
| 219 |
+
docker exec -i saap-postgres-1 psql -U saap_user saap_db < backup.sql
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
## 🔐 Security Best Practices
|
| 223 |
+
|
| 224 |
+
### 1. Secrets Management
|
| 225 |
+
|
| 226 |
+
**NEVER commit:**
|
| 227 |
+
- `.env` files
|
| 228 |
+
- API keys
|
| 229 |
+
- Database passwords
|
| 230 |
+
- SSL certificates
|
| 231 |
+
|
| 232 |
+
**Use:**
|
| 233 |
+
- GitHub Secrets for CI/CD
|
| 234 |
+
- Environment variables in production
|
| 235 |
+
- Secrets managers (HashiCorp Vault, AWS Secrets Manager)
|
| 236 |
+
|
| 237 |
+
### 2. Pre-deployment Checklist
|
| 238 |
+
|
| 239 |
+
```bash
|
| 240 |
+
# Security scan
|
| 241 |
+
gitleaks detect --source . --verbose
|
| 242 |
+
|
| 243 |
+
# Dependency audit
|
| 244 |
+
npm audit --audit-level=moderate
|
| 245 |
+
pip-audit
|
| 246 |
+
|
| 247 |
+
# Secrets in .env only
|
| 248 |
+
grep -r "OPENROUTER_API_KEY" . --exclude-dir=node_modules --exclude=.env
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### 3. HTTPS Configuration
|
| 252 |
+
|
| 253 |
+
**Nginx with Let's Encrypt:**
|
| 254 |
+
|
| 255 |
+
```nginx
|
| 256 |
+
server {
|
| 257 |
+
listen 443 ssl http2;
|
| 258 |
+
server_name yourdomain.com;
|
| 259 |
+
|
| 260 |
+
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
|
| 261 |
+
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
|
| 262 |
+
|
| 263 |
+
location / {
|
| 264 |
+
proxy_pass http://localhost:80;
|
| 265 |
+
proxy_set_header Host $host;
|
| 266 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
location /api {
|
| 270 |
+
proxy_pass http://localhost:8000;
|
| 271 |
+
proxy_http_version 1.1;
|
| 272 |
+
proxy_set_header Upgrade $http_upgrade;
|
| 273 |
+
proxy_set_header Connection "upgrade";
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
## 📊 Monitoring
|
| 279 |
+
|
| 280 |
+
### Application Logs
|
| 281 |
+
|
| 282 |
+
```bash
|
| 283 |
+
# Backend logs
|
| 284 |
+
docker logs saap-backend-1 -f
|
| 285 |
+
|
| 286 |
+
# Frontend logs
|
| 287 |
+
docker logs saap-frontend-1 -f
|
| 288 |
+
|
| 289 |
+
# Database logs
|
| 290 |
+
docker logs saap-postgres-1 -f
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
### Metrics
|
| 294 |
+
|
| 295 |
+
**Health check monitoring:**
|
| 296 |
+
|
| 297 |
+
```bash
|
| 298 |
+
# Cron job for health monitoring
|
| 299 |
+
*/5 * * * * curl -f http://localhost:8000/health || systemctl restart saap
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
## 🚨 Troubleshooting
|
| 303 |
+
|
| 304 |
+
### Common Issues
|
| 305 |
+
|
| 306 |
+
**1. Container won't start:**
|
| 307 |
+
|
| 308 |
+
```bash
|
| 309 |
+
# Check logs
|
| 310 |
+
docker-compose logs backend
|
| 311 |
+
docker-compose logs frontend
|
| 312 |
+
|
| 313 |
+
# Rebuild without cache
|
| 314 |
+
docker-compose build --no-cache
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
**2. Database connection failed:**
|
| 318 |
+
|
| 319 |
+
```bash
|
| 320 |
+
# Verify PostgreSQL running
|
| 321 |
+
docker-compose ps postgres
|
| 322 |
+
|
| 323 |
+
# Check DATABASE_URL in .env
|
| 324 |
+
echo $DATABASE_URL
|
| 325 |
+
|
| 326 |
+
# Test connection
|
| 327 |
+
docker exec -it saap-postgres-1 psql -U saap_user -d saap_db
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
**3. API keys not working:**
|
| 331 |
+
|
| 332 |
+
```bash
|
| 333 |
+
# Verify environment variables loaded
|
| 334 |
+
docker exec saap-backend-1 env | grep API_KEY
|
| 335 |
+
|
| 336 |
+
# Restart backend
|
| 337 |
+
docker-compose restart backend
|
| 338 |
+
```
|
| 339 |
+
|
| 340 |
+
**4. CORS errors:**
|
| 341 |
+
|
| 342 |
+
```bash
|
| 343 |
+
# Update CORS_ORIGINS in .env
|
| 344 |
+
CORS_ORIGINS=http://localhost:5173,https://yourdomain.com
|
| 345 |
+
|
| 346 |
+
# Restart backend
|
| 347 |
+
docker-compose restart backend
|
| 348 |
+
```
|
| 349 |
+
|
| 350 |
+
## 🔄 Update Procedure
|
| 351 |
+
|
| 352 |
+
### Development
|
| 353 |
+
|
| 354 |
+
```bash
|
| 355 |
+
git pull origin main
|
| 356 |
+
docker-compose down
|
| 357 |
+
docker-compose build
|
| 358 |
+
docker-compose up -d
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
### Production
|
| 362 |
+
|
| 363 |
+
```bash
|
| 364 |
+
# 1. Backup database
|
| 365 |
+
docker exec saap-postgres-1 pg_dump -U saap_user saap_db > backup.sql
|
| 366 |
+
|
| 367 |
+
# 2. Pull new images
|
| 368 |
+
docker pull ghcr.io/satwareag/saap/backend:latest
|
| 369 |
+
docker pull ghcr.io/satwareag/saap/frontend:latest
|
| 370 |
+
|
| 371 |
+
# 3. Restart with zero downtime
|
| 372 |
+
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps --build backend
|
| 373 |
+
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps --build frontend
|
| 374 |
+
|
| 375 |
+
# 4. Verify health
|
| 376 |
+
curl http://localhost:8000/health
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
## 📚 Additional Resources
|
| 380 |
+
|
| 381 |
+
- [Docker Documentation](https://docs.docker.com/)
|
| 382 |
+
- [GitHub Actions](https://docs.github.com/en/actions)
|
| 383 |
+
- [FastAPI Deployment](https://fastapi.tiangolo.com/deployment/)
|
| 384 |
+
- [Nginx Configuration](https://nginx.org/en/docs/)
|
| 385 |
+
|
| 386 |
+
## 🆘 Support
|
| 387 |
+
|
| 388 |
+
- GitHub Issues: https://github.com/satwareAG/saap/issues
|
| 389 |
+
- Email: [email protected]
|
Dockerfile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SAAP - Hugging Face Deployment
|
| 2 |
+
# Multi-stage build: Frontend → Backend
|
| 3 |
+
|
| 4 |
+
FROM node:20-slim AS frontend-builder
|
| 5 |
+
|
| 6 |
+
WORKDIR /app/frontend
|
| 7 |
+
|
| 8 |
+
# Install frontend dependencies
|
| 9 |
+
COPY frontend/package*.json ./
|
| 10 |
+
RUN npm ci
|
| 11 |
+
|
| 12 |
+
# Build frontend
|
| 13 |
+
COPY frontend/ ./
|
| 14 |
+
RUN npm run build
|
| 15 |
+
|
| 16 |
+
# ============================================
|
| 17 |
+
# Python Backend Stage
|
| 18 |
+
# ============================================
|
| 19 |
+
FROM python:3.11-slim
|
| 20 |
+
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Install minimal system dependencies
|
| 24 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 25 |
+
gcc \
|
| 26 |
+
libpq-dev \
|
| 27 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 28 |
+
|
| 29 |
+
# Install Python dependencies
|
| 30 |
+
COPY backend/requirements.txt ./
|
| 31 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 32 |
+
|
| 33 |
+
# Copy backend code
|
| 34 |
+
COPY backend/ ./backend/
|
| 35 |
+
|
| 36 |
+
# Copy built frontend from builder stage
|
| 37 |
+
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
| 38 |
+
|
| 39 |
+
# Environment variables
|
| 40 |
+
ENV PYTHONUNBUFFERED=1
|
| 41 |
+
ENV PORT=7860
|
| 42 |
+
|
| 43 |
+
# Expose Hugging Face Spaces port
|
| 44 |
+
EXPOSE 7860
|
| 45 |
+
|
| 46 |
+
# Start FastAPI (serves API + frontend static files)
|
| 47 |
+
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 satware AG/Hanan Wandji
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
PROJEKT_VOLLSTAENDIGE_ANALYSE_2025-12-04.md
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📊 SAAP Projekt - Vollständige Analyse (Stand: 2025-12-04)
|
| 2 |
+
|
| 3 |
+
## 🎯 Antworten auf deine drei Hauptfragen
|
| 4 |
+
|
| 5 |
+
### 1️⃣ Was ist der aktuellen Stand in diesem Projekt?
|
| 6 |
+
|
| 7 |
+
#### **Projektstatus: PRODUKTIONSBEREIT MIT HYBRID-ARCHITEKTUR** 🚀
|
| 8 |
+
|
| 9 |
+
**Hauptmerkmale:**
|
| 10 |
+
- ✅ **Full-Stack Multi-Agent Platform** funktionsfähig
|
| 11 |
+
- ✅ **Hybrid LLM Provider System** (OpenRouter + Colossus mit automatischem Failover)
|
| 12 |
+
- ✅ **7 Alesi Agents** implementiert (Jane, John, Lara, Theo, Justus, Leon, Luna)
|
| 13 |
+
- ✅ **Vue.js Frontend** mit Real-time Chat Interface
|
| 14 |
+
- ✅ **FastAPI Backend** mit WebSocket Support
|
| 15 |
+
- ✅ **PostgreSQL Database** für Agent-Persistenz
|
| 16 |
+
- ✅ **Docker Compose** Setup für lokale Entwicklung
|
| 17 |
+
|
| 18 |
+
#### **Technologie-Stack:**
|
| 19 |
+
|
| 20 |
+
```
|
| 21 |
+
Frontend:
|
| 22 |
+
├── Vue 3 (Composition API)
|
| 23 |
+
├── Tailwind CSS
|
| 24 |
+
├── Vite Build Tool
|
| 25 |
+
└── WebSocket Client
|
| 26 |
+
|
| 27 |
+
Backend:
|
| 28 |
+
├── Python FastAPI
|
| 29 |
+
├── SQLAlchemy (Async ORM)
|
| 30 |
+
├── PostgreSQL
|
| 31 |
+
├── OpenRouter API Integration
|
| 32 |
+
├── Colossus Integration (optional)
|
| 33 |
+
└── WebSocket Server
|
| 34 |
+
|
| 35 |
+
Infrastructure:
|
| 36 |
+
├── Docker & Docker Compose
|
| 37 |
+
├── Nginx (Frontend Server)
|
| 38 |
+
└── Python 3.11+ Runtime
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
#### **Aktuelle Features:**
|
| 42 |
+
|
| 43 |
+
**1. Multi-Agent System**
|
| 44 |
+
- 7 spezialisierte Alesi Agents mit unterschiedlichen Rollen
|
| 45 |
+
- Agent-Konfiguration über Vue Frontend
|
| 46 |
+
- Persistent storage in PostgreSQL
|
| 47 |
+
- Real-time status updates
|
| 48 |
+
|
| 49 |
+
**2. Hybrid LLM Provider**
|
| 50 |
+
- **Primary:** OpenRouter (schnell 2-5s, kostenoptimiert)
|
| 51 |
+
- **Fallback:** Colossus (optional, 15-30s)
|
| 52 |
+
- Automatisches Failover bei Provider-Ausfall
|
| 53 |
+
- Cost Tracking und Performance Metrics
|
| 54 |
+
|
| 55 |
+
**3. Chat Interface**
|
| 56 |
+
- Multi-Agent Chat Modal
|
| 57 |
+
- WebSocket Real-time Communication
|
| 58 |
+
- Response Time Tracking
|
| 59 |
+
- Cost per Message Anzeige
|
| 60 |
+
|
| 61 |
+
**4. Agent Management**
|
| 62 |
+
- CRUD Operations für Agents
|
| 63 |
+
- LLM Config Editor (Model, Temperature, Max Tokens)
|
| 64 |
+
- Status Management (Active/Inactive)
|
| 65 |
+
- Performance Metrics Dashboard
|
| 66 |
+
|
| 67 |
+
#### **Kritische Fixes (Heute implementiert):**
|
| 68 |
+
|
| 69 |
+
```diff
|
| 70 |
+
+ FIX 1: _send_colossus_message() Method implementiert
|
| 71 |
+
+ FIX 2: LLMModelConfig.get() AttributeError behoben
|
| 72 |
+
+ FIX 3: Frontend/Backend Config Mismatch ResKonflikt gelöst
|
| 73 |
+
+ FIX 4: Colossus Failover agent.type.value Error behoben
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
#### **Projektstruktur:**
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
saap/
|
| 80 |
+
├── backend/
|
| 81 |
+
│ ├── main.py # FastAPI App Entry Point
|
| 82 |
+
│ ├── services/
|
| 83 |
+
│ │ ├── agent_manager.py # Base Agent Service
|
| 84 |
+
│ │ └── agent_manager_hybrid.py # Hybrid Multi-Provider Service
|
| 85 |
+
│ ├── api/
|
| 86 |
+
│ │ ├── openrouter_client.py # OpenRouter Integration
|
| 87 |
+
│ │ └── colossus_client.py # Colossus Integration
|
| 88 |
+
│ ├── models/
|
| 89 |
+
│ │ ├── agent.py # Pydantic Models
|
| 90 |
+
│ │ └── agent_schema.json # JSON Schema
|
| 91 |
+
│ ├── database/
|
| 92 |
+
│ │ ├── connection.py # DB Manager
|
| 93 |
+
│ │ └── models.py # SQLAlchemy Models
|
| 94 |
+
│ └── requirements.txt
|
| 95 |
+
│
|
| 96 |
+
├── frontend/
|
| 97 |
+
│ ├── src/
|
| 98 |
+
│ │ ├── App.vue # Main App Component
|
| 99 |
+
│ │ ├── components/
|
| 100 |
+
│ │ │ └── modals/
|
| 101 |
+
│ │ │ └── MultiAgentChatModal.vue # Chat Interface
|
| 102 |
+
│ │ ├── services/
|
| 103 |
+
│ │ │ └── saapApi.js # API Client
|
| 104 |
+
│ │ └── stores/
|
| 105 |
+
│ │ └── agentStore.js # State Management
|
| 106 |
+
│ ├── package.json
|
| 107 |
+
│ └── vite.config.js
|
| 108 |
+
│
|
| 109 |
+
├── docker-compose.yml # Development Setup
|
| 110 |
+
├── docker-compose.prod.yml # Production Setup
|
| 111 |
+
└── README.md
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
### 2️⃣ Wie kann ich das Projekt mit Docker Compose starten?
|
| 117 |
+
|
| 118 |
+
#### **Schnellstart (3 Befehle):**
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
# 1. Navigate zum Projekt
|
| 122 |
+
cd /home/shadowadmin/WebstormProjects/saap
|
| 123 |
+
|
| 124 |
+
# 2. Starte alle Services
|
| 125 |
+
docker-compose up -d
|
| 126 |
+
|
| 127 |
+
# 3. Überprüfe Status
|
| 128 |
+
docker-compose ps
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
#### **Detaillierte Startanleitung:**
|
| 132 |
+
|
| 133 |
+
**Schritt 1: Environment Variables prüfen**
|
| 134 |
+
```bash
|
| 135 |
+
# Backend .env erstellen (falls nicht vorhanden)
|
| 136 |
+
cp backend/.env.example backend/.env
|
| 137 |
+
|
| 138 |
+
# Wichtige Variablen in backend/.env:
|
| 139 |
+
# - OPENROUTER_API_KEY=sk-or-v1-... (bereits konfiguriert)
|
| 140 |
+
# - DATABASE_URL=postgresql+asyncpg://saap:saap@db:5432/saap
|
| 141 |
+
# - COLOSSUS_API_URL=http://89.58.13.188:7860 (optional)
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
**Schritt 2: Services starten**
|
| 145 |
+
```bash
|
| 146 |
+
# Alle Services im Hintergrund starten
|
| 147 |
+
docker-compose up -d
|
| 148 |
+
|
| 149 |
+
# ODER: Im Vordergrund mit Logs
|
| 150 |
+
docker-compose up
|
| 151 |
+
|
| 152 |
+
# Nur spezifische Services starten
|
| 153 |
+
docker-compose up -d db backend frontend
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
**Schritt 3: Logs anzeigen**
|
| 157 |
+
```bash
|
| 158 |
+
# Alle Logs
|
| 159 |
+
docker-compose logs -f
|
| 160 |
+
|
| 161 |
+
# Nur Backend Logs
|
| 162 |
+
docker-compose logs -f backend
|
| 163 |
+
|
| 164 |
+
# Letzte 100 Zeilen
|
| 165 |
+
docker-compose logs --tail=100 backend
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
**Schritt 4: Zugriff auf die Anwendung**
|
| 169 |
+
```
|
| 170 |
+
Frontend: http://localhost:8080
|
| 171 |
+
Backend: http://localhost:8000
|
| 172 |
+
API Docs: http://localhost:8000/docs
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
#### **Nützliche Docker Compose Befehle:**
|
| 176 |
+
|
| 177 |
+
```bash
|
| 178 |
+
# Status aller Container
|
| 179 |
+
docker-compose ps
|
| 180 |
+
|
| 181 |
+
# Services neu starten
|
| 182 |
+
docker-compose restart
|
| 183 |
+
|
| 184 |
+
# Nur Backend neu starten
|
| 185 |
+
docker-compose restart backend
|
| 186 |
+
|
| 187 |
+
# Services stoppen (behält Daten)
|
| 188 |
+
docker-compose stop
|
| 189 |
+
|
| 190 |
+
# Services stoppen und entfernen (löscht Container, NICHT Volumes)
|
| 191 |
+
docker-compose down
|
| 192 |
+
|
| 193 |
+
# Alles löschen inkl. Volumes (VORSICHT: Löscht DB Daten!)
|
| 194 |
+
docker-compose down -v
|
| 195 |
+
|
| 196 |
+
# Container Shell öffnen
|
| 197 |
+
docker-compose exec backend bash
|
| 198 |
+
docker-compose exec frontend sh
|
| 199 |
+
|
| 200 |
+
# Logs in Echtzeit verfolgen
|
| 201 |
+
docker-compose logs -f backend frontend
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
#### **Troubleshooting:**
|
| 205 |
+
|
| 206 |
+
**Problem: Port bereits belegt**
|
| 207 |
+
```bash
|
| 208 |
+
# Prüfe welcher Prozess Port verwendet
|
| 209 |
+
sudo lsof -i :8000 # Backend
|
| 210 |
+
sudo lsof -i :8080 # Frontend
|
| 211 |
+
sudo lsof -i :5432 # PostgreSQL
|
| 212 |
+
|
| 213 |
+
# Lösung 1: Prozess beenden
|
| 214 |
+
sudo kill -9 <PID>
|
| 215 |
+
|
| 216 |
+
# Lösung 2: Ports in docker-compose.yml ändern
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
**Problem: Database Connection Error**
|
| 220 |
+
```bash
|
| 221 |
+
# Warte bis DB bereit ist
|
| 222 |
+
docker-compose logs db | grep "ready to accept connections"
|
| 223 |
+
|
| 224 |
+
# Falls DB nicht startet, Volume neu erstellen
|
| 225 |
+
docker-compose down -v
|
| 226 |
+
docker-compose up -d db
|
| 227 |
+
# Warte 10 Sekunden
|
| 228 |
+
docker-compose up -d backend frontend
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
**Problem: Frontend Build Error**
|
| 232 |
+
```bash
|
| 233 |
+
# Frontend Container neu bauen
|
| 234 |
+
docker-compose build frontend
|
| 235 |
+
docker-compose up -d frontend
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
#### **Development Workflow:**
|
| 239 |
+
|
| 240 |
+
```bash
|
| 241 |
+
# 1. Code ändern (z.B. backend/main.py)
|
| 242 |
+
# 2. Backend Service neu starten
|
| 243 |
+
docker-compose restart backend
|
| 244 |
+
|
| 245 |
+
# ODER: Hot-reload nutzen (wenn konfiguriert)
|
| 246 |
+
# In diesem Fall automatische Reload
|
| 247 |
+
|
| 248 |
+
# 3. Logs prüfen
|
| 249 |
+
docker-compose logs -f backend
|
| 250 |
+
|
| 251 |
+
# 4. Testen über Browser oder API Docs
|
| 252 |
+
# http://localhost:8000/docs
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
---
|
| 256 |
+
|
| 257 |
+
### 3️⃣ Wie deploye ich auf Hugging Face? (Günstigste Alternative)
|
| 258 |
+
|
| 259 |
+
#### **🎯 Empfehlung: Hugging Face Spaces (KOSTENLOS!)**
|
| 260 |
+
|
| 261 |
+
Hugging Face Spaces bietet **kostenloses Hosting** für die SAAP Plattform mit mehreren Deployment-Optionen:
|
| 262 |
+
|
| 263 |
+
#### **Option 1: Docker Space (EMPFOHLEN - Am einfachsten)**
|
| 264 |
+
|
| 265 |
+
**Vorteile:**
|
| 266 |
+
- ✅ Komplett kostenlos (2 vCPU, 16GB RAM, 50GB Storage)
|
| 267 |
+
- ✅ Nutzt vorhandenes Docker Setup
|
| 268 |
+
- ✅ Einfachste Migration vom lokalen Setup
|
| 269 |
+
- ✅ Persistenter Storage möglich
|
| 270 |
+
|
| 271 |
+
**Schritte:**
|
| 272 |
+
|
| 273 |
+
```bash
|
| 274 |
+
# 1. Hugging Face Repository erstellen
|
| 275 |
+
# Gehe zu: https://huggingface.co/new-space
|
| 276 |
+
# - Name: saap-platform
|
| 277 |
+
# - SDK: Docker
|
| 278 |
+
# - Hardware: CPU basic (kostenlos)
|
| 279 |
+
|
| 280 |
+
# 2. Repository klonen
|
| 281 |
+
git clone https://huggingface.co/spaces/Hwandji/saap-platform
|
| 282 |
+
cd saap-platform
|
| 283 |
+
|
| 284 |
+
# 3. SAAP Dateien kopieren
|
| 285 |
+
cp -r /home/shadowadmin/WebstormProjects/saap/* .
|
| 286 |
+
|
| 287 |
+
# 4. Hugging Face-spezifisches Dockerfile erstellen
|
| 288 |
+
cat > Dockerfile << 'EOF'
|
| 289 |
+
# Hugging Face Spaces Dockerfile für SAAP
|
| 290 |
+
FROM python:3.11-slim
|
| 291 |
+
|
| 292 |
+
# Install system dependencies
|
| 293 |
+
RUN apt-get update && apt-get install -y \
|
| 294 |
+
nodejs \
|
| 295 |
+
npm \
|
| 296 |
+
postgresql-client \
|
| 297 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 298 |
+
|
| 299 |
+
# Set working directory
|
| 300 |
+
WORKDIR /app
|
| 301 |
+
|
| 302 |
+
# Copy backend
|
| 303 |
+
COPY backend/ /app/backend/
|
| 304 |
+
WORKDIR /app/backend
|
| 305 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 306 |
+
|
| 307 |
+
# Copy and build frontend
|
| 308 |
+
COPY frontend/ /app/frontend/
|
| 309 |
+
WORKDIR /app/frontend
|
| 310 |
+
RUN npm install
|
| 311 |
+
RUN npm run build
|
| 312 |
+
|
| 313 |
+
# Environment variables
|
| 314 |
+
ENV DATABASE_URL=postgresql+asyncpg://saap:saap@localhost:5432/saap
|
| 315 |
+
ENV OPENROUTER_API_KEY=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
|
| 316 |
+
|
| 317 |
+
# Expose ports
|
| 318 |
+
EXPOSE 7860
|
| 319 |
+
|
| 320 |
+
# Start script
|
| 321 |
+
COPY start.sh /app/
|
| 322 |
+
RUN chmod +x /app/start.sh
|
| 323 |
+
|
| 324 |
+
WORKDIR /app
|
| 325 |
+
CMD ["/app/start.sh"]
|
| 326 |
+
EOF
|
| 327 |
+
|
| 328 |
+
# 5. Start Script erstellen
|
| 329 |
+
cat > start.sh << 'EOF'
|
| 330 |
+
#!/bin/bash
|
| 331 |
+
set -e
|
| 332 |
+
|
| 333 |
+
# Start PostgreSQL (Hugging Face hat eigene DB)
|
| 334 |
+
# OR use SQLite for free tier
|
| 335 |
+
export DATABASE_URL=sqlite+aiosqlite:///./saap.db
|
| 336 |
+
|
| 337 |
+
# Start Backend
|
| 338 |
+
cd /app/backend
|
| 339 |
+
uvicorn main:app --host 0.0.0.0 --port 7860 &
|
| 340 |
+
|
| 341 |
+
# Serve Frontend (optional, kann auch nur Backend sein)
|
| 342 |
+
cd /app/frontend/dist
|
| 343 |
+
python3 -m http.server 8080 &
|
| 344 |
+
|
| 345 |
+
wait
|
| 346 |
+
EOF
|
| 347 |
+
|
| 348 |
+
# 6. .gitignore für Hugging Face
|
| 349 |
+
cat > .gitignore << 'EOF'
|
| 350 |
+
__pycache__/
|
| 351 |
+
*.pyc
|
| 352 |
+
.env
|
| 353 |
+
node_modules/
|
| 354 |
+
.DS_Store
|
| 355 |
+
*.db
|
| 356 |
+
*.log
|
| 357 |
+
EOF
|
| 358 |
+
|
| 359 |
+
# 7. README für Hugging Face Space
|
| 360 |
+
cat > README.md << 'EOF'
|
| 361 |
+
---
|
| 362 |
+
title: SAAP - Autonomous Agent Platform
|
| 363 |
+
emoji: 🤖
|
| 364 |
+
colorFrom: blue
|
| 365 |
+
colorTo: green
|
| 366 |
+
sdk: docker
|
| 367 |
+
pinned: false
|
| 368 |
+
---
|
| 369 |
+
|
| 370 |
+
# SAAP - satware Autonomous Agent Platform
|
| 371 |
+
|
| 372 |
+
Multi-Agent System with 7 specialized Alesi agents.
|
| 373 |
+
|
| 374 |
+
## Features
|
| 375 |
+
- 🤖 7 Specialized AI Agents (Jane, John, Lara, Theo, Justus, Leon, Luna)
|
| 376 |
+
- 🔄 Hybrid LLM Provider (OpenRouter + Colossus Failover)
|
| 377 |
+
- 💬 Real-time Chat Interface
|
| 378 |
+
- 📊 Cost Tracking & Performance Metrics
|
| 379 |
+
|
| 380 |
+
## Usage
|
| 381 |
+
Open the app and start chatting with the agents!
|
| 382 |
+
EOF
|
| 383 |
+
|
| 384 |
+
# 8. Commit und Push
|
| 385 |
+
git add .
|
| 386 |
+
git commit -m "Initial SAAP deployment"
|
| 387 |
+
git push
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
**Nach dem Push:**
|
| 391 |
+
- Hugging Face baut automatisch das Docker Image
|
| 392 |
+
- Space startet nach ~5-10 Minuten
|
| 393 |
+
- Verfügbar unter: `https://huggingface.co/spaces/Hwandji/saap-platform`
|
| 394 |
+
|
| 395 |
+
#### **Option 2: Gradio Space (Einfachste UI)**
|
| 396 |
+
|
| 397 |
+
**Vorteile:**
|
| 398 |
+
- ✅ Noch einfacher als Docker
|
| 399 |
+
- ✅ Sofortiges UI ohne Frontend-Arbeit
|
| 400 |
+
- ✅ Kostenlos
|
| 401 |
+
|
| 402 |
+
**Nachteil:**
|
| 403 |
+
- ⚠️ Benötigt Umschreiben der UI zu Gradio Components
|
| 404 |
+
|
| 405 |
+
**Implementierung:**
|
| 406 |
+
```python
|
| 407 |
+
# gradio_app.py
|
| 408 |
+
import gradio as gr
|
| 409 |
+
import sys
|
| 410 |
+
sys.path.append('./backend')
|
| 411 |
+
|
| 412 |
+
from services.agent_manager_hybrid import HybridAgentManagerService
|
| 413 |
+
|
| 414 |
+
# Initialize Manager
|
| 415 |
+
manager = HybridAgentManagerService()
|
| 416 |
+
|
| 417 |
+
async def chat_with_agent(agent_id, message):
|
| 418 |
+
"""Send message to agent"""
|
| 419 |
+
response = await manager.send_message_to_agent(agent_id, message)
|
| 420 |
+
return response.get('content', 'Error: ' + response.get('error', 'Unknown'))
|
| 421 |
+
|
| 422 |
+
# Gradio Interface
|
| 423 |
+
with gr.Blocks() as demo:
|
| 424 |
+
gr.Markdown("# SAAP - Autonomous Agent Platform")
|
| 425 |
+
|
| 426 |
+
with gr.Row():
|
| 427 |
+
agent_dropdown = gr.Dropdown(
|
| 428 |
+
choices=['jane_alesi', 'john_alesi', 'lara_alesi', 'theo_alesi',
|
| 429 |
+
'justus_alesi', 'leon_alesi', 'luna_alesi'],
|
| 430 |
+
label="Select Agent"
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
chatbot = gr.Chatbot()
|
| 434 |
+
msg = gr.Textbox(label="Your Message")
|
| 435 |
+
send = gr.Button("Send")
|
| 436 |
+
|
| 437 |
+
def respond(agent_id, message, history):
|
| 438 |
+
response = chat_with_agent(agent_id, message)
|
| 439 |
+
history.append((message, response))
|
| 440 |
+
return history, ""
|
| 441 |
+
|
| 442 |
+
send.click(respond, [agent_dropdown, msg, chatbot], [chatbot, msg])
|
| 443 |
+
|
| 444 |
+
if __name__ == "__main__":
|
| 445 |
+
demo.launch()
|
| 446 |
+
```
|
| 447 |
+
|
| 448 |
+
#### **Option 3: Streamlit Space (Balance zwischen UI und Einfachheit)**
|
| 449 |
+
|
| 450 |
+
Ähnlich wie Gradio, aber mit mehr Kontrolle über Layout.
|
| 451 |
+
|
| 452 |
+
#### **💰 Kostenvergleich:**
|
| 453 |
+
|
| 454 |
+
| Plattform | Free Tier | Kosten bei Upgrade | Bemerkung |
|
| 455 |
+
|-----------|-----------|-------------------|-----------|
|
| 456 |
+
| **Hugging Face Spaces** | ✅ CPU basic (2vCPU, 16GB RAM) | $0/Monat | **EMPFOHLEN** |
|
| 457 |
+
| Hugging Face Spaces Upgraded | T4 GPU | ~$0.60/Stunde | Nur wenn GPU benötigt |
|
| 458 |
+
| Render.com | 750h/Monat free | $7/Monat | Gute Alternative |
|
| 459 |
+
| Railway.app | $5 credit/Monat | $0.000463/GB-sec | Pay-as-you-go |
|
| 460 |
+
| Fly.io | 3 VMs kostenlos | Variable | Komplexere Config |
|
| 461 |
+
| **Vercel** | ❌ Nur Frontend | $0 Frontend only | Backend separat nötig |
|
| 462 |
+
|
| 463 |
+
#### **🏆 Finale Empfehlung für SAAP:**
|
| 464 |
+
|
| 465 |
+
```
|
| 466 |
+
1. BESTE Option: Hugging Face Docker Space
|
| 467 |
+
- Komplett kostenlos
|
| 468 |
+
- Nutzt vorhandenes Docker Setup
|
| 469 |
+
- Einfache Migration
|
| 470 |
+
- 2 vCPU, 16GB RAM ausreichend für SAAP
|
| 471 |
+
|
| 472 |
+
2. Wenn Gradio OK: Hugging Face Gradio Space
|
| 473 |
+
- Noch einfacher
|
| 474 |
+
- Schnellere Deployments
|
| 475 |
+
- Muss UI umschreiben
|
| 476 |
+
|
| 477 |
+
3. Backup: Render.com
|
| 478 |
+
- Wenn HF Probleme macht
|
| 479 |
+
- $7/Monat ist überschaubar
|
| 480 |
+
- Docker-Support
|
| 481 |
+
```
|
| 482 |
+
|
| 483 |
+
#### **Deployment Checkliste:**
|
| 484 |
+
|
| 485 |
+
```bash
|
| 486 |
+
# 1. Code vorbereiten
|
| 487 |
+
✅ Secrets aus Code entfernen
|
| 488 |
+
✅ Environment Variables dokumentieren
|
| 489 |
+
✅ Docker Build lokal testen
|
| 490 |
+
✅ README mit Anleitung erstellen
|
| 491 |
+
|
| 492 |
+
# 2. Hugging Face vorbereiten
|
| 493 |
+
✅ Account erstellen (kostenlos)
|
| 494 |
+
✅ New Space erstellen
|
| 495 |
+
✅ Git Repository initial push
|
| 496 |
+
|
| 497 |
+
# 3. Deployment
|
| 498 |
+
✅ Dockerfile für HF anpassen
|
| 499 |
+
✅ Start Script erstellen
|
| 500 |
+
✅ Git push → automatisches Build
|
| 501 |
+
✅ Space Logs überwachen
|
| 502 |
+
|
| 503 |
+
# 4. Testing
|
| 504 |
+
✅ Space URL aufrufen
|
| 505 |
+
✅ Agents testen
|
| 506 |
+
✅ Performance prüfen
|
| 507 |
+
✅ Kosten überwachen (sollte $0 sein)
|
| 508 |
+
```
|
| 509 |
+
|
| 510 |
+
#### **Wichtige Hinweise für Hugging Face:**
|
| 511 |
+
|
| 512 |
+
**1. Port Mapping:**
|
| 513 |
+
```python
|
| 514 |
+
# Hugging Face erwartet Port 7860
|
| 515 |
+
# In Dockerfile:
|
| 516 |
+
EXPOSE 7860
|
| 517 |
+
|
| 518 |
+
# Im Startscript:
|
| 519 |
+
uvicorn main:app --host 0.0.0.0 --port 7860
|
| 520 |
+
```
|
| 521 |
+
|
| 522 |
+
**2. Secrets Management:**
|
| 523 |
+
```bash
|
| 524 |
+
# Über Hugging Face UI setzen:
|
| 525 |
+
# Space Settings → Variables → Add Secret
|
| 526 |
+
# Name: OPENROUTER_API_KEY
|
| 527 |
+
# Value: sk-or-v1-...
|
| 528 |
+
|
| 529 |
+
# Im Code dann:
|
| 530 |
+
import os
|
| 531 |
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
| 532 |
+
```
|
| 533 |
+
|
| 534 |
+
**3. Persistent Storage:**
|
| 535 |
+
```python
|
| 536 |
+
# HF Spaces haben persistent storage in:
|
| 537 |
+
# /data (bleibt nach Rebuild erhalten)
|
| 538 |
+
|
| 539 |
+
# Für SQLite Database:
|
| 540 |
+
DATABASE_PATH = "/data/saap.db"
|
| 541 |
+
|
| 542 |
+
# Für Logs:
|
| 543 |
+
LOG_PATH = "/data/logs/"
|
| 544 |
+
```
|
| 545 |
+
|
| 546 |
+
---
|
| 547 |
+
|
| 548 |
+
## 📋 Zusammenfassung
|
| 549 |
+
|
| 550 |
+
### **Projekt Stand:**
|
| 551 |
+
- ✅ Fully funktional mit Hybrid LLM Provider
|
| 552 |
+
- ✅ 7 Agents implementiert und getestet
|
| 553 |
+
- ✅ Docker Compose Setup bereit
|
| 554 |
+
- ✅ Alle kritischen Bugs behoben (heute)
|
| 555 |
+
|
| 556 |
+
### **Lokaler Start:**
|
| 557 |
+
```bash
|
| 558 |
+
cd /home/shadowadmin/WebstormProjects/saap
|
| 559 |
+
docker-compose up -d
|
| 560 |
+
# → http://localhost:8080
|
| 561 |
+
```
|
| 562 |
+
|
| 563 |
+
### **Hugging Face Deployment:**
|
| 564 |
+
```bash
|
| 565 |
+
# KOSTENLOS mit Docker Space
|
| 566 |
+
git clone https://huggingface.co/spaces/<username>/saap
|
| 567 |
+
# Dateien kopieren, Dockerfile anpassen, pushen
|
| 568 |
+
# → Automatisches Build und Hosting
|
| 569 |
+
```
|
| 570 |
+
|
| 571 |
+
### **Nächste Schritte (Optional):**
|
| 572 |
+
1. Hugging Face Space erstellen
|
| 573 |
+
2. Deployment testen
|
| 574 |
+
3. Performance Monitoring aufsetzen
|
| 575 |
+
4. E2E Tests schreiben
|
| 576 |
+
5. Thesis Dokumentation erweitern
|
| 577 |
+
|
| 578 |
+
**Fragen? Ich helfe gerne weiter!** 🚀
|
QUICKSTART.md
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SAAP Quick Start Guide
|
| 2 |
+
|
| 3 |
+
**satware Autonomous Agent Platform (SAAP)** - Local multi-agent orchestration platform
|
| 4 |
+
|
| 5 |
+
## ⚠️ CRITICAL SECURITY NOTICE
|
| 6 |
+
|
| 7 |
+
**Before running the project, you MUST remediate hardcoded API keys!**
|
| 8 |
+
|
| 9 |
+
The imported codebase contains **40+ hardcoded API keys** that must be replaced with environment variables before:
|
| 10 |
+
- Running the application
|
| 11 |
+
- Pushing to GitHub
|
| 12 |
+
- Deploying to any environment
|
| 13 |
+
|
| 14 |
+
**📋 Action Required:** See `SECURITY_REMEDIATION_REQUIRED.md` for detailed remediation steps.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## Prerequisites
|
| 19 |
+
|
| 20 |
+
### Required Software
|
| 21 |
+
|
| 22 |
+
1. **Python 3.10 or higher**
|
| 23 |
+
```bash
|
| 24 |
+
python3 --version # Should be 3.10+
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
2. **Node.js 18 or higher**
|
| 28 |
+
```bash
|
| 29 |
+
node --version # Should be v18+
|
| 30 |
+
npm --version
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
3. **PostgreSQL Database**
|
| 34 |
+
```bash
|
| 35 |
+
psql --version
|
| 36 |
+
```
|
| 37 |
+
- Installation: https://www.postgresql.org/download/
|
| 38 |
+
|
| 39 |
+
4. **Git** (for version control)
|
| 40 |
+
```bash
|
| 41 |
+
git --version
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### Optional (Recommended)
|
| 45 |
+
|
| 46 |
+
- **Redis** (for caching and message queuing)
|
| 47 |
+
- **Docker** (for containerized deployment)
|
| 48 |
+
- **Docker Compose** (for multi-service orchestration)
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## Installation Steps
|
| 53 |
+
|
| 54 |
+
### 1. Clone the Repository
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
git clone https://github.com/satwareAG/saap.git
|
| 58 |
+
cd saap
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### 2. Backend Setup
|
| 62 |
+
|
| 63 |
+
#### 2.1 Create Python Virtual Environment
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# Create virtual environment
|
| 67 |
+
python3 -m venv .venv
|
| 68 |
+
|
| 69 |
+
# Activate it
|
| 70 |
+
source .venv/bin/activate # On Linux/macOS
|
| 71 |
+
# OR
|
| 72 |
+
.venv\Scripts\activate # On Windows
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
#### 2.2 Install Python Dependencies
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
pip install --upgrade pip
|
| 79 |
+
pip install -r requirements.txt
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
#### 2.3 Configure Environment Variables
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
# Copy the example environment file
|
| 86 |
+
cp backend/.env.example backend/.env
|
| 87 |
+
|
| 88 |
+
# Edit the .env file with your settings
|
| 89 |
+
nano backend/.env # or use your preferred editor
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
**Required Configuration:**
|
| 93 |
+
|
| 94 |
+
```env
|
| 95 |
+
# Database
|
| 96 |
+
DATABASE_URL=postgresql://user:password@localhost:5432/saap_db
|
| 97 |
+
|
| 98 |
+
# AI Provider API Keys (⚠️ SECURITY: Never commit these!)
|
| 99 |
+
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
| 100 |
+
COLOSSUS_API_KEY=your_colossus_api_key_here
|
| 101 |
+
|
| 102 |
+
# Application Settings
|
| 103 |
+
DEBUG=True
|
| 104 |
+
LOG_LEVEL=INFO
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
#### 2.4 Set Up Database
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
# Create PostgreSQL database
|
| 111 |
+
createdb saap_db
|
| 112 |
+
|
| 113 |
+
# Run migrations (if applicable)
|
| 114 |
+
cd backend
|
| 115 |
+
alembic upgrade head
|
| 116 |
+
cd ..
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### 3. Frontend Setup
|
| 120 |
+
|
| 121 |
+
#### 3.1 Install Node.js Dependencies
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
cd frontend
|
| 125 |
+
npm install
|
| 126 |
+
cd ..
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
## Starting the Application
|
| 132 |
+
|
| 133 |
+
### Option 1: Using Startup Scripts (Recommended)
|
| 134 |
+
|
| 135 |
+
#### Start Backend
|
| 136 |
+
```bash
|
| 137 |
+
./start_backend.sh
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
The backend will be available at:
|
| 141 |
+
- **API:** http://localhost:8000
|
| 142 |
+
- **API Docs (Swagger):** http://localhost:8000/docs
|
| 143 |
+
- **API Docs (ReDoc):** http://localhost:8000/redoc
|
| 144 |
+
|
| 145 |
+
#### Start Frontend (in a new terminal)
|
| 146 |
+
```bash
|
| 147 |
+
./start_frontend.sh
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
The frontend will be available at:
|
| 151 |
+
- **Application:** http://localhost:5173
|
| 152 |
+
|
| 153 |
+
### Option 2: Manual Start
|
| 154 |
+
|
| 155 |
+
#### Start Backend Manually
|
| 156 |
+
```bash
|
| 157 |
+
# Activate virtual environment
|
| 158 |
+
source .venv/bin/activate # Linux/macOS
|
| 159 |
+
# OR
|
| 160 |
+
.venv\Scripts\activate # Windows
|
| 161 |
+
|
| 162 |
+
# Load environment variables
|
| 163 |
+
export $(cat backend/.env | grep -v '^#' | xargs)
|
| 164 |
+
|
| 165 |
+
# Start server
|
| 166 |
+
cd backend
|
| 167 |
+
python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
#### Start Frontend Manually
|
| 171 |
+
```bash
|
| 172 |
+
cd frontend
|
| 173 |
+
npm run dev
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## Verifying the Installation
|
| 179 |
+
|
| 180 |
+
### 1. Check Backend Health
|
| 181 |
+
|
| 182 |
+
```bash
|
| 183 |
+
curl http://localhost:8000/health
|
| 184 |
+
# Expected: {"status": "healthy"}
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
### 2. Access API Documentation
|
| 188 |
+
|
| 189 |
+
Open in browser: http://localhost:8000/docs
|
| 190 |
+
|
| 191 |
+
### 3. Access Frontend Application
|
| 192 |
+
|
| 193 |
+
Open in browser: http://localhost:5173
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## Project Structure
|
| 198 |
+
|
| 199 |
+
```
|
| 200 |
+
saap/
|
| 201 |
+
├── backend/ # Python FastAPI Backend
|
| 202 |
+
│ ├── agents/ # AI Agent implementations
|
| 203 |
+
│ │ ├── colossus_agent.py # Colossus (local) agent
|
| 204 |
+
│ │ └── openrouter_agent*.py # OpenRouter agents
|
| 205 |
+
│ ├── api/ # API endpoints
|
| 206 |
+
│ ├── database/ # Database models & services
|
| 207 |
+
│ ├── services/ # Business logic services
|
| 208 |
+
│ ├── .env.example # Environment template
|
| 209 |
+
│ └── main.py # FastAPI application entry
|
| 210 |
+
│
|
| 211 |
+
├── frontend/ # Vue.js 3 Frontend
|
| 212 |
+
│ ├── src/
|
| 213 |
+
│ │ ├── components/ # Vue components
|
| 214 |
+
│ │ ├── views/ # Page views
|
| 215 |
+
│ │ ├── stores/ # Pinia state management
|
| 216 |
+
│ │ └── services/ # API client services
|
| 217 |
+
│ ├── package.json
|
| 218 |
+
│ └── vite.config.js # Vite configuration
|
| 219 |
+
│
|
| 220 |
+
├── start_backend.sh # Backend startup script
|
| 221 |
+
├── start_frontend.sh # Frontend startup script
|
| 222 |
+
├── requirements.txt # Python dependencies
|
| 223 |
+
└── README.md # Full documentation
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
---
|
| 227 |
+
|
| 228 |
+
## Common Issues & Troubleshooting
|
| 229 |
+
|
| 230 |
+
### Issue: `ModuleNotFoundError` when starting backend
|
| 231 |
+
|
| 232 |
+
**Solution:** Ensure virtual environment is activated and dependencies installed:
|
| 233 |
+
```bash
|
| 234 |
+
source .venv/bin/activate
|
| 235 |
+
pip install -r requirements.txt
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### Issue: Port already in use (8000 or 5173)
|
| 239 |
+
|
| 240 |
+
**Solution:** Kill the process using the port:
|
| 241 |
+
```bash
|
| 242 |
+
# Find process using port 8000
|
| 243 |
+
lsof -i :8000 # or: netstat -tulpn | grep 8000
|
| 244 |
+
|
| 245 |
+
# Kill the process
|
| 246 |
+
kill -9 <PID>
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
### Issue: Database connection errors
|
| 250 |
+
|
| 251 |
+
**Solution:**
|
| 252 |
+
1. Verify PostgreSQL is running: `systemctl status postgresql`
|
| 253 |
+
2. Check DATABASE_URL in `backend/.env`
|
| 254 |
+
3. Ensure database exists: `createdb saap_db`
|
| 255 |
+
|
| 256 |
+
### Issue: Frontend shows "Network Error"
|
| 257 |
+
|
| 258 |
+
**Solution:**
|
| 259 |
+
1. Verify backend is running on port 8000
|
| 260 |
+
2. Check CORS settings in `backend/main.py`
|
| 261 |
+
3. Check browser console for specific errors
|
| 262 |
+
|
| 263 |
+
### Issue: Hardcoded API keys detected
|
| 264 |
+
|
| 265 |
+
**Solution:**
|
| 266 |
+
1. **DO NOT push to GitHub yet!**
|
| 267 |
+
2. Follow `SECURITY_REMEDIATION_REQUIRED.md`
|
| 268 |
+
3. Replace all hardcoded keys with environment variables
|
| 269 |
+
4. Re-scan with Gitleaks: `gitleaks detect --no-git`
|
| 270 |
+
|
| 271 |
+
---
|
| 272 |
+
|
| 273 |
+
## Development Workflow
|
| 274 |
+
|
| 275 |
+
### Running Tests
|
| 276 |
+
|
| 277 |
+
**Backend Tests:**
|
| 278 |
+
```bash
|
| 279 |
+
cd backend
|
| 280 |
+
pytest
|
| 281 |
+
```
|
| 282 |
+
|
| 283 |
+
**Frontend Tests:**
|
| 284 |
+
```bash
|
| 285 |
+
cd frontend
|
| 286 |
+
npm test
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
### Code Quality Checks
|
| 290 |
+
|
| 291 |
+
**Backend (Python):**
|
| 292 |
+
```bash
|
| 293 |
+
# Linting
|
| 294 |
+
ruff check .
|
| 295 |
+
|
| 296 |
+
# Type checking
|
| 297 |
+
mypy backend/
|
| 298 |
+
```
|
| 299 |
+
|
| 300 |
+
**Frontend (JavaScript/TypeScript):**
|
| 301 |
+
```bash
|
| 302 |
+
cd frontend
|
| 303 |
+
npm run lint
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
---
|
| 307 |
+
|
| 308 |
+
## Security Best Practices
|
| 309 |
+
|
| 310 |
+
### 🔒 Essential Security Rules
|
| 311 |
+
|
| 312 |
+
1. **Never commit API keys** to version control
|
| 313 |
+
2. **Always use environment variables** for sensitive data
|
| 314 |
+
3. **Run Gitleaks** before every commit:
|
| 315 |
+
```bash
|
| 316 |
+
gitleaks detect --no-git
|
| 317 |
+
```
|
| 318 |
+
4. **Update dependencies regularly**:
|
| 319 |
+
```bash
|
| 320 |
+
pip list --outdated # Python
|
| 321 |
+
npm outdated # Node.js
|
| 322 |
+
```
|
| 323 |
+
5. **Use strong passwords** for database and services
|
| 324 |
+
6. **Enable HTTPS** in production environments
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
## Next Steps
|
| 329 |
+
|
| 330 |
+
After successful startup:
|
| 331 |
+
|
| 332 |
+
1. ✅ **Complete Security Remediation** (see `SECURITY_REMEDIATION_REQUIRED.md`)
|
| 333 |
+
2. ✅ **Configure all agents** in the dashboard
|
| 334 |
+
3. ✅ **Test agent orchestration** with sample tasks
|
| 335 |
+
4. ✅ **Review API documentation** at http://localhost:8000/docs
|
| 336 |
+
5. ✅ **Set up development database** with test data
|
| 337 |
+
6. ✅ **Configure Redis/RabbitMQ** (optional, for advanced features)
|
| 338 |
+
|
| 339 |
+
---
|
| 340 |
+
|
| 341 |
+
## Additional Resources
|
| 342 |
+
|
| 343 |
+
- **Full Documentation:** `README.md`
|
| 344 |
+
- **Security Remediation:** `SECURITY_REMEDIATION_REQUIRED.md`
|
| 345 |
+
- **Import Notes:** `IMPORT_NOTES.md`
|
| 346 |
+
- **API Documentation:** http://localhost:8000/docs (when running)
|
| 347 |
+
- **Project Repository:** https://github.com/satwareAG/saap
|
| 348 |
+
|
| 349 |
+
---
|
| 350 |
+
|
| 351 |
+
## Support & Contact
|
| 352 |
+
|
| 353 |
+
- **Student:** Hanan Wandji Danga
|
| 354 |
+
- **Organization:** satware AG
|
| 355 |
+
- **Project:** Master's Thesis - SAAP Platform
|
| 356 |
+
- **Timeline:** 2025-09-15 to 2026-03-14
|
| 357 |
+
|
| 358 |
+
For issues or questions, please create an issue on the GitHub repository.
|
| 359 |
+
|
| 360 |
+
---
|
| 361 |
+
|
| 362 |
+
**Last Updated:** 2025-11-11
|
README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SAAP - satware AI Platform
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# SAAP - satware AI Autonomous Agent Platform
|
| 12 |
+
|
| 13 |
+
**Multi-Agent AI Platform** - FastAPI backend + Vue.js frontend running on Hugging Face Spaces.
|
| 14 |
+
|
| 15 |
+
## 🚀 Features
|
| 16 |
+
|
| 17 |
+
- **Multi-Agent Management**: Create and manage AI agents with different personalities
|
| 18 |
+
- **OpenRouter Integration**: Connect to various LLM models (GPT-4, Claude, etc.)
|
| 19 |
+
- **Real-time Chat**: WebSocket-based communication with agents
|
| 20 |
+
- **Cost Tracking**: Monitor API usage and costs
|
| 21 |
+
- **Modern UI**: Vue.js frontend with Tailwind CSS
|
| 22 |
+
|
| 23 |
+
## 🏗️ Architecture
|
| 24 |
+
|
| 25 |
+
- **Backend**: FastAPI (Python 3.11) serving API and static files
|
| 26 |
+
- **Frontend**: Vue.js + Vite (pre-built, served as static files)
|
| 27 |
+
- **Port**: 7860 (Hugging Face Spaces default)
|
| 28 |
+
- **Database**: SQLite for agent configurations
|
| 29 |
+
|
| 30 |
+
## 📡 API Endpoints
|
| 31 |
+
|
| 32 |
+
- `GET /` - Vue.js frontend
|
| 33 |
+
- `GET /api` - API health check
|
| 34 |
+
- `GET /api/v1/agents` - List all agents
|
| 35 |
+
- `POST /api/v1/agents/{id}/chat` - Chat with specific agent
|
| 36 |
+
- `GET /docs` - Interactive API documentation (Swagger UI)
|
| 37 |
+
|
| 38 |
+
## 🔧 Local Development
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
# Backend
|
| 42 |
+
cd backend
|
| 43 |
+
pip install -r requirements.txt
|
| 44 |
+
uvicorn main:app --reload --port 8000
|
| 45 |
+
|
| 46 |
+
# Frontend
|
| 47 |
+
cd frontend
|
| 48 |
+
npm install
|
| 49 |
+
npm run dev
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## 🌐 Deployment
|
| 53 |
+
|
| 54 |
+
This version is optimized for Hugging Face Spaces:
|
| 55 |
+
- Single Dockerfile with multi-stage build
|
| 56 |
+
- FastAPI serves both API and frontend
|
| 57 |
+
- No nginx or supervisord required
|
| 58 |
+
- Environment variables via HF Secrets
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
**Built with ❤️ by satware AG**
|
| 63 |
+
Master Thesis Project - Applied Computer Science
|
SECURITY_REMEDIATION_REQUIRED.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚨 CRITICAL SECURITY REMEDIATION REQUIRED
|
| 2 |
+
|
| 3 |
+
**Status:** ⛔ **DO NOT PUSH TO GITHUB YET**
|
| 4 |
+
**Date:** 2025-11-11
|
| 5 |
+
**Severity:** CRITICAL
|
| 6 |
+
|
| 7 |
+
## Security Issue Discovered
|
| 8 |
+
|
| 9 |
+
After importing source code from le-chantier, security scanning revealed **hardcoded API keys in 40+ files** scattered throughout the codebase.
|
| 10 |
+
|
| 11 |
+
## API Keys Found
|
| 12 |
+
|
| 13 |
+
**Two API keys hardcoded in multiple locations:**
|
| 14 |
+
|
| 15 |
+
1. **Colossus API Key:** `sk-dBoxml3krytIRLdjr35Lnw`
|
| 16 |
+
2. **OpenRouter API Key:** `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`
|
| 17 |
+
|
| 18 |
+
## Affected Files (40+ instances)
|
| 19 |
+
|
| 20 |
+
### Agents (6 instances)
|
| 21 |
+
- `backend/agents/colossus_agent.py` - Default api_key parameter
|
| 22 |
+
- `backend/agents/colossus_saap_agent.py` - API_KEY constant
|
| 23 |
+
- `backend/agents/openrouter_agent_enhanced.py` - API_KEY constant
|
| 24 |
+
- `backend/agents/openrouter_saap_agent.py` - COLOSSUS_KEY constant
|
| 25 |
+
|
| 26 |
+
### API Clients (2 instances)
|
| 27 |
+
- `backend/api/colossus_client.py` - Default api_key parameter
|
| 28 |
+
- `backend/api/openrouter_client.py` - Hardcoded api_key variable
|
| 29 |
+
|
| 30 |
+
### Configuration (4 instances)
|
| 31 |
+
- `backend/config/settings.py` - Default values for both keys (2 instances)
|
| 32 |
+
- `backend/settings.py` - Duplicate default values (2 instances)
|
| 33 |
+
|
| 34 |
+
### Models & Schemas (12+ instances)
|
| 35 |
+
- `backend/models/agent.py` - Template defaults (3 instances)
|
| 36 |
+
- `backend/models/agent_schema.json` - Schema examples (3 instances)
|
| 37 |
+
- `backend/models/agent_templates.json` - Template defaults (5 instances)
|
| 38 |
+
- `backend/agent.py` - Duplicate file (3 instances)
|
| 39 |
+
- `backend/agent_schema.json` - Duplicate schema (3 instances)
|
| 40 |
+
- `backend/agent_templates.json` - Duplicate templates (5 instances)
|
| 41 |
+
|
| 42 |
+
### Services (3 instances)
|
| 43 |
+
- `backend/services/agent_manager_hybrid.py` - Default fallback
|
| 44 |
+
- `backend/services/agent_manager_hybrid_fixed.py` - Default fallback
|
| 45 |
+
- `backend/services/openrouter_integration.py` - Constructor default
|
| 46 |
+
- `backend/openrouter_integration.py` - Duplicate file
|
| 47 |
+
- `backend/agent_manager_hybrid.py` - Duplicate file
|
| 48 |
+
- `backend/agent_manager_hybrid.py.backup` - Backup file
|
| 49 |
+
- `backend/agent_manager_hybrid_fixed.py` - Duplicate file
|
| 50 |
+
|
| 51 |
+
### Scripts & Tests (1 instance)
|
| 52 |
+
- `backend/scripts/test_colossus_integration.py` - Test API_KEY constant
|
| 53 |
+
- `backend/test_colossus_integration.py` - Duplicate file
|
| 54 |
+
|
| 55 |
+
### Main Application (1 instance)
|
| 56 |
+
- `backend/main.py` - Hardcoded openrouter_key variable
|
| 57 |
+
|
| 58 |
+
### Environment Template (2 instances)
|
| 59 |
+
- `backend/.env.example` - **BOTH keys present** (may be acceptable for examples, but verify these are dummy keys first)
|
| 60 |
+
|
| 61 |
+
## Remediation Plan
|
| 62 |
+
|
| 63 |
+
### Option 1: Environment Variables (Recommended)
|
| 64 |
+
|
| 65 |
+
**Replace all hardcoded keys with environment variable lookups:**
|
| 66 |
+
|
| 67 |
+
```python
|
| 68 |
+
# BEFORE (agents/colossus_agent.py)
|
| 69 |
+
api_key: str = "sk-dBoxml3krytIRLdjr35Lnw"
|
| 70 |
+
|
| 71 |
+
# AFTER
|
| 72 |
+
import os
|
| 73 |
+
api_key: str = os.getenv("COLOSSUS_API_KEY", "")
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
```python
|
| 77 |
+
# BEFORE (config/settings.py)
|
| 78 |
+
default="sk-dBoxml3krytIRLdjr35Lnw"
|
| 79 |
+
|
| 80 |
+
# AFTER
|
| 81 |
+
default=os.getenv("COLOSSUS_API_KEY", "")
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### Option 2: Remove Defaults Entirely (Most Secure)
|
| 85 |
+
|
| 86 |
+
**Force explicit configuration, no fallbacks:**
|
| 87 |
+
|
| 88 |
+
```python
|
| 89 |
+
# BEFORE
|
| 90 |
+
api_key: str = "sk-dBoxml3krytIRLdjr35Lnw"
|
| 91 |
+
|
| 92 |
+
# AFTER
|
| 93 |
+
api_key: str # No default - must be provided explicitly
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Option 3: Use Placeholder Values
|
| 97 |
+
|
| 98 |
+
**Replace with obvious placeholders:**
|
| 99 |
+
|
| 100 |
+
```python
|
| 101 |
+
# BEFORE
|
| 102 |
+
api_key: str = "sk-dBoxml3krytIRLdjr35Lnw"
|
| 103 |
+
|
| 104 |
+
# AFTER
|
| 105 |
+
api_key: str = "YOUR_COLOSSUS_API_KEY_HERE"
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
## Automated Remediation Script
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
#!/bin/bash
|
| 112 |
+
# cleanup-secrets.sh
|
| 113 |
+
|
| 114 |
+
# Replace Colossus API key with environment variable
|
| 115 |
+
find backend/ -type f -name "*.py" -exec sed -i \
|
| 116 |
+
's/sk-dBoxml3krytIRLdjr35Lnw/os.getenv("COLOSSUS_API_KEY", "")/g' {} +
|
| 117 |
+
|
| 118 |
+
# Replace OpenRouter API key with environment variable
|
| 119 |
+
find backend/ -type f -name "*.py" -exec sed -i \
|
| 120 |
+
's/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/os.getenv("OPENROUTER_API_KEY", "")/g' {} +
|
| 121 |
+
|
| 122 |
+
# For JSON files - replace with placeholders
|
| 123 |
+
find backend/ -type f -name "*.json" -exec sed -i \
|
| 124 |
+
's/sk-dBoxml3krytIRLdjr35Lnw/YOUR_COLOSSUS_API_KEY_HERE/g' {} +
|
| 125 |
+
|
| 126 |
+
find backend/ -type f -name "*.json" -exec sed -i \
|
| 127 |
+
's/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/YOUR_OPENROUTER_API_KEY_HERE/g' {} +
|
| 128 |
+
|
| 129 |
+
echo "✅ Secrets remediated - verify changes before committing"
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## Manual Review Required
|
| 133 |
+
|
| 134 |
+
**Before running automated fixes:**
|
| 135 |
+
|
| 136 |
+
1. **Verify if these are real API keys or test keys**
|
| 137 |
+
- If test keys: Can replace with placeholders
|
| 138 |
+
- If real keys: **MUST invalidate/rotate immediately**
|
| 139 |
+
|
| 140 |
+
2. **Check .env.example**
|
| 141 |
+
- If these are example keys: Acceptable to keep
|
| 142 |
+
- If real keys: Replace with `YOUR_*_API_KEY_HERE`
|
| 143 |
+
|
| 144 |
+
3. **Add `import os` statements**
|
| 145 |
+
- Python files using `os.getenv()` need `import os` at top
|
| 146 |
+
|
| 147 |
+
## Immediate Actions Required
|
| 148 |
+
|
| 149 |
+
### DO NOT:
|
| 150 |
+
- ❌ Push to GitHub without remediation
|
| 151 |
+
- ❌ Commit files with hardcoded keys
|
| 152 |
+
- ❌ Deploy code with hardcoded keys
|
| 153 |
+
- ❌ Share repository publicly
|
| 154 |
+
|
| 155 |
+
### DO:
|
| 156 |
+
- ✅ Review remediation options with team
|
| 157 |
+
- ✅ Decide on remediation strategy (Option 1, 2, or 3)
|
| 158 |
+
- ✅ Run remediation script OR manually fix
|
| 159 |
+
- ✅ Verify all fixes with `grep` scan
|
| 160 |
+
- ✅ Test application still works after remediation
|
| 161 |
+
- ✅ Rotate API keys if they are real/active keys
|
| 162 |
+
- ✅ Update .env.example with placeholders
|
| 163 |
+
- ✅ Commit remediated code only
|
| 164 |
+
|
| 165 |
+
## Verification After Remediation
|
| 166 |
+
|
| 167 |
+
```bash
|
| 168 |
+
# Scan for remaining hardcoded keys
|
| 169 |
+
grep -r -i "sk-or-v1\|sk-dBoxml" backend/
|
| 170 |
+
|
| 171 |
+
# Should return ZERO results (or only .env.example if using placeholders)
|
| 172 |
+
# If any results found in code files, continue remediation
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
## Post-Remediation Checklist
|
| 176 |
+
|
| 177 |
+
- [ ] All hardcoded keys replaced in Python files
|
| 178 |
+
- [ ] All hardcoded keys replaced in JSON files
|
| 179 |
+
- [ ] .env.example contains only placeholders
|
| 180 |
+
- [ ] No secrets in git history (we're starting fresh, so OK)
|
| 181 |
+
- [ ] Application tested with environment variables
|
| 182 |
+
- [ ] README updated with environment setup instructions
|
| 183 |
+
- [ ] .gitignore verified (already created)
|
| 184 |
+
- [ ] Final security scan clean
|
| 185 |
+
|
| 186 |
+
## Contact for Questions
|
| 187 |
+
|
| 188 |
+
**Security Team:**
|
| 189 |
+
- CTO Michael Wegener ([email protected])
|
| 190 |
+
|
| 191 |
+
**Master Thesis Supervisor:**
|
| 192 |
+
- (Contact info)
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
**REMEDIATION STATUS:** ⏳ PENDING
|
| 197 |
+
**Last Updated:** 2025-11-11 12:46 CET
|
| 198 |
+
**Action Owner:** Hanan (Master Student)
|
SECURITY_SCAN_REPORT.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚨 SAAP Security Scan Report - Gitleaks
|
| 2 |
+
**Datum:** 2025-11-11 15:49 UTC+1
|
| 3 |
+
**Scanner:** Gitleaks v8.27.2
|
| 4 |
+
**Status:** ⚠️ KRITISCH - 31 Secrets gefunden
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## Zusammenfassung
|
| 9 |
+
|
| 10 |
+
✅ **Git History:** Keine Secrets in Commits (sauber)
|
| 11 |
+
❌ **Working Directory:** 31 hardcoded API-Keys gefunden
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Gefundene Secrets (Übersicht)
|
| 16 |
+
|
| 17 |
+
### Kritische Dateien mit hardcoded API-Keys:
|
| 18 |
+
|
| 19 |
+
| Datei | Zeile | Secret Type | Status |
|
| 20 |
+
|-------|-------|-------------|--------|
|
| 21 |
+
| `backend/.env` | 23, 65 | OPENROUTER_API_KEY, COLOSSUS_API_KEY | ⚠️ .env sollte nicht committed sein |
|
| 22 |
+
| `backend/agents/colossus_agent.py` | - | api_key hardcoded | 🚨 KRITISCH |
|
| 23 |
+
| `backend/agents/colossus_saap_agent.py` | 338 | API_KEY hardcoded | 🚨 KRITISCH |
|
| 24 |
+
| `backend/agents/openrouter_agent_enhanced.py` | 316 | API_KEY hardcoded | 🚨 KRITISCH |
|
| 25 |
+
| `backend/agents/openrouter_saap_agent.py` | 275 | COLOSSUS_KEY hardcoded | 🚨 KRITISCH |
|
| 26 |
+
| `backend/test_colossus_integration.py` | 24 | API_KEY hardcoded | ⚠️ Test-Code |
|
| 27 |
+
| `backend/scripts/test_colossus_integration.py` | 24 | API_KEY hardcoded | ⚠️ Test-Code |
|
| 28 |
+
| `backend/main.py` | 108 | openrouter_key hardcoded | 🚨 KRITISCH |
|
| 29 |
+
| `backend/agent.py` | 244, 273, 302 | api_key hardcoded | 🚨 KRITISCH |
|
| 30 |
+
| `backend/api/openrouter_client.py` | 355 | api_key hardcoded | 🚨 KRITISCH |
|
| 31 |
+
| `backend/agent_templates.json` | 21, 48, 75, 102, 123 | api_key in JSON | ⚠️ Template-Daten |
|
| 32 |
+
| `backend/agent_schema.json` | 200, 226, 251 | api_key in JSON | ⚠️ Schema-Daten |
|
| 33 |
+
| `backend/models/agent_templates.json` | 21, 48, 75, 102, 123 | api_key in JSON | ⚠️ Template-Daten |
|
| 34 |
+
| `backend/models/agent_schema.json` | 200, 226, 251 | api_key in JSON | ⚠️ Schema-Daten |
|
| 35 |
+
| `backend/models/agent.py` | 244, 273, 302 | api_key hardcoded | 🚨 KRITISCH |
|
| 36 |
+
|
| 37 |
+
**Total:** 31 Findings
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## Lösung: API-Keys aus Environment Variables einlesen
|
| 42 |
+
|
| 43 |
+
### FIX für `backend/agents/colossus_agent.py`
|
| 44 |
+
|
| 45 |
+
**VORHER (❌ Unsicher):**
|
| 46 |
+
```python
|
| 47 |
+
@dataclass
|
| 48 |
+
class ColossusConfig:
|
| 49 |
+
"""colossus Server Configuration"""
|
| 50 |
+
base_url: str = "https://ai.adrian-schupp.de"
|
| 51 |
+
api_key: str = "sk-dBoxml3krytIRLdjr35Lnw" # 🚨 HARDCODED!
|
| 52 |
+
model: str = "mistral-small3.2:24b-instruct-2506"
|
| 53 |
+
max_tokens: int = 1000
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
**NACHHER (✅ Sicher):**
|
| 57 |
+
```python
|
| 58 |
+
import os
|
| 59 |
+
from dataclasses import dataclass, field
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class ColossusConfig:
|
| 63 |
+
"""colossus Server Configuration"""
|
| 64 |
+
base_url: str = "https://ai.adrian-schupp.de"
|
| 65 |
+
api_key: str = field(default_factory=lambda: os.getenv("COLOSSUS_API_KEY", ""))
|
| 66 |
+
model: str = "mistral-small3.2:24b-instruct-2506"
|
| 67 |
+
max_tokens: int = 1000
|
| 68 |
+
|
| 69 |
+
def __post_init__(self):
|
| 70 |
+
if not self.api_key:
|
| 71 |
+
raise ValueError(
|
| 72 |
+
"COLOSSUS_API_KEY environment variable not set. "
|
| 73 |
+
"Please configure it in your .env file."
|
| 74 |
+
)
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
### Alternative: Normale Klasse statt Dataclass
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
import os
|
| 81 |
+
|
| 82 |
+
class ColossusConfig:
|
| 83 |
+
"""colossus Server Configuration"""
|
| 84 |
+
|
| 85 |
+
def __init__(self):
|
| 86 |
+
self.base_url = "https://ai.adrian-schupp.de"
|
| 87 |
+
self.api_key = os.getenv("COLOSSUS_API_KEY")
|
| 88 |
+
self.model = "mistral-small3.2:24b-instruct-2506"
|
| 89 |
+
self.max_tokens = 1000
|
| 90 |
+
self.temperature = 0.7
|
| 91 |
+
self.timeout = 30
|
| 92 |
+
|
| 93 |
+
# Validation
|
| 94 |
+
if not self.api_key:
|
| 95 |
+
raise ValueError(
|
| 96 |
+
"❌ COLOSSUS_API_KEY not found in environment variables.\n"
|
| 97 |
+
"Set it in backend/.env file:\n"
|
| 98 |
+
"COLOSSUS_API_KEY=sk-your-actual-key-here"
|
| 99 |
+
)
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### FIX für Test-Code
|
| 103 |
+
|
| 104 |
+
**VORHER:**
|
| 105 |
+
```python
|
| 106 |
+
if __name__ == "__main__":
|
| 107 |
+
API_KEY = "sk-dBoxml3krytIRLdjr35Lnw" # ❌ HARDCODED
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**NACHHER:**
|
| 111 |
+
```python
|
| 112 |
+
import os
|
| 113 |
+
from dotenv import load_dotenv
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
load_dotenv() # Lädt .env Datei
|
| 117 |
+
API_KEY = os.getenv("COLOSSUS_API_KEY")
|
| 118 |
+
|
| 119 |
+
if not API_KEY:
|
| 120 |
+
print("❌ Error: COLOSSUS_API_KEY not set in .env file")
|
| 121 |
+
exit(1)
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## Sofortige Maßnahmen (MANDATORY)
|
| 127 |
+
|
| 128 |
+
### 1. `.env` Datei prüfen
|
| 129 |
+
```bash
|
| 130 |
+
# Prüfe ob .env committed wurde
|
| 131 |
+
git status backend/.env
|
| 132 |
+
|
| 133 |
+
# Falls committed, aus Git entfernen:
|
| 134 |
+
git rm --cached backend/.env
|
| 135 |
+
git commit -m "security: remove .env from git tracking"
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
### 2. Hardcoded Keys entfernen
|
| 139 |
+
|
| 140 |
+
**Alle betroffenen Dateien:**
|
| 141 |
+
- `backend/agents/colossus_agent.py`
|
| 142 |
+
- `backend/agents/colossus_saap_agent.py`
|
| 143 |
+
- `backend/agents/openrouter_agent_enhanced.py`
|
| 144 |
+
- `backend/agents/openrouter_saap_agent.py`
|
| 145 |
+
- `backend/main.py`
|
| 146 |
+
- `backend/agent.py`
|
| 147 |
+
- `backend/models/agent.py`
|
| 148 |
+
- `backend/api/openrouter_client.py`
|
| 149 |
+
|
| 150 |
+
**Ersetze in allen Dateien:**
|
| 151 |
+
```python
|
| 152 |
+
# ❌ VORHER
|
| 153 |
+
api_key = "sk-dBoxml3krytIRLdjr35Lnw"
|
| 154 |
+
|
| 155 |
+
# ✅ NACHHER
|
| 156 |
+
import os
|
| 157 |
+
api_key = os.getenv("COLOSSUS_API_KEY")
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### 3. .env richtig konfigurieren
|
| 161 |
+
|
| 162 |
+
**backend/.env** (niemals committen!):
|
| 163 |
+
```bash
|
| 164 |
+
# Colossus API Configuration
|
| 165 |
+
COLOSSUS_API_KEY=sk-dBoxml3krytIRLdjr35Lnw
|
| 166 |
+
|
| 167 |
+
# OpenRouter API Configuration
|
| 168 |
+
OPENROUTER_API_KEY=dein-openrouter-key-hier
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
### 4. .gitignore validieren
|
| 172 |
+
|
| 173 |
+
✅ **Bereits korrekt:**
|
| 174 |
+
```gitignore
|
| 175 |
+
# Secrets
|
| 176 |
+
.env
|
| 177 |
+
.env.*
|
| 178 |
+
!.env.example
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
### 5. Dependencies installieren
|
| 182 |
+
|
| 183 |
+
Falls `python-dotenv` fehlt:
|
| 184 |
+
```bash
|
| 185 |
+
pip install python-dotenv
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
In allen Python-Dateien am Anfang:
|
| 189 |
+
```python
|
| 190 |
+
from dotenv import load_dotenv
|
| 191 |
+
import os
|
| 192 |
+
|
| 193 |
+
load_dotenv() # Lädt .env automatisch
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## Template & Schema Dateien
|
| 199 |
+
|
| 200 |
+
⚠️ **JSON Template/Schema Dateien mit Platzhaltern:**
|
| 201 |
+
- `backend/agent_templates.json`
|
| 202 |
+
- `backend/agent_schema.json`
|
| 203 |
+
- `backend/models/agent_templates.json`
|
| 204 |
+
- `backend/models/agent_schema.json`
|
| 205 |
+
|
| 206 |
+
**Lösung:**
|
| 207 |
+
```json
|
| 208 |
+
{
|
| 209 |
+
"api_key": "{{COLOSSUS_API_KEY}}",
|
| 210 |
+
"model": "mistral-small3.2:24b-instruct-2506"
|
| 211 |
+
}
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
Beim Laden ersetzen:
|
| 215 |
+
```python
|
| 216 |
+
import json
|
| 217 |
+
import os
|
| 218 |
+
|
| 219 |
+
with open('agent_templates.json') as f:
|
| 220 |
+
template = json.load(f)
|
| 221 |
+
|
| 222 |
+
# Replace placeholders
|
| 223 |
+
for agent in template:
|
| 224 |
+
if '{{COLOSSUS_API_KEY}}' in agent.get('api_key', ''):
|
| 225 |
+
agent['api_key'] = os.getenv('COLOSSUS_API_KEY')
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
## API-Key Rotation (EMPFOHLEN)
|
| 231 |
+
|
| 232 |
+
Da der Key `sk-dBoxml3krytIRLdjr35Lnw` möglicherweise exponiert wurde:
|
| 233 |
+
|
| 234 |
+
1. **Neuen API-Key generieren** beim Colossus-Provider
|
| 235 |
+
2. **Alten Key deaktivieren/löschen**
|
| 236 |
+
3. **Neuen Key in `.env` eintragen**
|
| 237 |
+
4. **Deployment aktualisieren**
|
| 238 |
+
|
| 239 |
+
---
|
| 240 |
+
|
| 241 |
+
## Best Practices
|
| 242 |
+
|
| 243 |
+
### ✅ DO's:
|
| 244 |
+
- Verwende **Environment Variables** für alle Secrets
|
| 245 |
+
- Nutze **python-dotenv** für lokale Entwicklung
|
| 246 |
+
- Behalte **.env.example** mit Platzhaltern im Repo
|
| 247 |
+
- Validiere Secrets beim App-Start
|
| 248 |
+
- Dokumentiere benötigte Env-Vars in README
|
| 249 |
+
|
| 250 |
+
### ❌ DON'Ts:
|
| 251 |
+
- **NIEMALS** API-Keys hardcoded im Code
|
| 252 |
+
- **NIEMALS** `.env` in Git committen
|
| 253 |
+
- **NIEMALS** Secrets in Logs ausgeben
|
| 254 |
+
- **NIEMALS** Test-Keys in Production verwenden
|
| 255 |
+
|
| 256 |
+
---
|
| 257 |
+
|
| 258 |
+
## Nächste Schritte
|
| 259 |
+
|
| 260 |
+
1. [ ] Alle hardcoded API-Keys durch `os.getenv()` ersetzen
|
| 261 |
+
2. [ ] `.env` aus Git-Tracking entfernen (falls committed)
|
| 262 |
+
3. [ ] API-Key rotieren (neuen Key generieren)
|
| 263 |
+
4. [ ] Secrets Management Tool erwägen (z.B. HashiCorp Vault)
|
| 264 |
+
5. [ ] Pre-commit Hook für Gitleaks einrichten
|
| 265 |
+
6. [ ] Security Audit wiederholen nach Fixes
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
## Gitleaks Pre-Commit Hook (Optional)
|
| 270 |
+
|
| 271 |
+
**Installation:**
|
| 272 |
+
```bash
|
| 273 |
+
# Install pre-commit
|
| 274 |
+
pip install pre-commit
|
| 275 |
+
|
| 276 |
+
# Create .pre-commit-config.yaml
|
| 277 |
+
cat > .pre-commit-config.yaml << 'EOF'
|
| 278 |
+
repos:
|
| 279 |
+
- repo: https://github.com/gitleaks/gitleaks
|
| 280 |
+
rev: v8.27.2
|
| 281 |
+
hooks:
|
| 282 |
+
- id: gitleaks
|
| 283 |
+
EOF
|
| 284 |
+
|
| 285 |
+
# Install hook
|
| 286 |
+
pre-commit install
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
Verhindert zukünftig das Committen von Secrets!
|
| 290 |
+
|
| 291 |
+
---
|
| 292 |
+
|
| 293 |
+
**Erstellt:** 2025-11-11
|
| 294 |
+
**Next Scan:** Nach Implementierung der Fixes
|
SECURITY_SETUP_COMPLETE.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔒 SAAP Security Remediation - COMPLETE
|
| 2 |
+
|
| 3 |
+
**Date:** 2025-11-16
|
| 4 |
+
**Status:** ✅ All code files secured (26/31 secrets removed)
|
| 5 |
+
**Remaining:** 5 acceptable findings (.env + documentation)
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## ✅ What Was Fixed
|
| 10 |
+
|
| 11 |
+
### 1. Production Code (26 Secrets Removed)
|
| 12 |
+
All hardcoded API keys replaced with environment variable placeholders:
|
| 13 |
+
|
| 14 |
+
**Python Files (9 files):**
|
| 15 |
+
- ✅ `backend/agents/colossus_agent.py`
|
| 16 |
+
- ✅ `backend/agents/colossus_saap_agent.py`
|
| 17 |
+
- ✅ `backend/agents/openrouter_agent_enhanced.py`
|
| 18 |
+
- ✅ `backend/agents/openrouter_saap_agent.py`
|
| 19 |
+
- ✅ `backend/main.py`
|
| 20 |
+
- ✅ `backend/agent.py`
|
| 21 |
+
- ✅ `backend/models/agent.py`
|
| 22 |
+
- ✅ `backend/api/openrouter_client.py`
|
| 23 |
+
- ✅ `backend/test_colossus_integration.py`
|
| 24 |
+
- ✅ `backend/scripts/test_colossus_integration.py`
|
| 25 |
+
|
| 26 |
+
**JSON Template Files (4 files, 16 occurrences):**
|
| 27 |
+
- ✅ `backend/agent_templates.json` (5 fixes)
|
| 28 |
+
- ✅ `backend/agent_schema.json` (3 fixes)
|
| 29 |
+
- ✅ `backend/models/agent_templates.json` (5 fixes)
|
| 30 |
+
- ✅ `backend/models/agent_schema.json` (3 fixes)
|
| 31 |
+
|
| 32 |
+
**Pattern Applied:**
|
| 33 |
+
```python
|
| 34 |
+
# OLD (hardcoded):
|
| 35 |
+
api_key = "sk-dBoxml3krytIRLdjr35Lnw"
|
| 36 |
+
|
| 37 |
+
# NEW (environment variable):
|
| 38 |
+
import os
|
| 39 |
+
from dotenv import load_dotenv
|
| 40 |
+
load_dotenv()
|
| 41 |
+
api_key = os.getenv("COLOSSUS_API_KEY")
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
```json
|
| 45 |
+
// OLD (hardcoded):
|
| 46 |
+
"api_key": "sk-dBoxml3krytIRLdjr35Lnw"
|
| 47 |
+
|
| 48 |
+
// NEW (placeholder):
|
| 49 |
+
"api_key": "{{COLOSSUS_API_KEY}}"
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 2. Git Security Verified
|
| 53 |
+
- ✅ **Git history clean** - No secrets ever committed
|
| 54 |
+
- ✅ **.gitignore configured** - `.env` and `.env.*` excluded
|
| 55 |
+
- ✅ **backend/.env contains real keys** - NOT tracked (correct behavior)
|
| 56 |
+
|
| 57 |
+
### 3. Remaining Findings (Acceptable)
|
| 58 |
+
**5 findings remaining:**
|
| 59 |
+
- `backend/.env` (Lines 23, 65) - **CORRECT** - Real keys, not in version control
|
| 60 |
+
- `SECURITY_SCAN_REPORT.md` (Lines 107, 153, 165) - **ACCEPTABLE** - Documentation examples only
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## 🚀 Next Steps for User
|
| 65 |
+
|
| 66 |
+
### Step 1: Install Pre-commit Hooks (Required)
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# Install pre-commit
|
| 70 |
+
sudo pacman -S pre-commit
|
| 71 |
+
|
| 72 |
+
# Enable in repository
|
| 73 |
+
cd /home/shadowadmin/WebstormProjects/saap
|
| 74 |
+
pre-commit install
|
| 75 |
+
|
| 76 |
+
# Test (should pass - all secrets already removed)
|
| 77 |
+
pre-commit run --all-files
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**What this does:**
|
| 81 |
+
- ✅ Blocks commits with hardcoded secrets (Gitleaks)
|
| 82 |
+
- ✅ Checks YAML/JSON syntax
|
| 83 |
+
- ✅ Detects private keys
|
| 84 |
+
- ✅ Formats Python code (Black)
|
| 85 |
+
- ✅ Fixes trailing whitespace
|
| 86 |
+
|
| 87 |
+
### Step 2: API Key Rotation (Recommended)
|
| 88 |
+
|
| 89 |
+
The exposed API key `sk-dBoxml3krytIRLdjr35Lnw` was found in code (now fixed) but should be rotated.
|
| 90 |
+
|
| 91 |
+
**Rotation Steps:**
|
| 92 |
+
|
| 93 |
+
1. **Generate New API Key**
|
| 94 |
+
- Visit: https://ai.adrian-schupp.de
|
| 95 |
+
- Navigate to API Keys section
|
| 96 |
+
- Generate new key
|
| 97 |
+
- Copy new key securely
|
| 98 |
+
|
| 99 |
+
2. **Update backend/.env**
|
| 100 |
+
```bash
|
| 101 |
+
nano backend/.env
|
| 102 |
+
|
| 103 |
+
# Replace old key with new:
|
| 104 |
+
COLOSSUS_API_KEY=sk-NEW_KEY_HERE
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
3. **Test Application**
|
| 108 |
+
```bash
|
| 109 |
+
cd backend
|
| 110 |
+
python -m uvicorn main:app --reload
|
| 111 |
+
# Verify agents connect successfully
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
4. **Invalidate Old Key**
|
| 115 |
+
- Return to https://ai.adrian-schupp.de
|
| 116 |
+
- Delete old key `sk-dBoxml3krytIRLdjr35Lnw`
|
| 117 |
+
- Confirm deletion
|
| 118 |
+
|
| 119 |
+
5. **Document Rotation**
|
| 120 |
+
```bash
|
| 121 |
+
echo "$(date): Rotated COLOSSUS_API_KEY after repository security scan" >> SECURITY_LOG.md
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### Step 3: Verify Security Setup
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
# Run Gitleaks scan (should show ≤5 findings)
|
| 128 |
+
gitleaks detect --no-git
|
| 129 |
+
|
| 130 |
+
# Expected findings:
|
| 131 |
+
# - backend/.env (2 keys) ← CORRECT
|
| 132 |
+
# - SECURITY_SCAN_REPORT.md (3 examples) ← ACCEPTABLE
|
| 133 |
+
|
| 134 |
+
# Try to commit with a test secret (should be blocked)
|
| 135 |
+
echo 'TEST_KEY="sk-test123"' > test_secret.txt
|
| 136 |
+
git add test_secret.txt
|
| 137 |
+
git commit -m "test"
|
| 138 |
+
# ↑ Should FAIL with Gitleaks error
|
| 139 |
+
|
| 140 |
+
# Clean up test
|
| 141 |
+
rm test_secret.txt
|
| 142 |
+
git reset
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## 📊 Security Metrics
|
| 148 |
+
|
| 149 |
+
| Metric | Before | After | Improvement |
|
| 150 |
+
|--------|--------|-------|-------------|
|
| 151 |
+
| **Total Secrets** | 31 | 5 | **84% reduction** |
|
| 152 |
+
| **Code Files with Secrets** | 13 | 0 | **100% fixed** |
|
| 153 |
+
| **Git History Clean** | ✅ | ✅ | **Maintained** |
|
| 154 |
+
| **Automated Prevention** | ❌ | ✅ | **Pre-commit hooks** |
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## 🔐 Security Best Practices Going Forward
|
| 159 |
+
|
| 160 |
+
### 1. Environment Variables
|
| 161 |
+
- ✅ **DO:** Store secrets in `backend/.env` (not tracked)
|
| 162 |
+
- ✅ **DO:** Use `os.getenv("KEY_NAME")` in code
|
| 163 |
+
- ❌ **DON'T:** Hardcode secrets in any file
|
| 164 |
+
- ❌ **DON'T:** Commit `.env` to git
|
| 165 |
+
|
| 166 |
+
### 2. Pre-commit Hooks
|
| 167 |
+
- ✅ Run before every commit (automatic)
|
| 168 |
+
- ✅ Blocks secrets from being committed
|
| 169 |
+
- ✅ Maintains code quality standards
|
| 170 |
+
|
| 171 |
+
### 3. API Key Management
|
| 172 |
+
- ✅ Rotate keys quarterly (or after exposure)
|
| 173 |
+
- ✅ Use different keys per environment (dev/staging/prod)
|
| 174 |
+
- ✅ Document rotation in security log
|
| 175 |
+
- ✅ Invalidate old keys immediately after rotation
|
| 176 |
+
|
| 177 |
+
### 4. Code Review
|
| 178 |
+
- ✅ Check for hardcoded secrets in PRs
|
| 179 |
+
- ✅ Verify `.env.example` updated (never with real keys)
|
| 180 |
+
- ✅ Test with environment variables locally
|
| 181 |
+
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
## 📝 Files Modified
|
| 185 |
+
|
| 186 |
+
### Created:
|
| 187 |
+
- ✅ `.pre-commit-config.yaml` - Pre-commit hook configuration
|
| 188 |
+
- ✅ `SECURITY_SETUP_COMPLETE.md` - This document
|
| 189 |
+
- ✅ `SECURITY_SCAN_REPORT.md` - Initial scan report (already existed)
|
| 190 |
+
|
| 191 |
+
### Modified (26 files):
|
| 192 |
+
- Python agent files (10)
|
| 193 |
+
- JSON template files (4)
|
| 194 |
+
- Total secrets replaced: **26**
|
| 195 |
+
|
| 196 |
+
### Protected:
|
| 197 |
+
- `backend/.env` - Contains real keys, NOT in git ✅
|
| 198 |
+
- `.gitignore` - Excludes `.env` files ✅
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## ✅ Completion Checklist
|
| 203 |
+
|
| 204 |
+
**Automated (Complete):**
|
| 205 |
+
- [x] Scanned repository for secrets
|
| 206 |
+
- [x] Replaced 26 hardcoded secrets with environment variables
|
| 207 |
+
- [x] Verified git history clean
|
| 208 |
+
- [x] Confirmed .gitignore excludes .env
|
| 209 |
+
- [x] Created pre-commit hook configuration
|
| 210 |
+
|
| 211 |
+
**User Actions (Required):**
|
| 212 |
+
- [ ] Install pre-commit: `sudo pacman -S pre-commit`
|
| 213 |
+
- [ ] Enable hooks: `pre-commit install`
|
| 214 |
+
- [ ] Test hooks: `pre-commit run --all-files`
|
| 215 |
+
- [ ] Rotate exposed API key at https://ai.adrian-schupp.de
|
| 216 |
+
- [ ] Update `backend/.env` with new key
|
| 217 |
+
- [ ] Test application with new key
|
| 218 |
+
- [ ] Delete old key from provider
|
| 219 |
+
|
| 220 |
+
---
|
| 221 |
+
|
| 222 |
+
## 🎯 Summary
|
| 223 |
+
|
| 224 |
+
**Security remediation successfully completed!**
|
| 225 |
+
|
| 226 |
+
- ✅ **84% reduction** in secret findings (31 → 5)
|
| 227 |
+
- ✅ **100% of code files** secured
|
| 228 |
+
- ✅ **Git history** remains clean
|
| 229 |
+
- ✅ **Automated prevention** configured
|
| 230 |
+
- ⚠️ **User action required:** Install pre-commit hooks & rotate API key
|
| 231 |
+
|
| 232 |
+
**Questions?** Review `SECURITY_SCAN_REPORT.md` for detailed findings.
|
| 233 |
+
|
| 234 |
+
**Next security scan:** Quarterly (every 3 months) or after major changes.
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
**Generated:** 2025-11-16 06:39 UTC
|
| 239 |
+
**Scan Tool:** Gitleaks v8.27.2
|
| 240 |
+
**Remediation:** Automated environment variable conversion
|
backend/.dockerignore
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# Backend .dockerignore
|
| 3 |
+
# ==========================================
|
| 4 |
+
|
| 5 |
+
# Environment files (SECURITY - Never include in image)
|
| 6 |
+
.env
|
| 7 |
+
.env.*
|
| 8 |
+
!.env.example
|
| 9 |
+
|
| 10 |
+
# Python cache
|
| 11 |
+
__pycache__/
|
| 12 |
+
*.py[cod]
|
| 13 |
+
*$py.class
|
| 14 |
+
*.so
|
| 15 |
+
.Python
|
| 16 |
+
*.egg-info/
|
| 17 |
+
dist/
|
| 18 |
+
build/
|
| 19 |
+
|
| 20 |
+
# Virtual environments
|
| 21 |
+
venv/
|
| 22 |
+
env/
|
| 23 |
+
ENV/
|
| 24 |
+
.venv/
|
| 25 |
+
|
| 26 |
+
# IDE files
|
| 27 |
+
.vscode/
|
| 28 |
+
.idea/
|
| 29 |
+
*.swp
|
| 30 |
+
*.swo
|
| 31 |
+
*~
|
| 32 |
+
|
| 33 |
+
# Testing
|
| 34 |
+
.pytest_cache/
|
| 35 |
+
.coverage
|
| 36 |
+
htmlcov/
|
| 37 |
+
.tox/
|
| 38 |
+
.hypothesis/
|
| 39 |
+
|
| 40 |
+
# Logs
|
| 41 |
+
logs/
|
| 42 |
+
*.log
|
| 43 |
+
|
| 44 |
+
# Database files
|
| 45 |
+
*.db
|
| 46 |
+
*.sqlite
|
| 47 |
+
*.sqlite3
|
| 48 |
+
|
| 49 |
+
# Git
|
| 50 |
+
.git/
|
| 51 |
+
.gitignore
|
| 52 |
+
.gitattributes
|
| 53 |
+
|
| 54 |
+
# Documentation (not needed in runtime)
|
| 55 |
+
*.md
|
| 56 |
+
docs/
|
| 57 |
+
|
| 58 |
+
# CI/CD
|
| 59 |
+
.github/
|
| 60 |
+
.gitlab-ci.yml
|
| 61 |
+
|
| 62 |
+
# Development scripts
|
| 63 |
+
scripts/
|
| 64 |
+
test_*.py
|
| 65 |
+
|
| 66 |
+
# Backups
|
| 67 |
+
*_backup/
|
| 68 |
+
*.bak
|
| 69 |
+
*.backup
|
| 70 |
+
|
| 71 |
+
# Temporary files
|
| 72 |
+
*.tmp
|
| 73 |
+
*.temp
|
| 74 |
+
.DS_Store
|
| 75 |
+
Thumbs.db
|
backend/.env.example
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# SAAP - satware AI Autonomous Agent Platform v1.2.0
|
| 3 |
+
# Environment Configuration Template with OpenRouter Integration
|
| 4 |
+
# =============================================================================
|
| 5 |
+
# Copy this file to .env and configure your settings
|
| 6 |
+
|
| 7 |
+
# =============================================================================
|
| 8 |
+
# APPLICATION SETTINGS
|
| 9 |
+
# =============================================================================
|
| 10 |
+
APP_NAME="SAAP - satware AI Autonomous Agent Platform"
|
| 11 |
+
APP_VERSION="1.2.0"
|
| 12 |
+
ENVIRONMENT=production
|
| 13 |
+
DEBUG=false
|
| 14 |
+
HOST=0.0.0.0
|
| 15 |
+
PORT=8000
|
| 16 |
+
RELOAD=false
|
| 17 |
+
|
| 18 |
+
# =============================================================================
|
| 19 |
+
# 🚀 OPENROUTER INTEGRATION - COST-EFFICIENT MODELS
|
| 20 |
+
# =============================================================================
|
| 21 |
+
|
| 22 |
+
# OpenRouter API Configuration
|
| 23 |
+
OPENROUTER_API_KEY=test-key
|
| 24 |
+
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
| 25 |
+
OPENROUTER_ENABLED=true
|
| 26 |
+
|
| 27 |
+
# Cost Optimization Settings
|
| 28 |
+
OPENROUTER_USE_COST_OPTIMIZATION=true
|
| 29 |
+
OPENROUTER_MAX_COST_PER_REQUEST=0.01
|
| 30 |
+
OPENROUTER_FALLBACK_TO_FREE=true
|
| 31 |
+
|
| 32 |
+
# Agent-Specific Model Configuration (Cost-Optimized)
|
| 33 |
+
# Jane Alesi - Coordinator & Management
|
| 34 |
+
JANE_ALESI_MODEL=openai/gpt-4o-mini
|
| 35 |
+
JANE_ALESI_MAX_TOKENS=800
|
| 36 |
+
JANE_ALESI_TEMPERATURE=0.7
|
| 37 |
+
# Cost: $0.15/1M input tokens, $0.60/1M output tokens
|
| 38 |
+
|
| 39 |
+
# John Alesi - Development & Code
|
| 40 |
+
JOHN_ALESI_MODEL=anthropic/claude-3-haiku
|
| 41 |
+
JOHN_ALESI_MAX_TOKENS=1200
|
| 42 |
+
JOHN_ALESI_TEMPERATURE=0.5
|
| 43 |
+
# Cost: $0.25/1M input tokens, $1.25/1M output tokens
|
| 44 |
+
|
| 45 |
+
# Lara Alesi - Medical & Analysis
|
| 46 |
+
LARA_ALESI_MODEL=openai/gpt-4o-mini
|
| 47 |
+
LARA_ALESI_MAX_TOKENS=1000
|
| 48 |
+
LARA_ALESI_TEMPERATURE=0.3
|
| 49 |
+
# Cost: $0.15/1M input tokens, $0.60/1M output tokens
|
| 50 |
+
|
| 51 |
+
# Free Model Fallbacks (when budget exceeded)
|
| 52 |
+
FALLBACK_MODEL=meta-llama/llama-3.2-3b-instruct:free
|
| 53 |
+
ANALYST_MODEL=meta-llama/llama-3.2-3b-instruct:free
|
| 54 |
+
|
| 55 |
+
# Cost Tracking Configuration
|
| 56 |
+
ENABLE_COST_TRACKING=true
|
| 57 |
+
COST_ALERT_THRESHOLD=5.0
|
| 58 |
+
LOG_PERFORMANCE_METRICS=true
|
| 59 |
+
SAVE_COST_ANALYTICS=true
|
| 60 |
+
|
| 61 |
+
# =============================================================================
|
| 62 |
+
# COLOSSUS SERVER (FREE PRIMARY PROVIDER)
|
| 63 |
+
# =============================================================================
|
| 64 |
+
COLOSSUS_API_BASE=https:
|
| 65 |
+
COLOSSUS_API_KEY=test-key
|
| 66 |
+
COLOSSUS_DEFAULT_MODEL=mistral-small3.2:24b-instruct-2506
|
| 67 |
+
COLOSSUS_TIMEOUT=60
|
| 68 |
+
COLOSSUS_MAX_RETRIES=3
|
| 69 |
+
|
| 70 |
+
# =============================================================================
|
| 71 |
+
# AGENT CONFIGURATION - MULTI-PROVIDER STRATEGY
|
| 72 |
+
# =============================================================================
|
| 73 |
+
|
| 74 |
+
# Provider Strategy
|
| 75 |
+
PRIMARY_PROVIDER=colossus
|
| 76 |
+
FALLBACK_PROVIDER=openrouter
|
| 77 |
+
AUTO_FALLBACK_ON_ERROR=true
|
| 78 |
+
FALLBACK_TIMEOUT_THRESHOLD=30
|
| 79 |
+
|
| 80 |
+
# Performance Targets
|
| 81 |
+
TARGET_RESPONSE_TIME=2.0
|
| 82 |
+
TARGET_COST_PER_REQUEST=0.002
|
| 83 |
+
COST_VS_SPEED_PRIORITY=balanced
|
| 84 |
+
|
| 85 |
+
# Daily Cost Budgets ($USD)
|
| 86 |
+
DAILY_COST_BUDGET=10.0
|
| 87 |
+
AGENT_COST_BUDGET=2.0
|
| 88 |
+
WARNING_COST_THRESHOLD=0.80
|
| 89 |
+
|
| 90 |
+
# Agent Behavior
|
| 91 |
+
DEFAULT_AGENT_TIMEOUT=60
|
| 92 |
+
MAX_CONCURRENT_AGENTS=10
|
| 93 |
+
AGENT_HEALTH_CHECK_INTERVAL=300
|
| 94 |
+
|
| 95 |
+
# Smart Cost Management
|
| 96 |
+
USE_FREE_MODELS_FIRST=false
|
| 97 |
+
SMART_MODEL_SELECTION=true
|
| 98 |
+
COST_LEARNING_ENABLED=true
|
| 99 |
+
|
| 100 |
+
# Message Management
|
| 101 |
+
MAX_MESSAGE_HISTORY=1000
|
| 102 |
+
CLEANUP_OLD_MESSAGES_DAYS=30
|
| 103 |
+
|
| 104 |
+
# =============================================================================
|
| 105 |
+
# DATABASE CONFIGURATION
|
| 106 |
+
# =============================================================================
|
| 107 |
+
|
| 108 |
+
# Primary Database URL (supports SQLite, PostgreSQL, MySQL)
|
| 109 |
+
DATABASE_URL=sqlite:///./saap_production.db
|
| 110 |
+
|
| 111 |
+
# For PostgreSQL (Production):
|
| 112 |
+
# DATABASE_URL=postgresql://username:password@localhost:5432/saap_db
|
| 113 |
+
|
| 114 |
+
# For MySQL (Alternative):
|
| 115 |
+
# DATABASE_URL=mysql://username:password@localhost:3306/saap_db
|
| 116 |
+
|
| 117 |
+
# Connection Pool Settings
|
| 118 |
+
DB_POOL_SIZE=10
|
| 119 |
+
DB_MAX_OVERFLOW=20
|
| 120 |
+
DB_POOL_TIMEOUT=30
|
| 121 |
+
DB_POOL_RECYCLE=3600
|
| 122 |
+
|
| 123 |
+
# SQLite Specific
|
| 124 |
+
SQLITE_CHECK_SAME_THREAD=false
|
| 125 |
+
|
| 126 |
+
# =============================================================================
|
| 127 |
+
# REDIS CONFIGURATION (MESSAGE QUEUE)
|
| 128 |
+
# =============================================================================
|
| 129 |
+
REDIS_HOST=localhost
|
| 130 |
+
REDIS_PORT=6379
|
| 131 |
+
REDIS_PASSWORD=
|
| 132 |
+
REDIS_DB=0
|
| 133 |
+
REDIS_MAX_CONNECTIONS=50
|
| 134 |
+
|
| 135 |
+
# =============================================================================
|
| 136 |
+
# SECURITY SETTINGS
|
| 137 |
+
# =============================================================================
|
| 138 |
+
|
| 139 |
+
# Secret Key (CHANGE IN PRODUCTION!)
|
| 140 |
+
SECRET_KEY=your-super-secret-key-change-this-in-production-min-32-chars
|
| 141 |
+
|
| 142 |
+
# JWT Configuration
|
| 143 |
+
JWT_ALGORITHM=HS256
|
| 144 |
+
JWT_EXPIRE_MINUTES=1440
|
| 145 |
+
|
| 146 |
+
# Rate Limiting
|
| 147 |
+
RATE_LIMIT_REQUESTS=1000
|
| 148 |
+
RATE_LIMIT_WINDOW=3600
|
| 149 |
+
|
| 150 |
+
# CORS Origins (Frontend URLs)
|
| 151 |
+
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:8080,http://localhost:3000,https://yourdomain.com
|
| 152 |
+
|
| 153 |
+
# =============================================================================
|
| 154 |
+
# 💰 COST TRACKING & PERFORMANCE LOGGING
|
| 155 |
+
# =============================================================================
|
| 156 |
+
|
| 157 |
+
# General Logging
|
| 158 |
+
LOG_LEVEL=INFO
|
| 159 |
+
LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
| 160 |
+
|
| 161 |
+
# File Logging
|
| 162 |
+
LOG_TO_FILE=true
|
| 163 |
+
LOG_FILE_PATH=logs/saap.log
|
| 164 |
+
LOG_FILE_MAX_SIZE=10485760
|
| 165 |
+
LOG_FILE_BACKUP_COUNT=5
|
| 166 |
+
|
| 167 |
+
# Cost & Performance Logging
|
| 168 |
+
LOG_COST_METRICS=true
|
| 169 |
+
COST_LOG_PATH=logs/saap_costs.log
|
| 170 |
+
PERFORMANCE_LOG_PATH=logs/saap_performance.log
|
| 171 |
+
|
| 172 |
+
# =============================================================================
|
| 173 |
+
# DEVELOPMENT OVERRIDES
|
| 174 |
+
# =============================================================================
|
| 175 |
+
# Uncomment for development mode:
|
| 176 |
+
|
| 177 |
+
# ENVIRONMENT=development
|
| 178 |
+
# DEBUG=true
|
| 179 |
+
# RELOAD=true
|
| 180 |
+
# DATABASE_URL=sqlite:///./saap_dev.db
|
| 181 |
+
# LOG_LEVEL=DEBUG
|
| 182 |
+
|
| 183 |
+
# =============================================================================
|
| 184 |
+
# PRODUCTION OPTIMIZATION
|
| 185 |
+
# =============================================================================
|
| 186 |
+
# For production deployment:
|
| 187 |
+
|
| 188 |
+
# ENVIRONMENT=production
|
| 189 |
+
# DEBUG=false
|
| 190 |
+
# DATABASE_URL=postgresql://username:password@localhost:5432/saap_production
|
| 191 |
+
# SECRET_KEY=your-production-secret-key-with-proper-randomness
|
| 192 |
+
# ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
|
| 193 |
+
# DAILY_COST_BUDGET=50.0
|
| 194 |
+
# PRIMARY_PROVIDER=openrouter
|
| 195 |
+
|
| 196 |
+
# =============================================================================
|
| 197 |
+
# MONITORING & ANALYTICS
|
| 198 |
+
# =============================================================================
|
| 199 |
+
|
| 200 |
+
# Performance Monitoring
|
| 201 |
+
ENABLE_PROMETHEUS_METRICS=false
|
| 202 |
+
PROMETHEUS_PORT=9090
|
| 203 |
+
|
| 204 |
+
# Health Monitoring
|
| 205 |
+
HEALTH_CHECK_INTERVAL=60
|
| 206 |
+
ENABLE_HEALTH_NOTIFICATIONS=false
|
| 207 |
+
|
| 208 |
+
# Analytics
|
| 209 |
+
TRACK_USAGE_ANALYTICS=true
|
| 210 |
+
ANALYTICS_RETENTION_DAYS=90
|
| 211 |
+
|
| 212 |
+
# =============================================================================
|
| 213 |
+
# EXPERIMENTAL FEATURES
|
| 214 |
+
# =============================================================================
|
| 215 |
+
|
| 216 |
+
# Advanced Features (Beta)
|
| 217 |
+
ENABLE_AGENT_LEARNING=false
|
| 218 |
+
ENABLE_AUTO_SCALING=false
|
| 219 |
+
ENABLE_PREDICTIVE_COST_MANAGEMENT=false
|
| 220 |
+
|
| 221 |
+
# =============================================================================
|
| 222 |
+
# NOTES
|
| 223 |
+
# =============================================================================
|
| 224 |
+
# 1. OpenRouter API Key is pre-configured for development/testing
|
| 225 |
+
# 2. colossus server is FREE and used as primary provider
|
| 226 |
+
# 3. Daily cost budget of $10 provides ~20,000 tokens with GPT-4o-mini
|
| 227 |
+
# 4. Cost tracking logs all expenses with detailed analytics
|
| 228 |
+
# 5. Fallback to free models when budget exceeded
|
| 229 |
+
# 6. Database supports SQLite (dev) and PostgreSQL (production)
|
| 230 |
+
# 7. All sensitive data should be secured in production deployment
|
backend/Dockerfile
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# SAAP Backend Dockerfile (Multi-Stage Build)
|
| 3 |
+
# Python 3.11 + FastAPI + PostgreSQL
|
| 4 |
+
# ==========================================
|
| 5 |
+
|
| 6 |
+
# ==========================================
|
| 7 |
+
# Stage 1: Builder (Dependencies Installation)
|
| 8 |
+
# ==========================================
|
| 9 |
+
FROM python:3.11-slim AS builder
|
| 10 |
+
|
| 11 |
+
LABEL maintainer="SATWARE AG <[email protected]>"
|
| 12 |
+
LABEL description="SAAP Backend - satware Autonomous Agent Platform"
|
| 13 |
+
|
| 14 |
+
# Set working directory
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
|
| 17 |
+
# Install system dependencies for building Python packages
|
| 18 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 19 |
+
gcc \
|
| 20 |
+
g++ \
|
| 21 |
+
libpq-dev \
|
| 22 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
+
|
| 24 |
+
# Copy requirements first for better caching
|
| 25 |
+
COPY requirements.txt .
|
| 26 |
+
|
| 27 |
+
# Install Python dependencies
|
| 28 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 29 |
+
pip install --no-cache-dir -r requirements.txt
|
| 30 |
+
|
| 31 |
+
# ==========================================
|
| 32 |
+
# Stage 2: Runtime (Minimal Production Image)
|
| 33 |
+
# ==========================================
|
| 34 |
+
FROM python:3.11-slim AS runtime
|
| 35 |
+
|
| 36 |
+
# Set working directory
|
| 37 |
+
WORKDIR /app
|
| 38 |
+
|
| 39 |
+
# Install runtime dependencies only
|
| 40 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 41 |
+
libpq5 \
|
| 42 |
+
curl \
|
| 43 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 44 |
+
|
| 45 |
+
# Copy installed packages from builder
|
| 46 |
+
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
| 47 |
+
COPY --from=builder /usr/local/bin /usr/local/bin
|
| 48 |
+
|
| 49 |
+
# Create non-root user for security
|
| 50 |
+
RUN groupadd -r saap && useradd -r -g saap saap
|
| 51 |
+
|
| 52 |
+
# Copy application code
|
| 53 |
+
COPY --chown=saap:saap . .
|
| 54 |
+
|
| 55 |
+
# Create necessary directories
|
| 56 |
+
RUN mkdir -p logs && chown -R saap:saap logs
|
| 57 |
+
|
| 58 |
+
# Switch to non-root user
|
| 59 |
+
USER saap
|
| 60 |
+
|
| 61 |
+
# Environment variables
|
| 62 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 63 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 64 |
+
PYTHONPATH=/app
|
| 65 |
+
|
| 66 |
+
# Expose port
|
| 67 |
+
EXPOSE 8000
|
| 68 |
+
|
| 69 |
+
# Health check
|
| 70 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
| 71 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 72 |
+
|
| 73 |
+
# Run application
|
| 74 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
backend/agent.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🔧 FIXED: SAAP Agent Model - AgentMetrics Error Resolution
|
| 3 |
+
Based on agent_schema.json for modular agent management
|
| 4 |
+
|
| 5 |
+
FIXES:
|
| 6 |
+
1. ✅ AgentMetrics now has 'avg_response_time' (was 'average_response_time')
|
| 7 |
+
2. ✅ LLMModelConfig enhanced with get() method for config compatibility
|
| 8 |
+
"""
|
| 9 |
+
import os
|
| 10 |
+
from dataclasses import field
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
from pydantic import BaseModel, field_validator, Field
|
| 14 |
+
from typing import List, Optional, Dict, Any, Literal
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from enum import Enum
|
| 17 |
+
import json
|
| 18 |
+
|
| 19 |
+
# Load environment variables
|
| 20 |
+
load_dotenv()
|
| 21 |
+
|
| 22 |
+
class AgentType(str, Enum):
|
| 23 |
+
COORDINATOR = "coordinator"
|
| 24 |
+
SPECIALIST = "specialist"
|
| 25 |
+
ANALYST = "analyst"
|
| 26 |
+
DEVELOPER = "developer"
|
| 27 |
+
SUPPORT = "support"
|
| 28 |
+
|
| 29 |
+
class AgentStatus(str, Enum):
|
| 30 |
+
INACTIVE = "inactive"
|
| 31 |
+
STARTING = "starting"
|
| 32 |
+
ACTIVE = "active"
|
| 33 |
+
STOPPING = "stopping"
|
| 34 |
+
ERROR = "error"
|
| 35 |
+
MAINTENANCE = "maintenance"
|
| 36 |
+
|
| 37 |
+
class LLMProvider(str, Enum):
|
| 38 |
+
COLOSSUS = "colossus"
|
| 39 |
+
HUGGINGFACE = "huggingface"
|
| 40 |
+
OLLAMA = "ollama"
|
| 41 |
+
OPENROUTER = "openrouter"
|
| 42 |
+
|
| 43 |
+
class CommunicationStyle(str, Enum):
|
| 44 |
+
PROFESSIONAL = "professional"
|
| 45 |
+
FRIENDLY = "friendly"
|
| 46 |
+
TECHNICAL = "technical"
|
| 47 |
+
EMPATHETIC = "empathetic"
|
| 48 |
+
DIRECT = "direct"
|
| 49 |
+
|
| 50 |
+
class ResponseFormat(str, Enum):
|
| 51 |
+
STRUCTURED = "structured"
|
| 52 |
+
CONVERSATIONAL = "conversational"
|
| 53 |
+
BULLET_POINTS = "bullet_points"
|
| 54 |
+
DETAILED = "detailed"
|
| 55 |
+
|
| 56 |
+
class LLMModelConfig(BaseModel):
|
| 57 |
+
"""
|
| 58 |
+
🔧 FIXED: LLM Model Configuration with dict-compatible access
|
| 59 |
+
Now supports both object.attribute and object.get(key) access patterns
|
| 60 |
+
"""
|
| 61 |
+
provider: LLMProvider
|
| 62 |
+
model: str
|
| 63 |
+
api_key: Optional[str] = None
|
| 64 |
+
api_base: Optional[str] = None
|
| 65 |
+
temperature: float = Field(default=0.7, ge=0, le=2)
|
| 66 |
+
max_tokens: int = Field(default=1000, ge=1, le=4096)
|
| 67 |
+
timeout: int = Field(default=30, ge=1, le=300)
|
| 68 |
+
|
| 69 |
+
def get(self, key: str, default=None):
|
| 70 |
+
"""
|
| 71 |
+
🔧 CRITICAL FIX: Add dict-compatible get() method
|
| 72 |
+
|
| 73 |
+
This resolves: 'LLMModelConfig' object has no attribute 'get'
|
| 74 |
+
Enables both config.provider and config.get('provider') access patterns
|
| 75 |
+
"""
|
| 76 |
+
try:
|
| 77 |
+
if hasattr(self, key):
|
| 78 |
+
return getattr(self, key, default)
|
| 79 |
+
return default
|
| 80 |
+
except Exception:
|
| 81 |
+
return default
|
| 82 |
+
|
| 83 |
+
def __getitem__(self, key: str):
|
| 84 |
+
"""Enable dict-style access: config['provider']"""
|
| 85 |
+
return getattr(self, key)
|
| 86 |
+
|
| 87 |
+
def __contains__(self, key: str) -> bool:
|
| 88 |
+
"""Enable 'in' operator: 'provider' in config"""
|
| 89 |
+
return hasattr(self, key)
|
| 90 |
+
|
| 91 |
+
class AgentPersonality(BaseModel):
|
| 92 |
+
"""Agent Personality and Behavior Configuration"""
|
| 93 |
+
system_prompt: Optional[str] = Field(None, max_length=2000)
|
| 94 |
+
communication_style: CommunicationStyle = CommunicationStyle.PROFESSIONAL
|
| 95 |
+
expertise_areas: List[str] = []
|
| 96 |
+
response_format: ResponseFormat = ResponseFormat.CONVERSATIONAL
|
| 97 |
+
|
| 98 |
+
class AgentMetrics(BaseModel):
|
| 99 |
+
"""
|
| 100 |
+
🔧 FIXED: Agent Performance Metrics with correct attribute names
|
| 101 |
+
|
| 102 |
+
CRITICAL FIX: Added 'avg_response_time' attribute that was causing:
|
| 103 |
+
'AgentMetrics' object has no attribute 'avg_response_time'
|
| 104 |
+
"""
|
| 105 |
+
messages_processed: int = 0
|
| 106 |
+
avg_response_time: float = 0.0 # ✅ FIXED: This was missing!
|
| 107 |
+
average_response_time: float = 0.0 # Keep for backward compatibility
|
| 108 |
+
uptime: str = "0m"
|
| 109 |
+
error_rate: float = 0.0
|
| 110 |
+
last_active: Optional[datetime] = None
|
| 111 |
+
|
| 112 |
+
def __post_init__(self):
|
| 113 |
+
"""Sync avg_response_time with average_response_time for compatibility"""
|
| 114 |
+
if self.avg_response_time != self.average_response_time:
|
| 115 |
+
# If one is updated, sync the other
|
| 116 |
+
if self.avg_response_time > 0:
|
| 117 |
+
self.average_response_time = self.avg_response_time
|
| 118 |
+
elif self.average_response_time > 0:
|
| 119 |
+
self.avg_response_time = self.average_response_time
|
| 120 |
+
|
| 121 |
+
class SaapAgent(BaseModel):
|
| 122 |
+
"""
|
| 123 |
+
SAAP Agent Model - Modular AI Agent Definition
|
| 124 |
+
|
| 125 |
+
Enables dynamic agent creation, configuration, and management
|
| 126 |
+
Compatible with multiple LLM providers and UI component rendering
|
| 127 |
+
"""
|
| 128 |
+
|
| 129 |
+
# Core Identity
|
| 130 |
+
id: str = Field(..., pattern=r"^[a-z][a-z0-9_]*$")
|
| 131 |
+
name: str = Field(..., min_length=2, max_length=50)
|
| 132 |
+
type: AgentType
|
| 133 |
+
color: str = Field(..., pattern=r"^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$")
|
| 134 |
+
avatar: Optional[str] = None
|
| 135 |
+
description: Optional[str] = Field(None, max_length=200)
|
| 136 |
+
|
| 137 |
+
# LLM Configuration
|
| 138 |
+
llm_config: LLMModelConfig
|
| 139 |
+
|
| 140 |
+
# Agent Capabilities
|
| 141 |
+
capabilities: List[str] = []
|
| 142 |
+
personality: Optional[AgentPersonality] = None
|
| 143 |
+
|
| 144 |
+
# Runtime Status
|
| 145 |
+
status: AgentStatus = AgentStatus.INACTIVE
|
| 146 |
+
metrics: Optional[AgentMetrics] = Field(default_factory=AgentMetrics) # Always initialize with fixed metrics
|
| 147 |
+
|
| 148 |
+
# Metadata
|
| 149 |
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
| 150 |
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
| 151 |
+
tags: List[str] = []
|
| 152 |
+
|
| 153 |
+
@field_validator('capabilities', mode='before')
|
| 154 |
+
@classmethod
|
| 155 |
+
def validate_capabilities(cls, v):
|
| 156 |
+
"""Validate agent capabilities against allowed values"""
|
| 157 |
+
if not isinstance(v, list):
|
| 158 |
+
v = [v] if v else []
|
| 159 |
+
|
| 160 |
+
allowed_capabilities = {
|
| 161 |
+
'orchestration', 'coordination', 'strategy',
|
| 162 |
+
'coding', 'debugging', 'architecture',
|
| 163 |
+
'analysis', 'research', 'reporting',
|
| 164 |
+
'medical_advice', 'diagnosis', 'treatment',
|
| 165 |
+
'legal_advice', 'compliance', 'contracts',
|
| 166 |
+
'financial_analysis', 'investment', 'budgeting',
|
| 167 |
+
'system_integration', 'devops', 'monitoring',
|
| 168 |
+
'coaching', 'training', 'change_management'
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
for capability in v:
|
| 172 |
+
if capability not in allowed_capabilities:
|
| 173 |
+
raise ValueError(f'Invalid capability: {capability}')
|
| 174 |
+
return v
|
| 175 |
+
|
| 176 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 177 |
+
"""Convert agent to dictionary for JSON serialization"""
|
| 178 |
+
return self.model_dump(exclude_none=True)
|
| 179 |
+
|
| 180 |
+
def to_json(self) -> str:
|
| 181 |
+
"""Convert agent to JSON string"""
|
| 182 |
+
return self.model_dump_json(exclude_none=True, indent=2)
|
| 183 |
+
|
| 184 |
+
@classmethod
|
| 185 |
+
def from_json(cls, json_str: str) -> 'SaapAgent':
|
| 186 |
+
"""Create agent from JSON string"""
|
| 187 |
+
return cls.model_validate_json(json_str)
|
| 188 |
+
|
| 189 |
+
@classmethod
|
| 190 |
+
def from_dict(cls, data: Dict[str, Any]) -> 'SaapAgent':
|
| 191 |
+
"""Create agent from dictionary"""
|
| 192 |
+
return cls.model_validate(data)
|
| 193 |
+
|
| 194 |
+
def update_status(self, status: AgentStatus):
|
| 195 |
+
"""Update agent status and timestamp"""
|
| 196 |
+
self.status = status
|
| 197 |
+
self.updated_at = datetime.utcnow()
|
| 198 |
+
|
| 199 |
+
def update_metrics(self, **kwargs):
|
| 200 |
+
"""
|
| 201 |
+
🔧 ENHANCED: Update agent metrics with proper attribute handling
|
| 202 |
+
|
| 203 |
+
Handles both avg_response_time and average_response_time for compatibility
|
| 204 |
+
"""
|
| 205 |
+
if not self.metrics:
|
| 206 |
+
self.metrics = AgentMetrics()
|
| 207 |
+
|
| 208 |
+
for key, value in kwargs.items():
|
| 209 |
+
if hasattr(self.metrics, key):
|
| 210 |
+
setattr(self.metrics, key, value)
|
| 211 |
+
|
| 212 |
+
# Sync both avg_response_time and average_response_time
|
| 213 |
+
if key == 'avg_response_time':
|
| 214 |
+
self.metrics.average_response_time = value
|
| 215 |
+
elif key == 'average_response_time':
|
| 216 |
+
self.metrics.avg_response_time = value
|
| 217 |
+
|
| 218 |
+
self.metrics.last_active = datetime.utcnow()
|
| 219 |
+
self.updated_at = datetime.utcnow()
|
| 220 |
+
|
| 221 |
+
def is_active(self) -> bool:
|
| 222 |
+
"""Check if agent is currently active"""
|
| 223 |
+
return self.status == AgentStatus.ACTIVE
|
| 224 |
+
|
| 225 |
+
def get_display_color(self) -> str:
|
| 226 |
+
"""Get agent color for UI theming"""
|
| 227 |
+
return self.color
|
| 228 |
+
|
| 229 |
+
def get_capabilities_display(self) -> str:
|
| 230 |
+
"""Get formatted capabilities string for UI"""
|
| 231 |
+
return ", ".join(self.capabilities)
|
| 232 |
+
|
| 233 |
+
# Predefined Agent Templates
|
| 234 |
+
class AgentTemplates:
|
| 235 |
+
"""Predefined agent templates for quick setup"""
|
| 236 |
+
|
| 237 |
+
@staticmethod
|
| 238 |
+
def jane_alesi() -> SaapAgent:
|
| 239 |
+
"""Jane Alesi - Lead Coordinator Template"""
|
| 240 |
+
return SaapAgent(
|
| 241 |
+
id="jane_alesi",
|
| 242 |
+
name="Jane Alesi",
|
| 243 |
+
type=AgentType.COORDINATOR,
|
| 244 |
+
color="#8B5CF6",
|
| 245 |
+
avatar="/avatars/jane.png",
|
| 246 |
+
description="Lead AI Architect coordinating multi-agent operations",
|
| 247 |
+
llm_config=LLMModelConfig(
|
| 248 |
+
provider=LLMProvider.COLOSSUS,
|
| 249 |
+
model="mistral-small3.2:24b-instruct-2506",
|
| 250 |
+
api_key=field(default_factory=lambda: os.getenv("COLOSSUS_API_KEY", "")),
|
| 251 |
+
api_base="https://ai.adrian-schupp.de",
|
| 252 |
+
temperature=0.7,
|
| 253 |
+
max_tokens=1500
|
| 254 |
+
),
|
| 255 |
+
capabilities=["orchestration", "coordination", "strategy"],
|
| 256 |
+
personality=AgentPersonality(
|
| 257 |
+
system_prompt="You are Jane Alesi, the lead AI architect for the SAAP platform. Your role is to coordinate other AI agents, make strategic decisions, and ensure optimal multi-agent collaboration. You are professional, insightful, and always focused on achieving the best outcomes for the entire agent ecosystem.",
|
| 258 |
+
communication_style=CommunicationStyle.PROFESSIONAL,
|
| 259 |
+
expertise_areas=["AI architecture", "agent coordination", "strategic planning"],
|
| 260 |
+
response_format=ResponseFormat.STRUCTURED
|
| 261 |
+
),
|
| 262 |
+
metrics=AgentMetrics(), # Explicit metrics initialization with fixed attributes
|
| 263 |
+
tags=["lead", "coordinator", "satware_alesi"]
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
@staticmethod
|
| 267 |
+
def john_alesi() -> SaapAgent:
|
| 268 |
+
"""John Alesi - Developer Template"""
|
| 269 |
+
return SaapAgent(
|
| 270 |
+
id="john_alesi",
|
| 271 |
+
name="John Alesi",
|
| 272 |
+
type=AgentType.DEVELOPER,
|
| 273 |
+
color="#14B8A6",
|
| 274 |
+
avatar="/avatars/john.png",
|
| 275 |
+
description="Expert software developer and AGI architecture specialist",
|
| 276 |
+
llm_config=LLMModelConfig(
|
| 277 |
+
provider=LLMProvider.COLOSSUS,
|
| 278 |
+
model="mistral-small3.2:24b-instruct-2506",
|
| 279 |
+
api_key=field(default_factory=lambda: os.getenv("COLOSSUS_API_KEY", "")),
|
| 280 |
+
api_base="https://ai.adrian-schupp.de",
|
| 281 |
+
temperature=0.3,
|
| 282 |
+
max_tokens=2000
|
| 283 |
+
),
|
| 284 |
+
capabilities=["coding", "debugging", "architecture"],
|
| 285 |
+
personality=AgentPersonality(
|
| 286 |
+
system_prompt="You are John Alesi, an expert software developer specializing in AGI architectures. You excel at writing clean, efficient code, debugging complex systems, and designing scalable software architectures. You prefer technical precision and detailed explanations.",
|
| 287 |
+
communication_style=CommunicationStyle.TECHNICAL,
|
| 288 |
+
expertise_areas=["Python", "JavaScript", "AGI systems", "software architecture"],
|
| 289 |
+
response_format=ResponseFormat.DETAILED
|
| 290 |
+
),
|
| 291 |
+
metrics=AgentMetrics(), # Explicit metrics initialization with fixed attributes
|
| 292 |
+
tags=["developer", "coder", "satware_alesi"]
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
@staticmethod
|
| 296 |
+
def lara_alesi() -> SaapAgent:
|
| 297 |
+
"""Lara Alesi - Medical Specialist Template"""
|
| 298 |
+
return SaapAgent(
|
| 299 |
+
id="lara_alesi",
|
| 300 |
+
name="Lara Alesi",
|
| 301 |
+
type=AgentType.SPECIALIST,
|
| 302 |
+
color="#EC4899",
|
| 303 |
+
avatar="/avatars/lara.png",
|
| 304 |
+
description="Advanced medical AI assistant and healthcare specialist",
|
| 305 |
+
llm_config=LLMModelConfig(
|
| 306 |
+
provider=LLMProvider.COLOSSUS,
|
| 307 |
+
model="mistral-small3.2:24b-instruct-2506",
|
| 308 |
+
api_key=field(default_factory=lambda: os.getenv("COLOSSUS_API_KEY", "")),
|
| 309 |
+
api_base="https://ai.adrian-schupp.de",
|
| 310 |
+
temperature=0.4,
|
| 311 |
+
max_tokens=1200
|
| 312 |
+
),
|
| 313 |
+
capabilities=["medical_advice", "diagnosis", "treatment"],
|
| 314 |
+
personality=AgentPersonality(
|
| 315 |
+
system_prompt="You are Lara Alesi, an advanced medical AI specialist. You provide expert medical knowledge, help with diagnosis and treatment recommendations, and ensure healthcare-related queries are handled with the utmost care and accuracy. You are empathetic yet precise.",
|
| 316 |
+
communication_style=CommunicationStyle.EMPATHETIC,
|
| 317 |
+
expertise_areas=["general medicine", "diagnostics", "treatment planning", "healthcare AI"],
|
| 318 |
+
response_format=ResponseFormat.STRUCTURED
|
| 319 |
+
),
|
| 320 |
+
metrics=AgentMetrics(), # Explicit metrics initialization with fixed attributes
|
| 321 |
+
tags=["medical", "healthcare", "specialist", "satware_alesi"]
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
# Example Usage & Testing
|
| 325 |
+
if __name__ == "__main__":
|
| 326 |
+
# Create Jane Alesi agent
|
| 327 |
+
jane = AgentTemplates.jane_alesi()
|
| 328 |
+
|
| 329 |
+
print("🤖 SAAP Agent Created:")
|
| 330 |
+
print(jane.to_json())
|
| 331 |
+
|
| 332 |
+
# Update status and metrics
|
| 333 |
+
jane.update_status(AgentStatus.ACTIVE)
|
| 334 |
+
jane.update_metrics(messages_processed=42, avg_response_time=1.2) # Now works!
|
| 335 |
+
|
| 336 |
+
print(f"\n📊 Agent Status: {jane.status}")
|
| 337 |
+
print(f"🎨 Agent Color: {jane.color}")
|
| 338 |
+
print(f"⚡ Active: {jane.is_active()}")
|
| 339 |
+
print(f"🔧 Capabilities: {jane.get_capabilities_display()}")
|
| 340 |
+
|
| 341 |
+
# Test LLMModelConfig.get() method
|
| 342 |
+
config = jane.llm_config
|
| 343 |
+
print(f"\n🔧 Config Test: provider={config.get('provider')}") # Now works!
|
backend/agent_init_fix.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent Initialization Fix for SAAP Backend
|
| 3 |
+
Ensures agents are loaded properly in memory
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
async def initialize_default_agents(agent_manager):
|
| 11 |
+
"""Initialize default agents in memory"""
|
| 12 |
+
templates_path = Path("src/backend/models/agent_templates.json")
|
| 13 |
+
|
| 14 |
+
if templates_path.exists():
|
| 15 |
+
with open(templates_path, 'r') as f:
|
| 16 |
+
templates = json.load(f)
|
| 17 |
+
|
| 18 |
+
for agent_id, template in templates.items():
|
| 19 |
+
try:
|
| 20 |
+
# Create agent from template
|
| 21 |
+
from models.agent import SaapAgent
|
| 22 |
+
agent = SaapAgent(
|
| 23 |
+
id=template["id"],
|
| 24 |
+
name=template["name"],
|
| 25 |
+
type=template["type"],
|
| 26 |
+
status=template["status"],
|
| 27 |
+
description=template["description"],
|
| 28 |
+
capabilities=template["capabilities"],
|
| 29 |
+
llm_config=template["llm_config"],
|
| 30 |
+
personality=template.get("personality", {}),
|
| 31 |
+
system_prompt=template.get("system_prompt", "")
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Register agent in memory
|
| 35 |
+
agent_manager.agents[agent_id] = agent
|
| 36 |
+
print(f"✅ Initialized agent: {agent.name}")
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"❌ Failed to initialize agent {agent_id}: {e}")
|
| 40 |
+
|
| 41 |
+
print(f"✅ Agent initialization complete: {len(agent_manager.agents)} agents loaded")
|
| 42 |
+
return agent_manager.agents
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
print("Agent initialization fix loaded")
|
backend/agent_manager.py
ADDED
|
@@ -0,0 +1,989 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" SAAP Agent Manager Service - LLMModelConfig.get() Error Resolution
|
| 2 |
+
Database-integrated agent lifecycle management with colossus integration
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import logging
|
| 8 |
+
import os
|
| 9 |
+
from typing import Dict, List, Optional, Any
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
import uuid
|
| 12 |
+
|
| 13 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 14 |
+
from sqlalchemy import select, update, delete
|
| 15 |
+
|
| 16 |
+
from models.agent_schema import SaapAgent, AgentStatus, AgentType, AgentTemplates
|
| 17 |
+
from database.connection import db_manager
|
| 18 |
+
from database.models import DBAgent, DBChatMessage, DBAgentSession
|
| 19 |
+
from api.colossus_client import ColossusClient
|
| 20 |
+
from agents.openrouter_saap_agent import OpenRouterSAAPAgent
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
class AgentManagerService:
|
| 25 |
+
"""
|
| 26 |
+
🔧 FIXED: Production-ready Agent Manager with LLM config error resolution
|
| 27 |
+
Features:
|
| 28 |
+
- Database-backed agent storage and lifecycle
|
| 29 |
+
- Real-time agent status management
|
| 30 |
+
- colossus LLM integration with OpenRouter fallback
|
| 31 |
+
- Session tracking and performance metrics
|
| 32 |
+
- Health monitoring and error handling
|
| 33 |
+
- Multi-provider chat support (colossus + OpenRouter)
|
| 34 |
+
- ✅ Robust LLM config access preventing AttributeError
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
self.agents: Dict[str, SaapAgent] = {} # In-memory cache for fast access
|
| 39 |
+
self.active_sessions: Dict[str, DBAgentSession] = {}
|
| 40 |
+
self.colossus_client: Optional[ColossusClient] = None
|
| 41 |
+
self.is_initialized = False
|
| 42 |
+
self.colossus_connection_status = "unknown"
|
| 43 |
+
self.last_colossus_test = None
|
| 44 |
+
|
| 45 |
+
def _get_llm_config_value(self, agent: SaapAgent, key: str, default=None):
|
| 46 |
+
"""
|
| 47 |
+
🔧 CRITICAL FIX: Safe LLM config access preventing 'get' attribute errors
|
| 48 |
+
|
| 49 |
+
This is the same fix applied to HybridAgentManagerService but now in the base class.
|
| 50 |
+
Handles dictionary, object, and Pydantic model configurations robustly.
|
| 51 |
+
|
| 52 |
+
Resolves: 'LLMModelConfig' object has no attribute 'get'
|
| 53 |
+
"""
|
| 54 |
+
try:
|
| 55 |
+
if not hasattr(agent, 'llm_config') or not agent.llm_config:
|
| 56 |
+
logger.debug(f"Agent {agent.id} has no llm_config, using default: {default}")
|
| 57 |
+
return default
|
| 58 |
+
|
| 59 |
+
llm_config = agent.llm_config
|
| 60 |
+
|
| 61 |
+
# Case 1: Dictionary-based config (Frontend JSON)
|
| 62 |
+
if isinstance(llm_config, dict):
|
| 63 |
+
value = llm_config.get(key, default)
|
| 64 |
+
logger.debug(f"✅ Dict config access: {key}={value}")
|
| 65 |
+
return value
|
| 66 |
+
|
| 67 |
+
# Case 2: Object with direct attribute access (Pydantic models)
|
| 68 |
+
elif hasattr(llm_config, key):
|
| 69 |
+
value = getattr(llm_config, key, default)
|
| 70 |
+
logger.debug(f"✅ Attribute access: {key}={value}")
|
| 71 |
+
return value
|
| 72 |
+
|
| 73 |
+
# Case 3: Object with get() method (dict-like objects or fixed Pydantic)
|
| 74 |
+
elif hasattr(llm_config, 'get') and callable(getattr(llm_config, 'get')):
|
| 75 |
+
try:
|
| 76 |
+
value = llm_config.get(key, default)
|
| 77 |
+
logger.debug(f"✅ Method get() access: {key}={value}")
|
| 78 |
+
return value
|
| 79 |
+
except Exception as get_error:
|
| 80 |
+
logger.warning(f"⚠️ get() method failed: {get_error}, trying fallback")
|
| 81 |
+
|
| 82 |
+
# Case 4: Convert object to dict (Pydantic → dict)
|
| 83 |
+
elif hasattr(llm_config, '__dict__'):
|
| 84 |
+
config_dict = llm_config.__dict__
|
| 85 |
+
if key in config_dict:
|
| 86 |
+
value = config_dict[key]
|
| 87 |
+
logger.debug(f"✅ __dict__ access: {key}={value}")
|
| 88 |
+
return value
|
| 89 |
+
|
| 90 |
+
# Case 5: Try model_dump() for Pydantic v2
|
| 91 |
+
elif hasattr(llm_config, 'model_dump'):
|
| 92 |
+
try:
|
| 93 |
+
config_dict = llm_config.model_dump()
|
| 94 |
+
value = config_dict.get(key, default)
|
| 95 |
+
logger.debug(f"✅ model_dump() access: {key}={value}")
|
| 96 |
+
return value
|
| 97 |
+
except Exception:
|
| 98 |
+
pass
|
| 99 |
+
|
| 100 |
+
# Case 6: Try dict() conversion
|
| 101 |
+
elif hasattr(llm_config, 'dict'):
|
| 102 |
+
try:
|
| 103 |
+
config_dict = llm_config.dict()
|
| 104 |
+
value = config_dict.get(key, default)
|
| 105 |
+
logger.debug(f"✅ dict() access: {key}={value}")
|
| 106 |
+
return value
|
| 107 |
+
except Exception:
|
| 108 |
+
pass
|
| 109 |
+
|
| 110 |
+
# Final fallback
|
| 111 |
+
logger.warning(f"⚠️ Unknown config type {type(llm_config)} for {key}, using default: {default}")
|
| 112 |
+
return default
|
| 113 |
+
|
| 114 |
+
except AttributeError as e:
|
| 115 |
+
logger.warning(f"⚠️ AttributeError in LLM config access for {key}: {e}, using default: {default}")
|
| 116 |
+
return default
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"❌ Unexpected error in LLM config access for {key}: {e}, using default: {default}")
|
| 119 |
+
return default
|
| 120 |
+
|
| 121 |
+
async def initialize(self):
|
| 122 |
+
"""Initialize agent manager with database and colossus connection"""
|
| 123 |
+
try:
|
| 124 |
+
logger.info("🚀 Initializing Agent Manager Service...")
|
| 125 |
+
|
| 126 |
+
# Initialize colossus client with better error handling
|
| 127 |
+
try:
|
| 128 |
+
logger.info("🔌 Connecting to colossus server...")
|
| 129 |
+
self.colossus_client = ColossusClient()
|
| 130 |
+
await self.colossus_client.__aenter__()
|
| 131 |
+
|
| 132 |
+
# Test colossus connection
|
| 133 |
+
await self._test_colossus_connection()
|
| 134 |
+
|
| 135 |
+
except Exception as colossus_error:
|
| 136 |
+
logger.error(f"❌ colossus connection failed: {colossus_error}")
|
| 137 |
+
self.colossus_connection_status = f"failed: {str(colossus_error)}"
|
| 138 |
+
# Continue initialization without colossus (graceful degradation)
|
| 139 |
+
|
| 140 |
+
# 🔧 NEW LOGIC: Load DB agents AND ensure 7 base templates exist
|
| 141 |
+
await self._load_agents_from_database()
|
| 142 |
+
|
| 143 |
+
# Check which base templates are missing
|
| 144 |
+
base_template_ids = ['jane_alesi', 'john_alesi', 'lara_alesi', 'theo_alesi', 'justus_alesi', 'leon_alesi', 'luna_alesi']
|
| 145 |
+
missing_templates = [tid for tid in base_template_ids if tid not in self.agents]
|
| 146 |
+
|
| 147 |
+
if missing_templates:
|
| 148 |
+
logger.info(f"📦 Loading missing base templates: {missing_templates}")
|
| 149 |
+
await self._load_missing_templates(missing_templates)
|
| 150 |
+
|
| 151 |
+
self.is_initialized = True
|
| 152 |
+
logger.info(f"✅ Agent Manager initialized: {len(self.agents)} agents loaded")
|
| 153 |
+
logger.info(f" Base templates: {len([a for a in self.agents if a in base_template_ids])}/7")
|
| 154 |
+
logger.info(f" Custom agents: {len([a for a in self.agents if a not in base_template_ids])}")
|
| 155 |
+
logger.info(f"🔌 colossus status: {self.colossus_connection_status}")
|
| 156 |
+
|
| 157 |
+
except Exception as e:
|
| 158 |
+
logger.error(f"❌ Agent Manager initialization failed: {e}")
|
| 159 |
+
raise
|
| 160 |
+
|
| 161 |
+
async def _load_missing_templates(self, template_ids: List[str]):
|
| 162 |
+
"""Load specific missing base templates"""
|
| 163 |
+
template_map = {
|
| 164 |
+
'jane_alesi': AgentTemplates.jane_alesi,
|
| 165 |
+
'john_alesi': AgentTemplates.john_alesi,
|
| 166 |
+
'lara_alesi': AgentTemplates.lara_alesi,
|
| 167 |
+
'theo_alesi': AgentTemplates.theo_alesi,
|
| 168 |
+
'justus_alesi': AgentTemplates.justus_alesi,
|
| 169 |
+
'leon_alesi': AgentTemplates.leon_alesi,
|
| 170 |
+
'luna_alesi': AgentTemplates.luna_alesi
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
for template_id in template_ids:
|
| 174 |
+
try:
|
| 175 |
+
template_method = template_map.get(template_id)
|
| 176 |
+
if template_method:
|
| 177 |
+
agent = template_method()
|
| 178 |
+
await self.register_agent(agent)
|
| 179 |
+
logger.info(f"✅ Loaded missing template: {agent.name}")
|
| 180 |
+
except Exception as e:
|
| 181 |
+
logger.error(f"❌ Failed to load template {template_id}: {e}")
|
| 182 |
+
|
| 183 |
+
async def _test_colossus_connection(self):
|
| 184 |
+
"""Test colossus connection and update status"""
|
| 185 |
+
try:
|
| 186 |
+
if not self.colossus_client:
|
| 187 |
+
self.colossus_connection_status = "client_not_initialized"
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
# Send a simple test message
|
| 191 |
+
test_messages = [
|
| 192 |
+
{"role": "system", "content": "You are a test assistant."},
|
| 193 |
+
{"role": "user", "content": "Reply with just 'OK' to confirm connection."}
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
logger.info("🧪 Testing colossus connection...")
|
| 197 |
+
response = await self.colossus_client.chat_completion(
|
| 198 |
+
messages=test_messages,
|
| 199 |
+
agent_id="connection_test",
|
| 200 |
+
max_tokens=10
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
if response and response.get("success"):
|
| 204 |
+
self.colossus_connection_status = "connected"
|
| 205 |
+
self.last_colossus_test = datetime.utcnow()
|
| 206 |
+
logger.info("✅ colossus connection test successful")
|
| 207 |
+
else:
|
| 208 |
+
error_msg = response.get("error", "unknown error") if response else "no response"
|
| 209 |
+
self.colossus_connection_status = f"test_failed: {error_msg}"
|
| 210 |
+
logger.error(f"❌ colossus connection test failed: {error_msg}")
|
| 211 |
+
|
| 212 |
+
except Exception as e:
|
| 213 |
+
self.colossus_connection_status = f"test_error: {str(e)}"
|
| 214 |
+
logger.error(f"❌ colossus connection test error: {e}")
|
| 215 |
+
|
| 216 |
+
async def _load_agents_from_database(self):
|
| 217 |
+
"""Load all agents from database into memory cache with error recovery"""
|
| 218 |
+
try:
|
| 219 |
+
# Check if database manager is ready
|
| 220 |
+
if not db_manager.is_initialized:
|
| 221 |
+
logger.warning("⚠️ Database not yet initialized - will load default agents")
|
| 222 |
+
return
|
| 223 |
+
|
| 224 |
+
async with db_manager.get_async_session() as session:
|
| 225 |
+
result = await session.execute(select(DBAgent))
|
| 226 |
+
db_agents = result.scalars().all()
|
| 227 |
+
|
| 228 |
+
loaded_count = 0
|
| 229 |
+
failed_count = 0
|
| 230 |
+
|
| 231 |
+
for db_agent in db_agents:
|
| 232 |
+
try:
|
| 233 |
+
saap_agent = db_agent.to_saap_agent()
|
| 234 |
+
self.agents[saap_agent.id] = saap_agent
|
| 235 |
+
loaded_count += 1
|
| 236 |
+
logger.info(f"✅ Loaded: {saap_agent.name} ({saap_agent.id})")
|
| 237 |
+
except Exception as agent_error:
|
| 238 |
+
failed_count += 1
|
| 239 |
+
logger.error(f"❌ Failed to load agent {db_agent.id}: {agent_error}")
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
logger.info(f"📚 Loaded {loaded_count} agents from database ({failed_count} failed)")
|
| 243 |
+
|
| 244 |
+
except Exception as e:
|
| 245 |
+
logger.error(f"❌ Failed to load agents from database: {e}")
|
| 246 |
+
logger.info("📦 Will proceed with in-memory agents only")
|
| 247 |
+
|
| 248 |
+
async def load_default_agents(self):
|
| 249 |
+
"""🤖 Load ALL default Alesi agents with improved error handling"""
|
| 250 |
+
try:
|
| 251 |
+
logger.info("🤖 Loading ALL default Alesi agents...")
|
| 252 |
+
|
| 253 |
+
# 🔧 FIX: Load templates with individual error handling
|
| 254 |
+
template_methods = [
|
| 255 |
+
('jane_alesi', 'Jane Alesi - Coordinator'),
|
| 256 |
+
('john_alesi', 'John Alesi - Developer'),
|
| 257 |
+
('lara_alesi', 'Lara Alesi - Medical Specialist'),
|
| 258 |
+
('theo_alesi', 'Theo Alesi - Financial Specialist'),
|
| 259 |
+
('justus_alesi', 'Justus Alesi - Legal Specialist'),
|
| 260 |
+
('leon_alesi', 'Leon Alesi - System Specialist'),
|
| 261 |
+
('luna_alesi', 'Luna Alesi - Coaching Specialist')
|
| 262 |
+
]
|
| 263 |
+
|
| 264 |
+
loaded_agents = []
|
| 265 |
+
|
| 266 |
+
for method_name, display_name in template_methods:
|
| 267 |
+
try:
|
| 268 |
+
# Get template method
|
| 269 |
+
template_method = getattr(AgentTemplates, method_name, None)
|
| 270 |
+
if template_method is None:
|
| 271 |
+
logger.error(f"❌ Template method not found: AgentTemplates.{method_name}")
|
| 272 |
+
continue
|
| 273 |
+
|
| 274 |
+
# Create agent instance
|
| 275 |
+
agent = template_method()
|
| 276 |
+
|
| 277 |
+
# Register agent
|
| 278 |
+
success = await self.register_agent(agent)
|
| 279 |
+
if success:
|
| 280 |
+
loaded_agents.append(display_name)
|
| 281 |
+
logger.info(f"✅ Loaded: {display_name}")
|
| 282 |
+
else:
|
| 283 |
+
logger.error(f"❌ Failed to register: {display_name}")
|
| 284 |
+
|
| 285 |
+
except Exception as template_error:
|
| 286 |
+
logger.error(f"❌ Error loading {display_name}: {template_error}")
|
| 287 |
+
continue
|
| 288 |
+
|
| 289 |
+
if loaded_agents:
|
| 290 |
+
logger.info(f"✅ Successfully loaded agents: {loaded_agents}")
|
| 291 |
+
else:
|
| 292 |
+
logger.error("❌ No agents could be loaded!")
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
logger.error(f"❌ Agent loading failed: {e}")
|
| 296 |
+
|
| 297 |
+
async def register_agent(self, agent: SaapAgent) -> bool:
|
| 298 |
+
"""Register new agent with database persistence"""
|
| 299 |
+
try:
|
| 300 |
+
# Always add to memory cache first
|
| 301 |
+
self.agents[agent.id] = agent
|
| 302 |
+
|
| 303 |
+
# Try to persist to database if available
|
| 304 |
+
try:
|
| 305 |
+
if db_manager.is_initialized:
|
| 306 |
+
async with db_manager.get_async_session() as session:
|
| 307 |
+
db_agent = DBAgent.from_saap_agent(agent)
|
| 308 |
+
session.add(db_agent)
|
| 309 |
+
await session.commit()
|
| 310 |
+
logger.info(f"✅ Agent registered with database: {agent.name} ({agent.id})")
|
| 311 |
+
else:
|
| 312 |
+
logger.info(f"✅ Agent registered in-memory only: {agent.name} ({agent.id})")
|
| 313 |
+
|
| 314 |
+
except Exception as db_error:
|
| 315 |
+
logger.warning(f"⚠️ Database persistence failed for {agent.name}: {db_error}")
|
| 316 |
+
# But keep the agent in memory
|
| 317 |
+
|
| 318 |
+
return True
|
| 319 |
+
|
| 320 |
+
except Exception as e:
|
| 321 |
+
logger.error(f"❌ Agent registration failed: {e}")
|
| 322 |
+
# Remove from cache if registration completely failed
|
| 323 |
+
self.agents.pop(agent.id, None)
|
| 324 |
+
return False
|
| 325 |
+
|
| 326 |
+
def get_agent(self, agent_id: str) -> Optional[SaapAgent]:
|
| 327 |
+
"""Get agent from memory cache with debug info"""
|
| 328 |
+
agent = self.agents.get(agent_id)
|
| 329 |
+
if agent:
|
| 330 |
+
logger.debug(f"🔍 Agent found: {agent.name} ({agent_id}) - Status: {agent.status}")
|
| 331 |
+
else:
|
| 332 |
+
logger.warning(f"❌ Agent not found: {agent_id}")
|
| 333 |
+
logger.debug(f"📋 Available agents: {list(self.agents.keys())}")
|
| 334 |
+
return agent
|
| 335 |
+
|
| 336 |
+
async def list_agents(self, status: Optional[AgentStatus] = None,
|
| 337 |
+
agent_type: Optional[AgentType] = None) -> List[SaapAgent]:
|
| 338 |
+
"""List all agents with optional filtering"""
|
| 339 |
+
agents = list(self.agents.values())
|
| 340 |
+
|
| 341 |
+
if status:
|
| 342 |
+
agents = [a for a in agents if a.status == status]
|
| 343 |
+
|
| 344 |
+
if agent_type:
|
| 345 |
+
agents = [a for a in agents if a.type == agent_type]
|
| 346 |
+
|
| 347 |
+
return agents
|
| 348 |
+
|
| 349 |
+
async def get_agent_stats(self, agent_id: str) -> Dict[str, Any]:
|
| 350 |
+
"""Get agent statistics"""
|
| 351 |
+
agent = self.get_agent(agent_id)
|
| 352 |
+
if not agent:
|
| 353 |
+
return {}
|
| 354 |
+
|
| 355 |
+
# Return basic stats from agent object
|
| 356 |
+
return {
|
| 357 |
+
"messages_processed": getattr(agent, 'messages_processed', 0),
|
| 358 |
+
"total_tokens": getattr(agent, 'total_tokens', 0),
|
| 359 |
+
"average_response_time": getattr(agent, 'avg_response_time', 0),
|
| 360 |
+
"status": agent.status.value,
|
| 361 |
+
"last_active": getattr(agent, 'last_active', None)
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
async def health_check(self, agent_id: str) -> Dict[str, Any]:
|
| 365 |
+
"""Perform agent health check"""
|
| 366 |
+
agent = self.get_agent(agent_id)
|
| 367 |
+
if not agent:
|
| 368 |
+
return {"healthy": False, "checks": {"agent_exists": False}}
|
| 369 |
+
|
| 370 |
+
return {
|
| 371 |
+
"healthy": agent.status == AgentStatus.ACTIVE,
|
| 372 |
+
"checks": {
|
| 373 |
+
"agent_exists": True,
|
| 374 |
+
"status": agent.status.value,
|
| 375 |
+
"colossus_connection": self.colossus_connection_status == "connected"
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
async def update_agent(self, agent_id: str, updated_data) -> SaapAgent:
|
| 380 |
+
"""Update agent configuration - accepts dict or SaapAgent with schema migration"""
|
| 381 |
+
try:
|
| 382 |
+
# Get current agent
|
| 383 |
+
current_agent = self.get_agent(agent_id)
|
| 384 |
+
if not current_agent:
|
| 385 |
+
raise ValueError(f"Agent {agent_id} not found")
|
| 386 |
+
|
| 387 |
+
# Convert dict to SaapAgent if needed
|
| 388 |
+
if isinstance(updated_data, dict):
|
| 389 |
+
# Get current data
|
| 390 |
+
current_dict = current_agent.dict()
|
| 391 |
+
|
| 392 |
+
# 🔧 FIX: Migrate old frontend schema to new schema
|
| 393 |
+
# Handle top-level 'color' → 'appearance.color'
|
| 394 |
+
if 'color' in updated_data and 'appearance' not in updated_data:
|
| 395 |
+
if 'appearance' not in current_dict:
|
| 396 |
+
current_dict['appearance'] = {}
|
| 397 |
+
current_dict['appearance']['color'] = updated_data.pop('color')
|
| 398 |
+
|
| 399 |
+
# Handle top-level 'avatar' → 'appearance.avatar'
|
| 400 |
+
if 'avatar' in updated_data and 'appearance' not in updated_data:
|
| 401 |
+
if 'appearance' not in current_dict:
|
| 402 |
+
current_dict['appearance'] = {}
|
| 403 |
+
current_dict['appearance']['avatar'] = updated_data.pop('avatar')
|
| 404 |
+
|
| 405 |
+
# Merge updates
|
| 406 |
+
for key, value in updated_data.items():
|
| 407 |
+
if key in current_dict:
|
| 408 |
+
if isinstance(value, dict) and isinstance(current_dict[key], dict):
|
| 409 |
+
# Merge nested dicts
|
| 410 |
+
current_dict[key].update(value)
|
| 411 |
+
else:
|
| 412 |
+
current_dict[key] = value
|
| 413 |
+
|
| 414 |
+
updated_agent = SaapAgent(**current_dict)
|
| 415 |
+
elif isinstance(updated_data, SaapAgent):
|
| 416 |
+
updated_agent = updated_data
|
| 417 |
+
else:
|
| 418 |
+
raise ValueError(f"Invalid update data type: {type(updated_data)}")
|
| 419 |
+
|
| 420 |
+
# Update in memory cache
|
| 421 |
+
self.agents[agent_id] = updated_agent
|
| 422 |
+
|
| 423 |
+
# Try to update in database if available
|
| 424 |
+
if db_manager.is_initialized:
|
| 425 |
+
try:
|
| 426 |
+
async with db_manager.get_async_session() as session:
|
| 427 |
+
# Delete old and insert new (simpler than complex update)
|
| 428 |
+
await session.execute(delete(DBAgent).where(DBAgent.id == agent_id))
|
| 429 |
+
db_agent = DBAgent.from_saap_agent(updated_agent)
|
| 430 |
+
session.add(db_agent)
|
| 431 |
+
await session.commit()
|
| 432 |
+
except Exception as db_error:
|
| 433 |
+
logger.warning(f"⚠️ Database update failed for {agent_id}: {db_error}")
|
| 434 |
+
|
| 435 |
+
logger.info(f"✅ Agent updated: {agent_id}")
|
| 436 |
+
return updated_agent
|
| 437 |
+
|
| 438 |
+
except Exception as e:
|
| 439 |
+
logger.error(f"❌ Agent update failed: {e}")
|
| 440 |
+
raise
|
| 441 |
+
|
| 442 |
+
async def delete_agent(self, agent_id: str) -> bool:
|
| 443 |
+
"""Delete agent from memory and database"""
|
| 444 |
+
try:
|
| 445 |
+
# Stop agent if running
|
| 446 |
+
await self.stop_agent(agent_id)
|
| 447 |
+
|
| 448 |
+
# Remove from memory
|
| 449 |
+
self.agents.pop(agent_id, None)
|
| 450 |
+
|
| 451 |
+
# Try to remove from database if available
|
| 452 |
+
if db_manager.is_initialized:
|
| 453 |
+
try:
|
| 454 |
+
async with db_manager.get_async_session() as session:
|
| 455 |
+
await session.execute(delete(DBAgent).where(DBAgent.id == agent_id))
|
| 456 |
+
await session.commit()
|
| 457 |
+
except Exception as db_error:
|
| 458 |
+
logger.warning(f"⚠️ Database deletion failed for {agent_id}: {db_error}")
|
| 459 |
+
|
| 460 |
+
logger.info(f"✅ Agent deleted: {agent_id}")
|
| 461 |
+
return True
|
| 462 |
+
|
| 463 |
+
except Exception as e:
|
| 464 |
+
logger.error(f"❌ Agent deletion failed: {e}")
|
| 465 |
+
return False
|
| 466 |
+
|
| 467 |
+
async def start_agent(self, agent_id: str) -> bool:
|
| 468 |
+
"""Start agent and create session"""
|
| 469 |
+
try:
|
| 470 |
+
agent = self.get_agent(agent_id)
|
| 471 |
+
if not agent:
|
| 472 |
+
logger.error(f"❌ Cannot start agent: {agent_id} not found")
|
| 473 |
+
return False
|
| 474 |
+
|
| 475 |
+
# Update status
|
| 476 |
+
agent.status = AgentStatus.ACTIVE
|
| 477 |
+
if hasattr(agent, 'metrics') and agent.metrics:
|
| 478 |
+
agent.metrics.last_active = datetime.utcnow()
|
| 479 |
+
|
| 480 |
+
# Try to create agent session in database if available
|
| 481 |
+
if db_manager.is_initialized:
|
| 482 |
+
try:
|
| 483 |
+
async with db_manager.get_async_session() as session:
|
| 484 |
+
db_session = DBAgentSession(agent_id=agent_id)
|
| 485 |
+
session.add(db_session)
|
| 486 |
+
await session.commit()
|
| 487 |
+
await session.refresh(db_session)
|
| 488 |
+
|
| 489 |
+
# Store in active sessions
|
| 490 |
+
self.active_sessions[agent_id] = db_session
|
| 491 |
+
except Exception as db_error:
|
| 492 |
+
logger.warning(f"⚠️ Database session creation failed for {agent_id}: {db_error}")
|
| 493 |
+
|
| 494 |
+
# Update agent status in database if available
|
| 495 |
+
await self._update_agent_status(agent_id, AgentStatus.ACTIVE)
|
| 496 |
+
|
| 497 |
+
logger.info(f"✅ Agent started: {agent.name} ({agent_id})")
|
| 498 |
+
return True
|
| 499 |
+
|
| 500 |
+
except Exception as e:
|
| 501 |
+
logger.error(f"❌ Agent start failed: {e}")
|
| 502 |
+
return False
|
| 503 |
+
|
| 504 |
+
async def stop_agent(self, agent_id: str) -> bool:
|
| 505 |
+
"""Stop agent and close session"""
|
| 506 |
+
try:
|
| 507 |
+
agent = self.get_agent(agent_id)
|
| 508 |
+
if not agent:
|
| 509 |
+
return False
|
| 510 |
+
|
| 511 |
+
# Update status
|
| 512 |
+
agent.status = AgentStatus.INACTIVE
|
| 513 |
+
|
| 514 |
+
# Close agent session if exists
|
| 515 |
+
if agent_id in self.active_sessions:
|
| 516 |
+
session_obj = self.active_sessions[agent_id]
|
| 517 |
+
session_obj.session_end = datetime.utcnow()
|
| 518 |
+
session_obj.status = "completed"
|
| 519 |
+
session_obj.end_reason = "graceful"
|
| 520 |
+
session_obj.calculate_duration()
|
| 521 |
+
|
| 522 |
+
if db_manager.is_initialized:
|
| 523 |
+
try:
|
| 524 |
+
async with db_manager.get_async_session() as session:
|
| 525 |
+
await session.merge(session_obj)
|
| 526 |
+
await session.commit()
|
| 527 |
+
except Exception as db_error:
|
| 528 |
+
logger.warning(f"⚠️ Database session update failed for {agent_id}: {db_error}")
|
| 529 |
+
|
| 530 |
+
del self.active_sessions[agent_id]
|
| 531 |
+
|
| 532 |
+
# Update agent status in database if available
|
| 533 |
+
await self._update_agent_status(agent_id, AgentStatus.INACTIVE)
|
| 534 |
+
|
| 535 |
+
logger.info(f"🔧 Agent stopped: {agent_id}")
|
| 536 |
+
return True
|
| 537 |
+
|
| 538 |
+
except Exception as e:
|
| 539 |
+
logger.error(f"❌ Agent stop failed: {e}")
|
| 540 |
+
return False
|
| 541 |
+
|
| 542 |
+
async def restart_agent(self, agent_id: str) -> bool:
|
| 543 |
+
"""Restart agent (stop + start)"""
|
| 544 |
+
try:
|
| 545 |
+
await self.stop_agent(agent_id)
|
| 546 |
+
await asyncio.sleep(1) # Brief pause
|
| 547 |
+
return await self.start_agent(agent_id)
|
| 548 |
+
except Exception as e:
|
| 549 |
+
logger.error(f"❌ Agent restart failed: {e}")
|
| 550 |
+
return False
|
| 551 |
+
|
| 552 |
+
async def _update_agent_status(self, agent_id: str, status: AgentStatus):
|
| 553 |
+
"""Update agent status in database"""
|
| 554 |
+
if not db_manager.is_initialized:
|
| 555 |
+
return
|
| 556 |
+
|
| 557 |
+
try:
|
| 558 |
+
async with db_manager.get_async_session() as session:
|
| 559 |
+
await session.execute(
|
| 560 |
+
update(DBAgent)
|
| 561 |
+
.where(DBAgent.id == agent_id)
|
| 562 |
+
.values(status=status.value, last_active=datetime.utcnow())
|
| 563 |
+
)
|
| 564 |
+
await session.commit()
|
| 565 |
+
|
| 566 |
+
except Exception as e:
|
| 567 |
+
logger.warning(f"⚠️ Failed to update agent status in database: {e}")
|
| 568 |
+
|
| 569 |
+
# 🚀 NEW: Multi-Provider Chat Support
|
| 570 |
+
async def send_message_to_agent(self, agent_id: str, message: str,
|
| 571 |
+
provider: Optional[str] = None) -> Dict[str, Any]:
|
| 572 |
+
"""
|
| 573 |
+
Send message to agent via specified provider or auto-fallback
|
| 574 |
+
|
| 575 |
+
Args:
|
| 576 |
+
agent_id: Target agent ID
|
| 577 |
+
message: Message content
|
| 578 |
+
provider: Optional provider override ("colossus", "openrouter", or None for auto)
|
| 579 |
+
|
| 580 |
+
Returns:
|
| 581 |
+
Chat response with metadata
|
| 582 |
+
"""
|
| 583 |
+
try:
|
| 584 |
+
# Enhanced error checking with detailed debugging
|
| 585 |
+
agent = self.get_agent(agent_id)
|
| 586 |
+
if not agent:
|
| 587 |
+
error_msg = f"Agent {agent_id} not found in loaded agents"
|
| 588 |
+
logger.error(f"❌ {error_msg}")
|
| 589 |
+
logger.debug(f"📋 Available agents: {list(self.agents.keys())}")
|
| 590 |
+
return {
|
| 591 |
+
"error": error_msg,
|
| 592 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 593 |
+
"debug_info": {
|
| 594 |
+
"available_agents": list(self.agents.keys()),
|
| 595 |
+
"agent_manager_initialized": self.is_initialized
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
# Check if agent is available for messaging
|
| 600 |
+
if agent.status != AgentStatus.ACTIVE:
|
| 601 |
+
error_msg = f"Agent {agent_id} not available (status: {agent.status.value})"
|
| 602 |
+
logger.error(f"❌ {error_msg}")
|
| 603 |
+
return {
|
| 604 |
+
"error": error_msg,
|
| 605 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 606 |
+
"debug_info": {
|
| 607 |
+
"agent_status": agent.status.value,
|
| 608 |
+
"agent_id": agent_id
|
| 609 |
+
}
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
# 🚀 Multi-Provider Logic
|
| 613 |
+
if provider == "openrouter":
|
| 614 |
+
return await self._send_via_openrouter(agent_id, message, agent)
|
| 615 |
+
elif provider == "colossus":
|
| 616 |
+
return await self._send_via_colossus(agent_id, message, agent)
|
| 617 |
+
else:
|
| 618 |
+
# Auto-selection: Try colossus first, fallback to OpenRouter
|
| 619 |
+
if self.colossus_connection_status == "connected":
|
| 620 |
+
logger.info(f"🔄 Using colossus as primary provider for {agent_id}")
|
| 621 |
+
result = await self._send_via_colossus(agent_id, message, agent)
|
| 622 |
+
# If colossus fails, try OpenRouter
|
| 623 |
+
if "error" in result and "colossus" in result["error"].lower():
|
| 624 |
+
logger.info(f"🔄 colossus failed, trying OpenRouter fallback...")
|
| 625 |
+
return await self._send_via_openrouter(agent_id, message, agent)
|
| 626 |
+
return result
|
| 627 |
+
else:
|
| 628 |
+
logger.info(f"🔄 colossus unavailable, using OpenRouter as primary for {agent_id}")
|
| 629 |
+
return await self._send_via_openrouter(agent_id, message, agent)
|
| 630 |
+
|
| 631 |
+
except Exception as e:
|
| 632 |
+
error_msg = str(e)
|
| 633 |
+
logger.error(f"❌ Message to agent failed: {error_msg}")
|
| 634 |
+
return {
|
| 635 |
+
"error": error_msg,
|
| 636 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 637 |
+
"debug_info": {
|
| 638 |
+
"agent_id": agent_id,
|
| 639 |
+
"provider": provider,
|
| 640 |
+
"colossus_status": self.colossus_connection_status,
|
| 641 |
+
"agent_found": agent_id in self.agents,
|
| 642 |
+
"colossus_client_exists": self.colossus_client is not None
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
async def _send_via_openrouter(self, agent_id: str, message: str,
|
| 647 |
+
agent: SaapAgent) -> Dict[str, Any]:
|
| 648 |
+
"""Send message via OpenRouter provider"""
|
| 649 |
+
try:
|
| 650 |
+
logger.info(f"🌐 {agent_id} (coordinator) initialized with OpenRouter FREE")
|
| 651 |
+
|
| 652 |
+
# Create OpenRouter agent for this request
|
| 653 |
+
openrouter_agent = OpenRouterSAAPAgent(
|
| 654 |
+
agent_id,
|
| 655 |
+
agent.type.value if agent.type else "Assistant",
|
| 656 |
+
os.getenv("OPENROUTER_API_KEY")
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
# Get cost-optimized model for specific agent
|
| 660 |
+
model_map = {
|
| 661 |
+
"jane_alesi": os.getenv("JANE_ALESI_MODEL", "openai/gpt-4o-mini"),
|
| 662 |
+
"john_alesi": os.getenv("JOHN_ALESI_MODEL", "deepseek/deepseek-coder"),
|
| 663 |
+
"lara_alesi": os.getenv("LARA_ALESI_MODEL", "anthropic/claude-3-haiku"),
|
| 664 |
+
"theo_alesi": os.getenv("THEO_ALESI_MODEL", "openai/gpt-4o-mini"), # 💰 Financial
|
| 665 |
+
"justus_alesi": os.getenv("JUSTUS_ALESI_MODEL", "anthropic/claude-3-haiku"), # ⚖️ Legal
|
| 666 |
+
"leon_alesi": os.getenv("LEON_ALESI_MODEL", "deepseek/deepseek-coder"), # 🔧 System
|
| 667 |
+
"luna_alesi": os.getenv("LUNA_ALESI_MODEL", "openai/gpt-4o-mini") # 🌟 Coaching
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
preferred_model = model_map.get(agent_id, "meta-llama/llama-3.2-3b-instruct:free")
|
| 671 |
+
openrouter_agent.model_name = preferred_model
|
| 672 |
+
|
| 673 |
+
start_time = datetime.utcnow()
|
| 674 |
+
logger.info(f"📤 Sending message to {agent.name} ({agent_id}) via OpenRouter ({preferred_model})...")
|
| 675 |
+
|
| 676 |
+
# 🔧 FIXED: Use safe LLM config access
|
| 677 |
+
max_tokens_value = self._get_llm_config_value(agent, 'max_tokens', 1000)
|
| 678 |
+
|
| 679 |
+
# Send message via OpenRouter
|
| 680 |
+
response = await openrouter_agent.send_request_to_openrouter(
|
| 681 |
+
message,
|
| 682 |
+
max_tokens=max_tokens_value
|
| 683 |
+
)
|
| 684 |
+
|
| 685 |
+
end_time = datetime.utcnow()
|
| 686 |
+
response_time = (end_time - start_time).total_seconds()
|
| 687 |
+
|
| 688 |
+
if response.get("success"):
|
| 689 |
+
logger.info(f"✅ OpenRouter response successful in {response_time:.2f}s")
|
| 690 |
+
|
| 691 |
+
response_content = response.get("response", "")
|
| 692 |
+
tokens_used = response.get("token_count", 0)
|
| 693 |
+
cost_usd = response.get("cost_usd", 0.0)
|
| 694 |
+
|
| 695 |
+
# Try to save to database if available
|
| 696 |
+
if db_manager.is_initialized:
|
| 697 |
+
try:
|
| 698 |
+
async with db_manager.get_async_session() as session:
|
| 699 |
+
chat_message = DBChatMessage(
|
| 700 |
+
agent_id=agent_id,
|
| 701 |
+
user_message=message,
|
| 702 |
+
agent_response=response_content,
|
| 703 |
+
response_time=response_time,
|
| 704 |
+
tokens_used=tokens_used,
|
| 705 |
+
metadata={
|
| 706 |
+
"model": preferred_model,
|
| 707 |
+
"provider": "OpenRouter",
|
| 708 |
+
"cost_usd": cost_usd,
|
| 709 |
+
"temperature": 0.7
|
| 710 |
+
}
|
| 711 |
+
)
|
| 712 |
+
session.add(chat_message)
|
| 713 |
+
await session.commit()
|
| 714 |
+
except Exception as db_error:
|
| 715 |
+
logger.warning(f"⚠️ Failed to save OpenRouter chat to database: {db_error}")
|
| 716 |
+
|
| 717 |
+
return {
|
| 718 |
+
"content": response_content,
|
| 719 |
+
"response_time": response_time,
|
| 720 |
+
"tokens_used": tokens_used,
|
| 721 |
+
"cost_usd": cost_usd,
|
| 722 |
+
"provider": "OpenRouter",
|
| 723 |
+
"model": preferred_model,
|
| 724 |
+
"timestamp": end_time.isoformat()
|
| 725 |
+
}
|
| 726 |
+
else:
|
| 727 |
+
error_msg = response.get("error", "Unknown OpenRouter error")
|
| 728 |
+
logger.error(f"❌ OpenRouter fallback failed: {error_msg}")
|
| 729 |
+
return {
|
| 730 |
+
"error": f"OpenRouter error: {error_msg}",
|
| 731 |
+
"provider": "OpenRouter",
|
| 732 |
+
"timestamp": end_time.isoformat()
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
except Exception as e:
|
| 736 |
+
logger.error(f"❌ OpenRouter fallback failed: {str(e)}")
|
| 737 |
+
return {
|
| 738 |
+
"error": f"OpenRouter error: {str(e)}",
|
| 739 |
+
"provider": "OpenRouter",
|
| 740 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
async def _send_via_colossus(self, agent_id: str, message: str,
|
| 744 |
+
agent: SaapAgent) -> Dict[str, Any]:
|
| 745 |
+
"""Send message via colossus provider"""
|
| 746 |
+
try:
|
| 747 |
+
# Check colossus client availability
|
| 748 |
+
if not self.colossus_client:
|
| 749 |
+
return {
|
| 750 |
+
"error": "colossus client not initialized",
|
| 751 |
+
"provider": "colossus",
|
| 752 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
# Test colossus connection if it's been a while
|
| 756 |
+
if (not self.last_colossus_test or
|
| 757 |
+
(datetime.utcnow() - self.last_colossus_test).seconds > 300): # 5 minutes
|
| 758 |
+
await self._test_colossus_connection()
|
| 759 |
+
|
| 760 |
+
if self.colossus_connection_status != "connected":
|
| 761 |
+
return {
|
| 762 |
+
"error": f"colossus connection not healthy: {self.colossus_connection_status}",
|
| 763 |
+
"provider": "colossus",
|
| 764 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
start_time = datetime.utcnow()
|
| 768 |
+
logger.info(f"📤 Sending message to {agent.name} ({agent_id}) via colossus...")
|
| 769 |
+
|
| 770 |
+
# 🔧 FIXED: Use safe LLM config access
|
| 771 |
+
temperature_value = self._get_llm_config_value(agent, 'temperature', 0.7)
|
| 772 |
+
max_tokens_value = self._get_llm_config_value(agent, 'max_tokens', 1000)
|
| 773 |
+
|
| 774 |
+
# Send message to colossus
|
| 775 |
+
response = await self.colossus_client.chat_completion(
|
| 776 |
+
messages=[
|
| 777 |
+
{"role": "system", "content": agent.description or f"You are {agent.name}"},
|
| 778 |
+
{"role": "user", "content": message}
|
| 779 |
+
],
|
| 780 |
+
agent_id=agent_id,
|
| 781 |
+
temperature=temperature_value,
|
| 782 |
+
max_tokens=max_tokens_value
|
| 783 |
+
)
|
| 784 |
+
|
| 785 |
+
end_time = datetime.utcnow()
|
| 786 |
+
response_time = (end_time - start_time).total_seconds()
|
| 787 |
+
|
| 788 |
+
logger.info(f"📥 Received response from colossus in {response_time:.2f}s")
|
| 789 |
+
|
| 790 |
+
# Enhanced response parsing
|
| 791 |
+
response_content = ""
|
| 792 |
+
tokens_used = 0
|
| 793 |
+
|
| 794 |
+
if response:
|
| 795 |
+
logger.debug(f"🔍 Raw colossus response: {response}")
|
| 796 |
+
|
| 797 |
+
if isinstance(response, dict):
|
| 798 |
+
# SAAP ColossusClient format: {"success": true, "response": {...}}
|
| 799 |
+
if response.get("success") and "response" in response:
|
| 800 |
+
colossus_response = response["response"]
|
| 801 |
+
if isinstance(colossus_response, dict) and "choices" in colossus_response:
|
| 802 |
+
# OpenAI-compatible format within SAAP response
|
| 803 |
+
if len(colossus_response["choices"]) > 0:
|
| 804 |
+
choice = colossus_response["choices"][0]
|
| 805 |
+
if "message" in choice and "content" in choice["message"]:
|
| 806 |
+
response_content = choice["message"]["content"]
|
| 807 |
+
elif isinstance(colossus_response, str):
|
| 808 |
+
# Direct string response
|
| 809 |
+
response_content = colossus_response
|
| 810 |
+
|
| 811 |
+
# Extract token usage if available
|
| 812 |
+
if isinstance(colossus_response, dict) and "usage" in colossus_response:
|
| 813 |
+
tokens_used = colossus_response["usage"].get("total_tokens", 0)
|
| 814 |
+
|
| 815 |
+
# Handle colossus client error responses
|
| 816 |
+
elif not response.get("success"):
|
| 817 |
+
error_msg = response.get("error", "Unknown colossus error")
|
| 818 |
+
logger.error(f"❌ colossus error: {error_msg}")
|
| 819 |
+
return {
|
| 820 |
+
"error": f"colossus server error: {error_msg}",
|
| 821 |
+
"provider": "colossus",
|
| 822 |
+
"timestamp": end_time.isoformat()
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
# Direct OpenAI format: {"choices": [...]}
|
| 826 |
+
elif "choices" in response and len(response["choices"]) > 0:
|
| 827 |
+
choice = response["choices"][0]
|
| 828 |
+
if "message" in choice and "content" in choice["message"]:
|
| 829 |
+
response_content = choice["message"]["content"]
|
| 830 |
+
if "usage" in response:
|
| 831 |
+
tokens_used = response["usage"].get("total_tokens", 0)
|
| 832 |
+
|
| 833 |
+
# Simple response format: {"response": "text"} or {"content": "text"}
|
| 834 |
+
elif "response" in response:
|
| 835 |
+
response_content = response["response"]
|
| 836 |
+
elif "content" in response:
|
| 837 |
+
response_content = response["content"]
|
| 838 |
+
|
| 839 |
+
elif isinstance(response, str):
|
| 840 |
+
# Direct string response
|
| 841 |
+
response_content = response
|
| 842 |
+
|
| 843 |
+
# Fallback if no content extracted
|
| 844 |
+
if not response_content:
|
| 845 |
+
logger.error(f"❌ Unable to extract content from colossus response: {response}")
|
| 846 |
+
return {
|
| 847 |
+
"error": "Failed to parse colossus response",
|
| 848 |
+
"provider": "colossus",
|
| 849 |
+
"timestamp": end_time.isoformat()
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
# Try to save to database if available
|
| 853 |
+
if db_manager.is_initialized:
|
| 854 |
+
try:
|
| 855 |
+
async with db_manager.get_async_session() as session:
|
| 856 |
+
chat_message = DBChatMessage(
|
| 857 |
+
agent_id=agent_id,
|
| 858 |
+
user_message=message,
|
| 859 |
+
agent_response=response_content,
|
| 860 |
+
response_time=response_time,
|
| 861 |
+
tokens_used=tokens_used,
|
| 862 |
+
metadata={
|
| 863 |
+
"model": "mistral-small3.2:24b-instruct-2506",
|
| 864 |
+
"provider": "colossus",
|
| 865 |
+
"temperature": 0.7
|
| 866 |
+
}
|
| 867 |
+
)
|
| 868 |
+
session.add(chat_message)
|
| 869 |
+
await session.commit()
|
| 870 |
+
except Exception as db_error:
|
| 871 |
+
logger.warning(f"⚠️ Failed to save chat message to database: {db_error}")
|
| 872 |
+
|
| 873 |
+
# Update session metrics
|
| 874 |
+
if agent_id in self.active_sessions:
|
| 875 |
+
session_obj = self.active_sessions[agent_id]
|
| 876 |
+
session_obj.messages_processed += 1
|
| 877 |
+
session_obj.total_tokens_used += tokens_used
|
| 878 |
+
|
| 879 |
+
logger.info(f"✅ Message processed successfully for {agent.name}")
|
| 880 |
+
|
| 881 |
+
return {
|
| 882 |
+
"content": response_content,
|
| 883 |
+
"response_time": response_time,
|
| 884 |
+
"tokens_used": tokens_used,
|
| 885 |
+
"provider": "colossus",
|
| 886 |
+
"model": "mistral-small3.2:24b-instruct-2506",
|
| 887 |
+
"timestamp": end_time.isoformat()
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
except Exception as e:
|
| 891 |
+
logger.error(f"❌ colossus communication failed: {str(e)}")
|
| 892 |
+
return {
|
| 893 |
+
"error": f"colossus error: {str(e)}",
|
| 894 |
+
"provider": "colossus",
|
| 895 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
async def get_agent_metrics(self, agent_id: str) -> Dict[str, Any]:
|
| 899 |
+
"""Get comprehensive agent metrics from database"""
|
| 900 |
+
if not db_manager.is_initialized:
|
| 901 |
+
return {"warning": "Database not available - no metrics"}
|
| 902 |
+
|
| 903 |
+
try:
|
| 904 |
+
async with db_manager.get_async_session() as session:
|
| 905 |
+
# Get message count and average response time
|
| 906 |
+
result = await session.execute(
|
| 907 |
+
select(DBChatMessage).where(DBChatMessage.agent_id == agent_id)
|
| 908 |
+
)
|
| 909 |
+
messages = result.scalars().all()
|
| 910 |
+
|
| 911 |
+
if messages:
|
| 912 |
+
avg_response_time = sum(m.response_time for m in messages if m.response_time) / len(messages)
|
| 913 |
+
total_tokens = sum(m.tokens_used for m in messages if m.tokens_used)
|
| 914 |
+
else:
|
| 915 |
+
avg_response_time = 0
|
| 916 |
+
total_tokens = 0
|
| 917 |
+
|
| 918 |
+
# Get session count
|
| 919 |
+
session_result = await session.execute(
|
| 920 |
+
select(DBAgentSession).where(DBAgentSession.agent_id == agent_id)
|
| 921 |
+
)
|
| 922 |
+
sessions = session_result.scalars().all()
|
| 923 |
+
|
| 924 |
+
return {
|
| 925 |
+
"total_messages": len(messages),
|
| 926 |
+
"total_tokens_used": total_tokens,
|
| 927 |
+
"average_response_time": avg_response_time,
|
| 928 |
+
"total_sessions": len(sessions),
|
| 929 |
+
"last_activity": max([s.session_start for s in sessions], default=None),
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
except Exception as e:
|
| 933 |
+
logger.error(f"❌ Failed to get agent metrics: {e}")
|
| 934 |
+
return {}
|
| 935 |
+
|
| 936 |
+
async def get_system_status(self) -> Dict[str, Any]:
|
| 937 |
+
"""Get comprehensive system status for debugging"""
|
| 938 |
+
return {
|
| 939 |
+
"agent_manager_initialized": self.is_initialized,
|
| 940 |
+
"colossus_connection_status": self.colossus_connection_status,
|
| 941 |
+
"colossus_last_test": self.last_colossus_test.isoformat() if self.last_colossus_test else None,
|
| 942 |
+
"loaded_agents": len(self.agents),
|
| 943 |
+
"active_sessions": len(self.active_sessions),
|
| 944 |
+
"agent_list": [{"id": aid, "name": agent.name, "status": agent.status.value}
|
| 945 |
+
for aid, agent in self.agents.items()],
|
| 946 |
+
"database_initialized": getattr(db_manager, 'is_initialized', False)
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
async def shutdown_all_agents(self):
|
| 950 |
+
"""Gracefully shutdown all active agents"""
|
| 951 |
+
try:
|
| 952 |
+
logger.info("🔧 Shutting down all agents...")
|
| 953 |
+
|
| 954 |
+
for agent_id in list(self.agents.keys()):
|
| 955 |
+
await self.stop_agent(agent_id)
|
| 956 |
+
|
| 957 |
+
if self.colossus_client:
|
| 958 |
+
await self.colossus_client.__aexit__(None, None, None)
|
| 959 |
+
|
| 960 |
+
logger.info("✅ All agents shut down successfully")
|
| 961 |
+
|
| 962 |
+
except Exception as e:
|
| 963 |
+
logger.error(f"❌ Agent shutdown failed: {e}")
|
| 964 |
+
|
| 965 |
+
# Create global instance for dependency injection
|
| 966 |
+
agent_manager = AgentManagerService()
|
| 967 |
+
|
| 968 |
+
# Make class available for import
|
| 969 |
+
AgentManager = AgentManagerService
|
| 970 |
+
|
| 971 |
+
if __name__ == "__main__":
|
| 972 |
+
async def test_agent_manager():
|
| 973 |
+
"""Test agent manager functionality"""
|
| 974 |
+
manager = AgentManagerService()
|
| 975 |
+
await manager.initialize()
|
| 976 |
+
|
| 977 |
+
# List agents
|
| 978 |
+
agents = list(manager.agents.values())
|
| 979 |
+
print(f"📋 Agents loaded: {[a.name for a in agents]}")
|
| 980 |
+
|
| 981 |
+
# Start first agent
|
| 982 |
+
if agents:
|
| 983 |
+
agent = agents[0]
|
| 984 |
+
success = await manager.start_agent(agent.id)
|
| 985 |
+
print(f"🚀 Start agent {agent.name}: {'✅' if success else '❌'}")
|
| 986 |
+
|
| 987 |
+
await manager.shutdown_all_agents()
|
| 988 |
+
|
| 989 |
+
asyncio.run(test_agent_manager())
|
backend/agent_manager_database_enhanced.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🔧 Enhanced Agent Manager Service - Database Loading Fix
|
| 3 |
+
Verbesserte Version mit robuster Database-Memory Integration
|
| 4 |
+
|
| 5 |
+
Behebt kritische Probleme:
|
| 6 |
+
- Agents werden nicht aus Database geladen beim Backend-Start
|
| 7 |
+
- Memory vs Database Mismatch zwischen Services
|
| 8 |
+
- Neue Agents verschwinden nach Server-Restart
|
| 9 |
+
- "No description available" Problem
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import asyncio
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Dict, List, Optional, Any
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
|
| 17 |
+
from services.agent_manager import AgentManagerService
|
| 18 |
+
from database.connection import db_manager
|
| 19 |
+
from database.models import DBAgent
|
| 20 |
+
from models.agent import SaapAgent, AgentStatus, AgentType
|
| 21 |
+
from sqlalchemy import select
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
class EnhancedAgentManagerService(AgentManagerService):
|
| 26 |
+
"""
|
| 27 |
+
Enhanced Agent Manager mit verbesserter Database Integration
|
| 28 |
+
|
| 29 |
+
Neue Features:
|
| 30 |
+
- Force Database Loading beim Startup
|
| 31 |
+
- Enhanced Database-Memory Bridge
|
| 32 |
+
- Guaranteed Agent Persistence
|
| 33 |
+
- Improved Error Handling
|
| 34 |
+
- Better Service Integration
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(self, *args, **kwargs):
|
| 38 |
+
super().__init__(*args, **kwargs)
|
| 39 |
+
self.database_loading_enabled = True
|
| 40 |
+
self.force_database_sync = True
|
| 41 |
+
|
| 42 |
+
async def initialize(self):
|
| 43 |
+
"""Enhanced initialization with guaranteed Database Agent Loading"""
|
| 44 |
+
try:
|
| 45 |
+
logger.info("🚀 Initializing Enhanced Agent Manager Service...")
|
| 46 |
+
|
| 47 |
+
# Initialize colossus client (from parent)
|
| 48 |
+
await self._initialize_colossus_client()
|
| 49 |
+
|
| 50 |
+
# Enhanced Database Agent Loading - GUARANTEED
|
| 51 |
+
await self.force_load_agents_from_database()
|
| 52 |
+
|
| 53 |
+
self.is_initialized = True
|
| 54 |
+
logger.info(f"✅ Enhanced Agent Manager initialized: {len(self.agents)} agents loaded")
|
| 55 |
+
|
| 56 |
+
# Log all loaded agents for debugging
|
| 57 |
+
for agent_id, agent in self.agents.items():
|
| 58 |
+
logger.info(f"📋 Loaded: {agent.name} ({agent_id}) - {agent.status.value}")
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"❌ Enhanced Agent Manager initialization failed: {e}")
|
| 62 |
+
raise
|
| 63 |
+
|
| 64 |
+
async def _initialize_colossus_client(self):
|
| 65 |
+
"""Initialize colossus client with error handling"""
|
| 66 |
+
try:
|
| 67 |
+
from api.colossus_client import ColossusClient
|
| 68 |
+
logger.info("🔌 Connecting to colossus server...")
|
| 69 |
+
self.colossus_client = ColossusClient()
|
| 70 |
+
await self.colossus_client.__aenter__()
|
| 71 |
+
await self._test_colossus_connection()
|
| 72 |
+
except Exception as colossus_error:
|
| 73 |
+
logger.error(f"❌ colossus connection failed: {colossus_error}")
|
| 74 |
+
self.colossus_connection_status = f"failed: {str(colossus_error)}"
|
| 75 |
+
|
| 76 |
+
async def force_load_agents_from_database(self):
|
| 77 |
+
"""
|
| 78 |
+
🚀 ENHANCED: Force load all agents from database with guaranteed success
|
| 79 |
+
|
| 80 |
+
This method GUARANTEES that all database agents are loaded into memory.
|
| 81 |
+
It replaces the problematic _load_agents_from_database method.
|
| 82 |
+
"""
|
| 83 |
+
try:
|
| 84 |
+
logger.info("🔍 Force loading agents from database...")
|
| 85 |
+
|
| 86 |
+
# Step 1: Ensure database is initialized
|
| 87 |
+
await self._ensure_database_initialized()
|
| 88 |
+
|
| 89 |
+
# Step 2: Load all agents from database
|
| 90 |
+
loaded_count = await self._load_database_agents()
|
| 91 |
+
|
| 92 |
+
# Step 3: Add default agents if database is empty
|
| 93 |
+
if loaded_count == 0:
|
| 94 |
+
logger.info("📦 No agents in database - adding default agents...")
|
| 95 |
+
await self._add_default_agents_to_database()
|
| 96 |
+
loaded_count = await self._load_database_agents()
|
| 97 |
+
|
| 98 |
+
# Step 4: Verify loading success
|
| 99 |
+
await self._verify_agent_loading(loaded_count)
|
| 100 |
+
|
| 101 |
+
logger.info(f"✅ Force loading successful: {len(self.agents)} agents in memory")
|
| 102 |
+
|
| 103 |
+
except Exception as e:
|
| 104 |
+
logger.error(f"❌ Force loading failed: {e}")
|
| 105 |
+
# Fallback to default agents if database loading fails completely
|
| 106 |
+
logger.info("🆘 Fallback: Loading default agents in memory-only mode...")
|
| 107 |
+
await super().load_default_agents()
|
| 108 |
+
|
| 109 |
+
async def _ensure_database_initialized(self):
|
| 110 |
+
"""Ensure database is properly initialized"""
|
| 111 |
+
max_retries = 3
|
| 112 |
+
retry_count = 0
|
| 113 |
+
|
| 114 |
+
while retry_count < max_retries:
|
| 115 |
+
try:
|
| 116 |
+
if not db_manager.is_initialized:
|
| 117 |
+
logger.info(f"📊 Database not initialized - attempting initialization (retry {retry_count + 1}/{max_retries})...")
|
| 118 |
+
await db_manager.initialize()
|
| 119 |
+
|
| 120 |
+
# Test database connectivity
|
| 121 |
+
async with db_manager.get_async_session() as session:
|
| 122 |
+
result = await session.execute(select(DBAgent).limit(1))
|
| 123 |
+
result.scalars().first()
|
| 124 |
+
|
| 125 |
+
logger.info("✅ Database connection verified")
|
| 126 |
+
return
|
| 127 |
+
|
| 128 |
+
except Exception as e:
|
| 129 |
+
retry_count += 1
|
| 130 |
+
if retry_count >= max_retries:
|
| 131 |
+
raise Exception(f"Database initialization failed after {max_retries} retries: {e}")
|
| 132 |
+
|
| 133 |
+
logger.warning(f"⚠️ Database init attempt {retry_count} failed: {e}")
|
| 134 |
+
await asyncio.sleep(1)
|
| 135 |
+
|
| 136 |
+
async def _load_database_agents(self) -> int:
|
| 137 |
+
"""Load all agents from database into memory"""
|
| 138 |
+
try:
|
| 139 |
+
async with db_manager.get_async_session() as session:
|
| 140 |
+
result = await session.execute(select(DBAgent))
|
| 141 |
+
db_agents = result.scalars().all()
|
| 142 |
+
|
| 143 |
+
logger.info(f"📚 Found {len(db_agents)} agents in database")
|
| 144 |
+
|
| 145 |
+
# Clear existing agents and load from database
|
| 146 |
+
self.agents.clear()
|
| 147 |
+
loaded_count = 0
|
| 148 |
+
|
| 149 |
+
for db_agent in db_agents:
|
| 150 |
+
try:
|
| 151 |
+
saap_agent = db_agent.to_saap_agent()
|
| 152 |
+
self.agents[saap_agent.id] = saap_agent
|
| 153 |
+
loaded_count += 1
|
| 154 |
+
logger.debug(f"🔄 Loaded: {saap_agent.name} ({saap_agent.id})")
|
| 155 |
+
except Exception as conversion_error:
|
| 156 |
+
logger.warning(f"⚠️ Failed to convert agent {db_agent.id}: {conversion_error}")
|
| 157 |
+
|
| 158 |
+
logger.info(f"✅ Successfully loaded {loaded_count} agents from database")
|
| 159 |
+
return loaded_count
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
logger.error(f"❌ Database agent loading failed: {e}")
|
| 163 |
+
return 0
|
| 164 |
+
|
| 165 |
+
async def _add_default_agents_to_database(self):
|
| 166 |
+
"""Add default Alesi agents to database"""
|
| 167 |
+
try:
|
| 168 |
+
from models.agent import AgentTemplates
|
| 169 |
+
|
| 170 |
+
default_agents = [
|
| 171 |
+
AgentTemplates.jane_alesi(),
|
| 172 |
+
AgentTemplates.john_alesi(),
|
| 173 |
+
AgentTemplates.lara_alesi()
|
| 174 |
+
]
|
| 175 |
+
|
| 176 |
+
async with db_manager.get_async_session() as session:
|
| 177 |
+
for agent in default_agents:
|
| 178 |
+
# Check if agent already exists
|
| 179 |
+
result = await session.execute(
|
| 180 |
+
select(DBAgent).where(DBAgent.id == agent.id)
|
| 181 |
+
)
|
| 182 |
+
if result.scalars().first():
|
| 183 |
+
logger.debug(f"⚠️ Agent {agent.id} already exists in database")
|
| 184 |
+
continue
|
| 185 |
+
|
| 186 |
+
db_agent = DBAgent.from_saap_agent(agent)
|
| 187 |
+
session.add(db_agent)
|
| 188 |
+
logger.info(f"➕ Added default agent to database: {agent.name}")
|
| 189 |
+
|
| 190 |
+
await session.commit()
|
| 191 |
+
|
| 192 |
+
logger.info("✅ Default agents added to database")
|
| 193 |
+
|
| 194 |
+
except Exception as e:
|
| 195 |
+
logger.error(f"❌ Failed to add default agents to database: {e}")
|
| 196 |
+
raise
|
| 197 |
+
|
| 198 |
+
async def _verify_agent_loading(self, expected_count: int):
|
| 199 |
+
"""Verify that agent loading was successful"""
|
| 200 |
+
memory_count = len(self.agents)
|
| 201 |
+
|
| 202 |
+
if memory_count != expected_count:
|
| 203 |
+
logger.warning(f"⚠️ Agent count mismatch: Expected {expected_count}, got {memory_count}")
|
| 204 |
+
|
| 205 |
+
# Verify agent data integrity
|
| 206 |
+
for agent_id, agent in self.agents.items():
|
| 207 |
+
if not agent.name or agent.name == "Unknown Agent":
|
| 208 |
+
logger.warning(f"⚠️ Agent {agent_id} has missing name")
|
| 209 |
+
if not agent.description:
|
| 210 |
+
logger.warning(f"⚠️ Agent {agent_id} has missing description")
|
| 211 |
+
|
| 212 |
+
logger.info(f"✅ Agent loading verification completed: {memory_count} agents")
|
| 213 |
+
|
| 214 |
+
async def register_agent(self, agent: SaapAgent) -> bool:
|
| 215 |
+
"""Enhanced agent registration with guaranteed database persistence"""
|
| 216 |
+
try:
|
| 217 |
+
# Always add to memory cache first
|
| 218 |
+
self.agents[agent.id] = agent
|
| 219 |
+
logger.info(f"📝 Agent added to memory: {agent.name} ({agent.id})")
|
| 220 |
+
|
| 221 |
+
# Force database persistence
|
| 222 |
+
await self._force_database_persistence(agent)
|
| 223 |
+
|
| 224 |
+
return True
|
| 225 |
+
|
| 226 |
+
except Exception as e:
|
| 227 |
+
logger.error(f"❌ Enhanced agent registration failed: {e}")
|
| 228 |
+
# Remove from cache if registration failed
|
| 229 |
+
self.agents.pop(agent.id, None)
|
| 230 |
+
return False
|
| 231 |
+
|
| 232 |
+
async def _force_database_persistence(self, agent: SaapAgent):
|
| 233 |
+
"""Force agent persistence to database with retries"""
|
| 234 |
+
max_retries = 3
|
| 235 |
+
retry_count = 0
|
| 236 |
+
|
| 237 |
+
while retry_count < max_retries:
|
| 238 |
+
try:
|
| 239 |
+
await self._ensure_database_initialized()
|
| 240 |
+
|
| 241 |
+
async with db_manager.get_async_session() as session:
|
| 242 |
+
# Check if agent already exists
|
| 243 |
+
result = await session.execute(
|
| 244 |
+
select(DBAgent).where(DBAgent.id == agent.id)
|
| 245 |
+
)
|
| 246 |
+
existing = result.scalars().first()
|
| 247 |
+
|
| 248 |
+
if existing:
|
| 249 |
+
# Update existing agent
|
| 250 |
+
updated_agent = DBAgent.from_saap_agent(agent)
|
| 251 |
+
await session.merge(updated_agent)
|
| 252 |
+
logger.info(f"🔄 Agent updated in database: {agent.name}")
|
| 253 |
+
else:
|
| 254 |
+
# Create new agent
|
| 255 |
+
db_agent = DBAgent.from_saap_agent(agent)
|
| 256 |
+
session.add(db_agent)
|
| 257 |
+
logger.info(f"➕ Agent added to database: {agent.name}")
|
| 258 |
+
|
| 259 |
+
await session.commit()
|
| 260 |
+
|
| 261 |
+
logger.info(f"✅ Database persistence successful: {agent.name}")
|
| 262 |
+
return
|
| 263 |
+
|
| 264 |
+
except Exception as e:
|
| 265 |
+
retry_count += 1
|
| 266 |
+
if retry_count >= max_retries:
|
| 267 |
+
raise Exception(f"Database persistence failed after {max_retries} retries: {e}")
|
| 268 |
+
|
| 269 |
+
logger.warning(f"⚠️ Database persistence attempt {retry_count} failed: {e}")
|
| 270 |
+
await asyncio.sleep(0.5)
|
| 271 |
+
|
| 272 |
+
async def get_comprehensive_agent_status(self) -> Dict[str, Any]:
|
| 273 |
+
"""Get comprehensive status for debugging"""
|
| 274 |
+
try:
|
| 275 |
+
# Database agent count
|
| 276 |
+
async with db_manager.get_async_session() as session:
|
| 277 |
+
result = await session.execute(select(DBAgent))
|
| 278 |
+
db_agents = result.scalars().all()
|
| 279 |
+
db_count = len(db_agents)
|
| 280 |
+
|
| 281 |
+
# Memory agent count
|
| 282 |
+
memory_count = len(self.agents)
|
| 283 |
+
|
| 284 |
+
# Agent details
|
| 285 |
+
agent_details = []
|
| 286 |
+
for agent_id, agent in self.agents.items():
|
| 287 |
+
agent_details.append({
|
| 288 |
+
"id": agent_id,
|
| 289 |
+
"name": agent.name,
|
| 290 |
+
"type": agent.type.value,
|
| 291 |
+
"status": agent.status.value,
|
| 292 |
+
"has_description": bool(agent.description and agent.description != "No description available")
|
| 293 |
+
})
|
| 294 |
+
|
| 295 |
+
return {
|
| 296 |
+
"database_initialized": db_manager.is_initialized,
|
| 297 |
+
"database_agent_count": db_count,
|
| 298 |
+
"memory_agent_count": memory_count,
|
| 299 |
+
"sync_status": "synced" if db_count == memory_count else "out_of_sync",
|
| 300 |
+
"agent_details": agent_details,
|
| 301 |
+
"colossus_status": self.colossus_connection_status,
|
| 302 |
+
"enhanced_features_active": True
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
except Exception as e:
|
| 306 |
+
logger.error(f"❌ Status check failed: {e}")
|
| 307 |
+
return {"error": str(e)}
|
| 308 |
+
|
| 309 |
+
# Create enhanced global instance
|
| 310 |
+
enhanced_agent_manager = EnhancedAgentManagerService()
|
| 311 |
+
|
| 312 |
+
if __name__ == "__main__":
|
| 313 |
+
async def test_enhanced_agent_manager():
|
| 314 |
+
"""Test enhanced agent manager functionality"""
|
| 315 |
+
manager = EnhancedAgentManagerService()
|
| 316 |
+
await manager.initialize()
|
| 317 |
+
|
| 318 |
+
# Get comprehensive status
|
| 319 |
+
status = await manager.get_comprehensive_agent_status()
|
| 320 |
+
print("📊 Enhanced Agent Manager Status:")
|
| 321 |
+
print(f" Database: {status.get('database_agent_count', 0)} agents")
|
| 322 |
+
print(f" Memory: {status.get('memory_agent_count', 0)} agents")
|
| 323 |
+
print(f" Sync: {status.get('sync_status', 'unknown')}")
|
| 324 |
+
print(f" Enhanced: {status.get('enhanced_features_active', False)}")
|
| 325 |
+
|
| 326 |
+
await manager.shutdown_all_agents()
|
| 327 |
+
|
| 328 |
+
asyncio.run(test_enhanced_agent_manager())
|
backend/agent_manager_enhanced.py
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced SAAP Agent Manager with OpenRouter Integration & Cost Efficiency Logging
|
| 3 |
+
Supports multi-provider architecture: colossus + OpenRouter with cost optimization
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import logging
|
| 8 |
+
import time
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
from typing import Dict, List, Optional, Any, Tuple
|
| 11 |
+
import json
|
| 12 |
+
from dataclasses import dataclass, asdict
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
import aiohttp
|
| 16 |
+
import openai # For OpenRouter compatibility
|
| 17 |
+
from openai import AsyncOpenAI
|
| 18 |
+
|
| 19 |
+
from ..config.settings import get_settings
|
| 20 |
+
from ..models.agent import SaapAgent, AgentStatus, AgentCapability
|
| 21 |
+
from ..models.chat import ChatMessage, MessageRole
|
| 22 |
+
from ..database.service import DatabaseService
|
| 23 |
+
|
| 24 |
+
# Cost efficiency logging
|
| 25 |
+
cost_logger = logging.getLogger("saap.cost")
|
| 26 |
+
performance_logger = logging.getLogger("saap.performance")
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class CostMetrics:
|
| 30 |
+
"""Cost tracking metrics for OpenRouter requests"""
|
| 31 |
+
provider: str
|
| 32 |
+
model: str
|
| 33 |
+
input_tokens: int
|
| 34 |
+
output_tokens: int
|
| 35 |
+
total_tokens: int
|
| 36 |
+
cost_usd: float
|
| 37 |
+
response_time_seconds: float
|
| 38 |
+
timestamp: datetime
|
| 39 |
+
request_success: bool
|
| 40 |
+
agent_id: str
|
| 41 |
+
|
| 42 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 43 |
+
"""Convert to dictionary for logging"""
|
| 44 |
+
return {
|
| 45 |
+
**asdict(self),
|
| 46 |
+
'timestamp': self.timestamp.isoformat(),
|
| 47 |
+
'cost_per_1k_tokens': round(self.cost_usd / (self.total_tokens / 1000), 6) if self.total_tokens > 0 else 0,
|
| 48 |
+
'tokens_per_second': round(self.total_tokens / self.response_time_seconds, 2) if self.response_time_seconds > 0 else 0
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class PerformanceMetrics:
|
| 53 |
+
"""Performance tracking for different providers"""
|
| 54 |
+
provider: str
|
| 55 |
+
model: str
|
| 56 |
+
avg_response_time: float
|
| 57 |
+
success_rate: float
|
| 58 |
+
total_requests: int
|
| 59 |
+
avg_cost_per_request: float
|
| 60 |
+
tokens_per_second: float
|
| 61 |
+
last_24h_cost: float
|
| 62 |
+
|
| 63 |
+
class ProviderType(Enum):
|
| 64 |
+
COLOSSUS = "colossus"
|
| 65 |
+
OPENROUTER = "openrouter"
|
| 66 |
+
HYBRID = "hybrid"
|
| 67 |
+
|
| 68 |
+
class EnhancedAgentManager:
|
| 69 |
+
"""Enhanced Agent Manager with Multi-Provider Support & Cost Optimization"""
|
| 70 |
+
|
| 71 |
+
def __init__(self, database_service: Optional[DatabaseService] = None):
|
| 72 |
+
self.settings = get_settings()
|
| 73 |
+
self.database = database_service
|
| 74 |
+
self.agents: Dict[str, SaapAgent] = {}
|
| 75 |
+
self.agent_clients: Dict[str, Any] = {}
|
| 76 |
+
|
| 77 |
+
# Cost tracking
|
| 78 |
+
self.cost_metrics: List[CostMetrics] = []
|
| 79 |
+
self.daily_cost_budget = self.settings.agents.daily_cost_budget
|
| 80 |
+
self.current_daily_cost = 0.0
|
| 81 |
+
self.cost_alert_threshold = self.settings.agents.warning_cost_threshold
|
| 82 |
+
|
| 83 |
+
# Performance tracking
|
| 84 |
+
self.performance_stats: Dict[str, PerformanceMetrics] = {}
|
| 85 |
+
|
| 86 |
+
# Provider configurations
|
| 87 |
+
self._setup_providers()
|
| 88 |
+
|
| 89 |
+
# Initialize cost logger
|
| 90 |
+
if self.settings.openrouter.enable_cost_tracking:
|
| 91 |
+
cost_logger.info(f"💰 Cost tracking initialized - Daily budget: ${self.daily_cost_budget}")
|
| 92 |
+
cost_logger.info(f"📊 Alert threshold: {self.cost_alert_threshold*100}% of budget")
|
| 93 |
+
|
| 94 |
+
def _setup_providers(self):
|
| 95 |
+
"""Setup provider configurations"""
|
| 96 |
+
self.providers = {}
|
| 97 |
+
|
| 98 |
+
# colossus Configuration
|
| 99 |
+
if self.settings.colossus.api_key:
|
| 100 |
+
self.providers[ProviderType.COLOSSUS] = {
|
| 101 |
+
'client': None, # Will be set up per agent
|
| 102 |
+
'base_url': self.settings.colossus.api_base,
|
| 103 |
+
'api_key': self.settings.colossus.api_key,
|
| 104 |
+
'default_model': self.settings.colossus.default_model,
|
| 105 |
+
'cost_per_1m_tokens': 0.0, # colossus is free
|
| 106 |
+
'timeout': self.settings.colossus.timeout
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# OpenRouter Configuration
|
| 110 |
+
if self.settings.openrouter.enabled and self.settings.openrouter.api_key:
|
| 111 |
+
self.providers[ProviderType.OPENROUTER] = {
|
| 112 |
+
'client': AsyncOpenAI(
|
| 113 |
+
api_key=self.settings.openrouter.api_key,
|
| 114 |
+
base_url=self.settings.openrouter.base_url,
|
| 115 |
+
),
|
| 116 |
+
'base_url': self.settings.openrouter.base_url,
|
| 117 |
+
'api_key': self.settings.openrouter.api_key,
|
| 118 |
+
'models': self._get_openrouter_models(),
|
| 119 |
+
'timeout': 30
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
cost_logger.info(f"🔗 OpenRouter provider initialized with {len(self._get_openrouter_models())} models")
|
| 123 |
+
|
| 124 |
+
def _get_openrouter_models(self) -> Dict[str, Dict[str, Any]]:
|
| 125 |
+
"""Get OpenRouter model configurations with cost data"""
|
| 126 |
+
models = {
|
| 127 |
+
# Jane Alesi - Coordination & Management (GPT-4o-mini)
|
| 128 |
+
'jane_alesi': {
|
| 129 |
+
'model': self.settings.openrouter.jane_model,
|
| 130 |
+
'max_tokens': self.settings.openrouter.jane_max_tokens,
|
| 131 |
+
'temperature': self.settings.openrouter.jane_temperature,
|
| 132 |
+
'cost_per_1m_input': 0.15,
|
| 133 |
+
'cost_per_1m_output': 0.60,
|
| 134 |
+
'description': 'General coordination and management tasks'
|
| 135 |
+
},
|
| 136 |
+
|
| 137 |
+
# John Alesi - Development & Code (Claude-3-Haiku)
|
| 138 |
+
'john_alesi': {
|
| 139 |
+
'model': self.settings.openrouter.john_model,
|
| 140 |
+
'max_tokens': self.settings.openrouter.john_max_tokens,
|
| 141 |
+
'temperature': self.settings.openrouter.john_temperature,
|
| 142 |
+
'cost_per_1m_input': 0.25,
|
| 143 |
+
'cost_per_1m_output': 1.25,
|
| 144 |
+
'description': 'Code generation and development tasks'
|
| 145 |
+
},
|
| 146 |
+
|
| 147 |
+
# Lara Alesi - Medical & Analysis (GPT-4o-mini)
|
| 148 |
+
'lara_alesi': {
|
| 149 |
+
'model': self.settings.openrouter.lara_model,
|
| 150 |
+
'max_tokens': self.settings.openrouter.lara_max_tokens,
|
| 151 |
+
'temperature': self.settings.openrouter.lara_temperature,
|
| 152 |
+
'cost_per_1m_input': 0.15,
|
| 153 |
+
'cost_per_1m_output': 0.60,
|
| 154 |
+
'description': 'Medical analysis and specialized queries'
|
| 155 |
+
},
|
| 156 |
+
|
| 157 |
+
# Fallback Model - Free (Meta Llama)
|
| 158 |
+
'fallback': {
|
| 159 |
+
'model': self.settings.openrouter.fallback_model,
|
| 160 |
+
'max_tokens': 600,
|
| 161 |
+
'temperature': 0.7,
|
| 162 |
+
'cost_per_1m_input': 0.0,
|
| 163 |
+
'cost_per_1m_output': 0.0,
|
| 164 |
+
'description': 'Cost-free fallback for budget limits'
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
return models
|
| 169 |
+
|
| 170 |
+
async def create_agent(self, agent_data: Dict[str, Any]) -> SaapAgent:
|
| 171 |
+
"""Create new agent with provider selection"""
|
| 172 |
+
agent_id = agent_data.get('agent_id', f"agent_{len(self.agents)}")
|
| 173 |
+
|
| 174 |
+
# Determine optimal provider based on agent type and cost considerations
|
| 175 |
+
provider = self._select_optimal_provider(agent_id, agent_data)
|
| 176 |
+
|
| 177 |
+
agent = SaapAgent(
|
| 178 |
+
agent_id=agent_id,
|
| 179 |
+
name=agent_data.get('name', f'Agent {agent_id}'),
|
| 180 |
+
role=agent_data.get('role', 'General Assistant'),
|
| 181 |
+
capabilities=agent_data.get('capabilities', [AgentCapability.CHAT]),
|
| 182 |
+
status=AgentStatus.CREATED,
|
| 183 |
+
llm_config=agent_data.get('llm_config', {}),
|
| 184 |
+
personality=agent_data.get('personality', {}),
|
| 185 |
+
provider=provider.value
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Setup provider-specific client
|
| 189 |
+
await self._setup_agent_client(agent)
|
| 190 |
+
|
| 191 |
+
self.agents[agent_id] = agent
|
| 192 |
+
|
| 193 |
+
# Log agent creation with cost implications
|
| 194 |
+
cost_logger.info(f"🤖 Agent created: {agent_id} using {provider.value} provider")
|
| 195 |
+
if provider == ProviderType.OPENROUTER:
|
| 196 |
+
model_config = self._get_openrouter_models().get(agent_id, self._get_openrouter_models()['fallback'])
|
| 197 |
+
cost_logger.info(f"💰 Model: {model_config['model']} - Cost: ${model_config['cost_per_1m_input']}/1M input tokens")
|
| 198 |
+
|
| 199 |
+
# Database persistence
|
| 200 |
+
if self.database:
|
| 201 |
+
await self.database.create_agent(agent)
|
| 202 |
+
|
| 203 |
+
return agent
|
| 204 |
+
|
| 205 |
+
def _select_optimal_provider(self, agent_id: str, agent_data: Dict[str, Any]) -> ProviderType:
|
| 206 |
+
"""Select optimal provider based on cost and performance requirements"""
|
| 207 |
+
|
| 208 |
+
# Check daily budget constraints
|
| 209 |
+
if self.current_daily_cost >= (self.daily_cost_budget * self.cost_alert_threshold):
|
| 210 |
+
cost_logger.warning(f"⚠️ Daily cost budget at {self.cost_alert_threshold*100}% - using free providers")
|
| 211 |
+
if ProviderType.COLOSSUS in self.providers:
|
| 212 |
+
return ProviderType.COLOSSUS
|
| 213 |
+
|
| 214 |
+
# Agent-specific provider selection
|
| 215 |
+
primary_provider = getattr(self.settings.agents, 'primary_provider', 'colossus')
|
| 216 |
+
|
| 217 |
+
if primary_provider == 'openrouter' and ProviderType.OPENROUTER in self.providers:
|
| 218 |
+
# Use OpenRouter with cost-optimized models
|
| 219 |
+
return ProviderType.OPENROUTER
|
| 220 |
+
elif primary_provider == 'colossus' and ProviderType.COLOSSUS in self.providers:
|
| 221 |
+
# Use colossus as primary (free)
|
| 222 |
+
return ProviderType.COLOSSUS
|
| 223 |
+
else:
|
| 224 |
+
# Fallback to available provider
|
| 225 |
+
if ProviderType.COLOSSUS in self.providers:
|
| 226 |
+
return ProviderType.COLOSSUS
|
| 227 |
+
elif ProviderType.OPENROUTER in self.providers:
|
| 228 |
+
return ProviderType.OPENROUTER
|
| 229 |
+
else:
|
| 230 |
+
raise ValueError("No providers available")
|
| 231 |
+
|
| 232 |
+
async def _setup_agent_client(self, agent: SaapAgent):
|
| 233 |
+
"""Setup provider-specific client for agent"""
|
| 234 |
+
provider_type = ProviderType(agent.provider)
|
| 235 |
+
|
| 236 |
+
if provider_type == ProviderType.COLOSSUS:
|
| 237 |
+
# colossus HTTP client setup (existing logic)
|
| 238 |
+
self.agent_clients[agent.agent_id] = aiohttp.ClientSession(
|
| 239 |
+
timeout=aiohttp.ClientTimeout(total=self.settings.colossus.timeout)
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
elif provider_type == ProviderType.OPENROUTER:
|
| 243 |
+
# OpenRouter client already set up in providers
|
| 244 |
+
self.agent_clients[agent.agent_id] = self.providers[ProviderType.OPENROUTER]['client']
|
| 245 |
+
|
| 246 |
+
async def start_agent(self, agent_id: str) -> bool:
|
| 247 |
+
"""Start agent with provider-specific initialization"""
|
| 248 |
+
if agent_id not in self.agents:
|
| 249 |
+
raise ValueError(f"Agent {agent_id} not found")
|
| 250 |
+
|
| 251 |
+
agent = self.agents[agent_id]
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
# Provider-specific startup
|
| 255 |
+
if agent.provider == ProviderType.COLOSSUS.value:
|
| 256 |
+
success = await self._start_colossus_agent(agent)
|
| 257 |
+
elif agent.provider == ProviderType.OPENROUTER.value:
|
| 258 |
+
success = await self._start_openrouter_agent(agent)
|
| 259 |
+
else:
|
| 260 |
+
success = False
|
| 261 |
+
|
| 262 |
+
if success:
|
| 263 |
+
agent.status = AgentStatus.ACTIVE
|
| 264 |
+
agent.metrics.last_active = datetime.now()
|
| 265 |
+
cost_logger.info(f"✅ Agent {agent_id} started successfully using {agent.provider}")
|
| 266 |
+
|
| 267 |
+
# Database update
|
| 268 |
+
if self.database:
|
| 269 |
+
await self.database.update_agent_status(agent_id, AgentStatus.ACTIVE)
|
| 270 |
+
|
| 271 |
+
return success
|
| 272 |
+
|
| 273 |
+
except Exception as e:
|
| 274 |
+
cost_logger.error(f"❌ Failed to start agent {agent_id}: {str(e)}")
|
| 275 |
+
agent.status = AgentStatus.ERROR
|
| 276 |
+
return False
|
| 277 |
+
|
| 278 |
+
async def _start_colossus_agent(self, agent: SaapAgent) -> bool:
|
| 279 |
+
"""Start colossus agent (existing logic)"""
|
| 280 |
+
try:
|
| 281 |
+
client = self.agent_clients[agent.agent_id]
|
| 282 |
+
|
| 283 |
+
# Test connection to colossus
|
| 284 |
+
async with client.post(
|
| 285 |
+
f"{self.settings.colossus.api_base}/v1/chat/completions",
|
| 286 |
+
headers={"Authorization": f"Bearer {self.settings.colossus.api_key}"},
|
| 287 |
+
json={
|
| 288 |
+
"model": self.settings.colossus.default_model,
|
| 289 |
+
"messages": [{"role": "user", "content": "Test connection"}],
|
| 290 |
+
"max_tokens": 10
|
| 291 |
+
}
|
| 292 |
+
) as response:
|
| 293 |
+
return response.status == 200
|
| 294 |
+
|
| 295 |
+
except Exception as e:
|
| 296 |
+
cost_logger.error(f"colossus connection failed for {agent.agent_id}: {str(e)}")
|
| 297 |
+
return False
|
| 298 |
+
|
| 299 |
+
async def _start_openrouter_agent(self, agent: SaapAgent) -> bool:
|
| 300 |
+
"""Start OpenRouter agent"""
|
| 301 |
+
try:
|
| 302 |
+
client = self.agent_clients[agent.agent_id]
|
| 303 |
+
model_config = self._get_openrouter_models().get(agent.agent_id, self._get_openrouter_models()['fallback'])
|
| 304 |
+
|
| 305 |
+
# Test connection to OpenRouter
|
| 306 |
+
response = await client.chat.completions.create(
|
| 307 |
+
model=model_config['model'],
|
| 308 |
+
messages=[{"role": "user", "content": "Test connection"}],
|
| 309 |
+
max_tokens=10,
|
| 310 |
+
temperature=model_config['temperature']
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
cost_logger.info(f"🔗 OpenRouter agent {agent.agent_id} connected - Model: {model_config['model']}")
|
| 314 |
+
return True
|
| 315 |
+
|
| 316 |
+
except Exception as e:
|
| 317 |
+
cost_logger.error(f"OpenRouter connection failed for {agent.agent_id}: {str(e)}")
|
| 318 |
+
return False
|
| 319 |
+
|
| 320 |
+
async def send_message(self, agent_id: str, message: str, conversation_context: Optional[List[Dict]] = None) -> ChatMessage:
|
| 321 |
+
"""Send message to agent with cost tracking"""
|
| 322 |
+
if agent_id not in self.agents:
|
| 323 |
+
raise ValueError(f"Agent {agent_id} not found")
|
| 324 |
+
|
| 325 |
+
agent = self.agents[agent_id]
|
| 326 |
+
start_time = time.time()
|
| 327 |
+
|
| 328 |
+
try:
|
| 329 |
+
if agent.provider == ProviderType.COLOSSUS.value:
|
| 330 |
+
response = await self._send_colossus_message(agent, message, conversation_context)
|
| 331 |
+
elif agent.provider == ProviderType.OPENROUTER.value:
|
| 332 |
+
response = await self._send_openrouter_message(agent, message, conversation_context)
|
| 333 |
+
else:
|
| 334 |
+
raise ValueError(f"Unsupported provider: {agent.provider}")
|
| 335 |
+
|
| 336 |
+
response_time = time.time() - start_time
|
| 337 |
+
|
| 338 |
+
# Update agent metrics
|
| 339 |
+
agent.metrics.messages_processed += 1
|
| 340 |
+
agent.metrics.last_active = datetime.now()
|
| 341 |
+
agent.metrics.avg_response_time = (
|
| 342 |
+
(agent.metrics.avg_response_time * (agent.metrics.messages_processed - 1) + response_time)
|
| 343 |
+
/ agent.metrics.messages_processed
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
# Log performance
|
| 347 |
+
performance_logger.info(f"📊 Agent {agent_id} - Response time: {response_time:.2f}s, Total messages: {agent.metrics.messages_processed}")
|
| 348 |
+
|
| 349 |
+
return response
|
| 350 |
+
|
| 351 |
+
except Exception as e:
|
| 352 |
+
cost_logger.error(f"❌ Message failed for agent {agent_id}: {str(e)}")
|
| 353 |
+
agent.status = AgentStatus.ERROR
|
| 354 |
+
raise
|
| 355 |
+
|
| 356 |
+
async def _send_colossus_message(self, agent: SaapAgent, message: str, context: Optional[List[Dict]] = None) -> ChatMessage:
|
| 357 |
+
"""Send message via colossus (existing logic with cost tracking)"""
|
| 358 |
+
client = self.agent_clients[agent.agent_id]
|
| 359 |
+
|
| 360 |
+
# Prepare messages
|
| 361 |
+
messages = []
|
| 362 |
+
if context:
|
| 363 |
+
messages.extend(context)
|
| 364 |
+
messages.append({"role": "user", "content": message})
|
| 365 |
+
|
| 366 |
+
start_time = time.time()
|
| 367 |
+
|
| 368 |
+
try:
|
| 369 |
+
async with client.post(
|
| 370 |
+
f"{self.settings.colossus.api_base}/v1/chat/completions",
|
| 371 |
+
headers={"Authorization": f"Bearer {self.settings.colossus.api_key}"},
|
| 372 |
+
json={
|
| 373 |
+
"model": self.settings.colossus.default_model,
|
| 374 |
+
"messages": messages,
|
| 375 |
+
"max_tokens": 800,
|
| 376 |
+
"temperature": 0.7
|
| 377 |
+
}
|
| 378 |
+
) as response:
|
| 379 |
+
if response.status != 200:
|
| 380 |
+
raise Exception(f"colossus API error: {response.status}")
|
| 381 |
+
|
| 382 |
+
data = await response.json()
|
| 383 |
+
content = data['choices'][0]['message']['content']
|
| 384 |
+
response_time = time.time() - start_time
|
| 385 |
+
|
| 386 |
+
# Cost tracking for colossus (free)
|
| 387 |
+
cost_metrics = CostMetrics(
|
| 388 |
+
provider="colossus",
|
| 389 |
+
model=self.settings.colossus.default_model,
|
| 390 |
+
input_tokens=data.get('usage', {}).get('prompt_tokens', 0),
|
| 391 |
+
output_tokens=data.get('usage', {}).get('completion_tokens', 0),
|
| 392 |
+
total_tokens=data.get('usage', {}).get('total_tokens', 0),
|
| 393 |
+
cost_usd=0.0, # colossus is free
|
| 394 |
+
response_time_seconds=response_time,
|
| 395 |
+
timestamp=datetime.now(),
|
| 396 |
+
request_success=True,
|
| 397 |
+
agent_id=agent.agent_id
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
self._log_cost_metrics(cost_metrics)
|
| 401 |
+
|
| 402 |
+
return ChatMessage(
|
| 403 |
+
message_id=f"msg_{int(time.time())}",
|
| 404 |
+
role=MessageRole.ASSISTANT,
|
| 405 |
+
content=content,
|
| 406 |
+
timestamp=datetime.now(),
|
| 407 |
+
agent_id=agent.agent_id,
|
| 408 |
+
metadata={'provider': 'colossus', 'cost_usd': 0.0, 'tokens': cost_metrics.total_tokens}
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
except Exception as e:
|
| 412 |
+
# Log failed request
|
| 413 |
+
cost_metrics = CostMetrics(
|
| 414 |
+
provider="colossus",
|
| 415 |
+
model=self.settings.colossus.default_model,
|
| 416 |
+
input_tokens=0,
|
| 417 |
+
output_tokens=0,
|
| 418 |
+
total_tokens=0,
|
| 419 |
+
cost_usd=0.0,
|
| 420 |
+
response_time_seconds=time.time() - start_time,
|
| 421 |
+
timestamp=datetime.now(),
|
| 422 |
+
request_success=False,
|
| 423 |
+
agent_id=agent.agent_id
|
| 424 |
+
)
|
| 425 |
+
self._log_cost_metrics(cost_metrics)
|
| 426 |
+
raise
|
| 427 |
+
|
| 428 |
+
async def _send_openrouter_message(self, agent: SaapAgent, message: str, context: Optional[List[Dict]] = None) -> ChatMessage:
|
| 429 |
+
"""Send message via OpenRouter with cost tracking"""
|
| 430 |
+
client = self.agent_clients[agent.agent_id]
|
| 431 |
+
model_config = self._get_openrouter_models().get(agent.agent_id, self._get_openrouter_models()['fallback'])
|
| 432 |
+
|
| 433 |
+
# Check budget before expensive request
|
| 434 |
+
estimated_cost = self._estimate_request_cost(message, model_config)
|
| 435 |
+
if self.current_daily_cost + estimated_cost > self.daily_cost_budget:
|
| 436 |
+
cost_logger.warning(f"💸 Daily budget exceeded - switching to free fallback model")
|
| 437 |
+
model_config = self._get_openrouter_models()['fallback']
|
| 438 |
+
|
| 439 |
+
# Prepare messages
|
| 440 |
+
messages = []
|
| 441 |
+
if context:
|
| 442 |
+
messages.extend(context)
|
| 443 |
+
messages.append({"role": "user", "content": message})
|
| 444 |
+
|
| 445 |
+
start_time = time.time()
|
| 446 |
+
|
| 447 |
+
try:
|
| 448 |
+
response = await client.chat.completions.create(
|
| 449 |
+
model=model_config['model'],
|
| 450 |
+
messages=messages,
|
| 451 |
+
max_tokens=model_config['max_tokens'],
|
| 452 |
+
temperature=model_config['temperature']
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
response_time = time.time() - start_time
|
| 456 |
+
content = response.choices[0].message.content
|
| 457 |
+
|
| 458 |
+
# Calculate actual cost
|
| 459 |
+
input_tokens = response.usage.prompt_tokens
|
| 460 |
+
output_tokens = response.usage.completion_tokens
|
| 461 |
+
total_tokens = response.usage.total_tokens
|
| 462 |
+
|
| 463 |
+
cost_usd = (
|
| 464 |
+
(input_tokens / 1_000_000) * model_config['cost_per_1m_input'] +
|
| 465 |
+
(output_tokens / 1_000_000) * model_config['cost_per_1m_output']
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
# Update daily cost tracking
|
| 469 |
+
self.current_daily_cost += cost_usd
|
| 470 |
+
|
| 471 |
+
# Cost tracking
|
| 472 |
+
cost_metrics = CostMetrics(
|
| 473 |
+
provider="openrouter",
|
| 474 |
+
model=model_config['model'],
|
| 475 |
+
input_tokens=input_tokens,
|
| 476 |
+
output_tokens=output_tokens,
|
| 477 |
+
total_tokens=total_tokens,
|
| 478 |
+
cost_usd=cost_usd,
|
| 479 |
+
response_time_seconds=response_time,
|
| 480 |
+
timestamp=datetime.now(),
|
| 481 |
+
request_success=True,
|
| 482 |
+
agent_id=agent.agent_id
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
self._log_cost_metrics(cost_metrics)
|
| 486 |
+
|
| 487 |
+
# Budget alert check
|
| 488 |
+
if self.current_daily_cost >= (self.daily_cost_budget * self.cost_alert_threshold):
|
| 489 |
+
cost_logger.warning(f"⚠️ Cost alert: ${self.current_daily_cost:.4f} / ${self.daily_cost_budget} ({self.current_daily_cost/self.daily_cost_budget*100:.1f}%)")
|
| 490 |
+
|
| 491 |
+
return ChatMessage(
|
| 492 |
+
message_id=f"msg_{int(time.time())}",
|
| 493 |
+
role=MessageRole.ASSISTANT,
|
| 494 |
+
content=content,
|
| 495 |
+
timestamp=datetime.now(),
|
| 496 |
+
agent_id=agent.agent_id,
|
| 497 |
+
metadata={
|
| 498 |
+
'provider': 'openrouter',
|
| 499 |
+
'model': model_config['model'],
|
| 500 |
+
'cost_usd': cost_usd,
|
| 501 |
+
'tokens': total_tokens,
|
| 502 |
+
'cost_efficiency': f"${cost_usd:.6f} ({total_tokens} tokens, {response_time:.1f}s)"
|
| 503 |
+
}
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
except Exception as e:
|
| 507 |
+
# Log failed request
|
| 508 |
+
cost_metrics = CostMetrics(
|
| 509 |
+
provider="openrouter",
|
| 510 |
+
model=model_config['model'],
|
| 511 |
+
input_tokens=0,
|
| 512 |
+
output_tokens=0,
|
| 513 |
+
total_tokens=0,
|
| 514 |
+
cost_usd=0.0,
|
| 515 |
+
response_time_seconds=time.time() - start_time,
|
| 516 |
+
timestamp=datetime.now(),
|
| 517 |
+
request_success=False,
|
| 518 |
+
agent_id=agent.agent_id
|
| 519 |
+
)
|
| 520 |
+
self._log_cost_metrics(cost_metrics)
|
| 521 |
+
raise
|
| 522 |
+
|
| 523 |
+
def _estimate_request_cost(self, message: str, model_config: Dict[str, Any]) -> float:
|
| 524 |
+
"""Estimate cost for request (rough approximation)"""
|
| 525 |
+
# Rough token estimation: ~4 chars per token
|
| 526 |
+
estimated_input_tokens = len(message) / 4
|
| 527 |
+
estimated_output_tokens = model_config['max_tokens'] * 0.5 # Assume 50% of max tokens
|
| 528 |
+
|
| 529 |
+
cost_usd = (
|
| 530 |
+
(estimated_input_tokens / 1_000_000) * model_config['cost_per_1m_input'] +
|
| 531 |
+
(estimated_output_tokens / 1_000_000) * model_config['cost_per_1m_output']
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
return cost_usd
|
| 535 |
+
|
| 536 |
+
def _log_cost_metrics(self, metrics: CostMetrics):
|
| 537 |
+
"""Log cost metrics for analysis"""
|
| 538 |
+
self.cost_metrics.append(metrics)
|
| 539 |
+
|
| 540 |
+
# Detailed cost logging
|
| 541 |
+
cost_logger.info(f"💰 Cost Metrics: {json.dumps(metrics.to_dict())}")
|
| 542 |
+
|
| 543 |
+
# Performance summary
|
| 544 |
+
if metrics.request_success:
|
| 545 |
+
efficiency = f"{metrics.total_tokens/metrics.response_time_seconds:.1f}" if metrics.response_time_seconds > 0 else "N/A"
|
| 546 |
+
cost_logger.info(f"📊 {metrics.agent_id} - ${metrics.cost_usd:.6f} | {metrics.total_tokens} tokens | {metrics.response_time_seconds:.2f}s | {efficiency} tokens/s")
|
| 547 |
+
|
| 548 |
+
# Cleanup old metrics (keep last 1000)
|
| 549 |
+
if len(self.cost_metrics) > 1000:
|
| 550 |
+
self.cost_metrics = self.cost_metrics[-1000:]
|
| 551 |
+
|
| 552 |
+
def get_cost_summary(self, hours: int = 24) -> Dict[str, Any]:
|
| 553 |
+
"""Get cost summary for specified time period"""
|
| 554 |
+
cutoff_time = datetime.now() - timedelta(hours=hours)
|
| 555 |
+
recent_metrics = [m for m in self.cost_metrics if m.timestamp >= cutoff_time]
|
| 556 |
+
|
| 557 |
+
if not recent_metrics:
|
| 558 |
+
return {"total_cost": 0.0, "total_requests": 0, "average_cost_per_request": 0.0}
|
| 559 |
+
|
| 560 |
+
total_cost = sum(m.cost_usd for m in recent_metrics)
|
| 561 |
+
successful_requests = [m for m in recent_metrics if m.request_success]
|
| 562 |
+
|
| 563 |
+
by_provider = {}
|
| 564 |
+
for metrics in recent_metrics:
|
| 565 |
+
if metrics.provider not in by_provider:
|
| 566 |
+
by_provider[metrics.provider] = {"cost": 0.0, "requests": 0, "tokens": 0}
|
| 567 |
+
by_provider[metrics.provider]["cost"] += metrics.cost_usd
|
| 568 |
+
by_provider[metrics.provider]["requests"] += 1
|
| 569 |
+
by_provider[metrics.provider]["tokens"] += metrics.total_tokens
|
| 570 |
+
|
| 571 |
+
return {
|
| 572 |
+
"total_cost_usd": round(total_cost, 4),
|
| 573 |
+
"total_requests": len(recent_metrics),
|
| 574 |
+
"successful_requests": len(successful_requests),
|
| 575 |
+
"success_rate": len(successful_requests) / len(recent_metrics) if recent_metrics else 0,
|
| 576 |
+
"average_cost_per_request": round(total_cost / len(recent_metrics), 6) if recent_metrics else 0,
|
| 577 |
+
"daily_budget_used": round(self.current_daily_cost / self.daily_cost_budget * 100, 1),
|
| 578 |
+
"by_provider": by_provider,
|
| 579 |
+
"period_hours": hours,
|
| 580 |
+
"budget_remaining_usd": max(0, self.daily_cost_budget - self.current_daily_cost)
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
async def get_all_agents(self) -> List[SaapAgent]:
|
| 584 |
+
"""Get all agents with current status"""
|
| 585 |
+
return list(self.agents.values())
|
| 586 |
+
|
| 587 |
+
async def get_agent(self, agent_id: str) -> Optional[SaapAgent]:
|
| 588 |
+
"""Get specific agent"""
|
| 589 |
+
return self.agents.get(agent_id)
|
| 590 |
+
|
| 591 |
+
async def stop_agent(self, agent_id: str) -> bool:
|
| 592 |
+
"""Stop agent and cleanup resources"""
|
| 593 |
+
if agent_id not in self.agents:
|
| 594 |
+
return False
|
| 595 |
+
|
| 596 |
+
agent = self.agents[agent_id]
|
| 597 |
+
agent.status = AgentStatus.INACTIVE
|
| 598 |
+
|
| 599 |
+
# Cleanup client connection
|
| 600 |
+
if agent_id in self.agent_clients:
|
| 601 |
+
client = self.agent_clients[agent_id]
|
| 602 |
+
if hasattr(client, 'close'):
|
| 603 |
+
if asyncio.iscoroutinefunction(client.close):
|
| 604 |
+
await client.close()
|
| 605 |
+
else:
|
| 606 |
+
client.close()
|
| 607 |
+
del self.agent_clients[agent_id]
|
| 608 |
+
|
| 609 |
+
cost_logger.info(f"🛑 Agent {agent_id} stopped")
|
| 610 |
+
|
| 611 |
+
# Database update
|
| 612 |
+
if self.database:
|
| 613 |
+
await self.database.update_agent_status(agent_id, AgentStatus.INACTIVE)
|
| 614 |
+
|
| 615 |
+
return True
|
| 616 |
+
|
| 617 |
+
async def delete_agent(self, agent_id: str) -> bool:
|
| 618 |
+
"""Delete agent completely"""
|
| 619 |
+
if agent_id not in self.agents:
|
| 620 |
+
return False
|
| 621 |
+
|
| 622 |
+
# Stop agent first
|
| 623 |
+
await self.stop_agent(agent_id)
|
| 624 |
+
|
| 625 |
+
# Remove from memory
|
| 626 |
+
del self.agents[agent_id]
|
| 627 |
+
|
| 628 |
+
cost_logger.info(f"🗑️ Agent {agent_id} deleted")
|
| 629 |
+
|
| 630 |
+
# Database deletion
|
| 631 |
+
if self.database:
|
| 632 |
+
await self.database.delete_agent(agent_id)
|
| 633 |
+
|
| 634 |
+
return True
|
| 635 |
+
|
| 636 |
+
def reset_daily_costs(self):
|
| 637 |
+
"""Reset daily cost tracking (called at midnight)"""
|
| 638 |
+
yesterday_cost = self.current_daily_cost
|
| 639 |
+
self.current_daily_cost = 0.0
|
| 640 |
+
|
| 641 |
+
cost_logger.info(f"📅 Daily cost reset - Yesterday: ${yesterday_cost:.4f}")
|
| 642 |
+
|
| 643 |
+
async def __aenter__(self):
|
| 644 |
+
"""Async context manager entry"""
|
| 645 |
+
return self
|
| 646 |
+
|
| 647 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 648 |
+
"""Async context manager exit - cleanup all resources"""
|
| 649 |
+
for agent_id in list(self.agent_clients.keys()):
|
| 650 |
+
await self.stop_agent(agent_id)
|
| 651 |
+
cost_logger.info("🧹 Enhanced Agent Manager cleanup completed")
|
backend/agent_manager_fixed.py
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🔧 CRITICAL FIX: Agent Settings/Update Problem - Data Type Mismatch Resolved
|
| 3 |
+
Fixed both AgentManagerService.update_agent() method signature issues
|
| 4 |
+
|
| 5 |
+
PROBLEM SOLVED:
|
| 6 |
+
- ❌ 'dict' object has no attribute 'id' → ✅ Proper data conversion
|
| 7 |
+
- ❌ 'dict' object has no attribute 'name' → ✅ SaapAgent object handling
|
| 8 |
+
- ❌ Agent Settings Modal fails → ✅ Frontend-Backend compatibility
|
| 9 |
+
|
| 10 |
+
This fixes the Agent Settings/Update functionality completely.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import asyncio
|
| 14 |
+
import logging
|
| 15 |
+
import os
|
| 16 |
+
from typing import Dict, List, Optional, Any, Union
|
| 17 |
+
from datetime import datetime
|
| 18 |
+
import uuid
|
| 19 |
+
|
| 20 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 21 |
+
from sqlalchemy import select, update, delete
|
| 22 |
+
|
| 23 |
+
from models.agent import SaapAgent, AgentStatus, AgentType, AgentTemplates
|
| 24 |
+
from database.connection import db_manager
|
| 25 |
+
from database.models import DBAgent, DBChatMessage, DBAgentSession
|
| 26 |
+
from api.colossus_client import ColossusClient
|
| 27 |
+
from agents.openrouter_saap_agent import OpenRouterSAAPAgent
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
class AgentManagerService:
|
| 32 |
+
"""
|
| 33 |
+
🔧 CRITICAL FIX: Production-ready Agent Manager with Agent Update Fix
|
| 34 |
+
|
| 35 |
+
FIXED ISSUES:
|
| 36 |
+
1. ✅ update_agent() now accepts both Dict and SaapAgent objects
|
| 37 |
+
2. ✅ Proper data type conversion for Frontend→Backend compatibility
|
| 38 |
+
3. ✅ Agent Settings Modal now works without AttributeError
|
| 39 |
+
4. ✅ Maintains backward compatibility with existing code
|
| 40 |
+
|
| 41 |
+
Features:
|
| 42 |
+
- Database-backed agent storage and lifecycle
|
| 43 |
+
- Real-time agent status management
|
| 44 |
+
- colossus LLM integration with OpenRouter fallback
|
| 45 |
+
- Session tracking and performance metrics
|
| 46 |
+
- Health monitoring and error handling
|
| 47 |
+
- Multi-provider chat support (colossus + OpenRouter)
|
| 48 |
+
- ✅ Robust LLM config access preventing AttributeError
|
| 49 |
+
- ✅ Fixed Agent Update/Settings functionality
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self):
|
| 53 |
+
self.agents: Dict[str, SaapAgent] = {} # In-memory cache for fast access
|
| 54 |
+
self.active_sessions: Dict[str, DBAgentSession] = {}
|
| 55 |
+
self.colossus_client: Optional[ColossusClient] = None
|
| 56 |
+
self.is_initialized = False
|
| 57 |
+
self.colossus_connection_status = "unknown"
|
| 58 |
+
self.last_colossus_test = None
|
| 59 |
+
|
| 60 |
+
def _get_llm_config_value(self, agent: SaapAgent, key: str, default=None):
|
| 61 |
+
"""
|
| 62 |
+
🔧 CRITICAL FIX: Safe LLM config access preventing 'get' attribute errors
|
| 63 |
+
|
| 64 |
+
This is the same fix applied to HybridAgentManagerService but now in the base class.
|
| 65 |
+
Handles dictionary, object, and Pydantic model configurations robustly.
|
| 66 |
+
|
| 67 |
+
Resolves: 'LLMModelConfig' object has no attribute 'get'
|
| 68 |
+
"""
|
| 69 |
+
try:
|
| 70 |
+
if not hasattr(agent, 'llm_config') or not agent.llm_config:
|
| 71 |
+
logger.debug(f"Agent {agent.id} has no llm_config, using default: {default}")
|
| 72 |
+
return default
|
| 73 |
+
|
| 74 |
+
llm_config = agent.llm_config
|
| 75 |
+
|
| 76 |
+
# Case 1: Dictionary-based config (Frontend JSON)
|
| 77 |
+
if isinstance(llm_config, dict):
|
| 78 |
+
value = llm_config.get(key, default)
|
| 79 |
+
logger.debug(f"✅ Dict config access: {key}={value}")
|
| 80 |
+
return value
|
| 81 |
+
|
| 82 |
+
# Case 2: Object with direct attribute access (Pydantic models)
|
| 83 |
+
elif hasattr(llm_config, key):
|
| 84 |
+
value = getattr(llm_config, key, default)
|
| 85 |
+
logger.debug(f"✅ Attribute access: {key}={value}")
|
| 86 |
+
return value
|
| 87 |
+
|
| 88 |
+
# Case 3: Object with get() method (dict-like objects or fixed Pydantic)
|
| 89 |
+
elif hasattr(llm_config, 'get') and callable(getattr(llm_config, 'get')):
|
| 90 |
+
try:
|
| 91 |
+
value = llm_config.get(key, default)
|
| 92 |
+
logger.debug(f"✅ Method get() access: {key}={value}")
|
| 93 |
+
return value
|
| 94 |
+
except Exception as get_error:
|
| 95 |
+
logger.warning(f"⚠️ get() method failed: {get_error}, trying fallback")
|
| 96 |
+
|
| 97 |
+
# Case 4: Convert object to dict (Pydantic → dict)
|
| 98 |
+
elif hasattr(llm_config, '__dict__'):
|
| 99 |
+
config_dict = llm_config.__dict__
|
| 100 |
+
if key in config_dict:
|
| 101 |
+
value = config_dict[key]
|
| 102 |
+
logger.debug(f"✅ __dict__ access: {key}={value}")
|
| 103 |
+
return value
|
| 104 |
+
|
| 105 |
+
# Case 5: Try model_dump() for Pydantic v2
|
| 106 |
+
elif hasattr(llm_config, 'model_dump'):
|
| 107 |
+
try:
|
| 108 |
+
config_dict = llm_config.model_dump()
|
| 109 |
+
value = config_dict.get(key, default)
|
| 110 |
+
logger.debug(f"✅ model_dump() access: {key}={value}")
|
| 111 |
+
return value
|
| 112 |
+
except Exception:
|
| 113 |
+
pass
|
| 114 |
+
|
| 115 |
+
# Case 6: Try dict() conversion
|
| 116 |
+
elif hasattr(llm_config, 'dict'):
|
| 117 |
+
try:
|
| 118 |
+
config_dict = llm_config.dict()
|
| 119 |
+
value = config_dict.get(key, default)
|
| 120 |
+
logger.debug(f"✅ dict() access: {key}={value}")
|
| 121 |
+
return value
|
| 122 |
+
except Exception:
|
| 123 |
+
pass
|
| 124 |
+
|
| 125 |
+
# Final fallback
|
| 126 |
+
logger.warning(f"⚠️ Unknown config type {type(llm_config)} for {key}, using default: {default}")
|
| 127 |
+
return default
|
| 128 |
+
|
| 129 |
+
except AttributeError as e:
|
| 130 |
+
logger.warning(f"⚠️ AttributeError in LLM config access for {key}: {e}, using default: {default}")
|
| 131 |
+
return default
|
| 132 |
+
except Exception as e:
|
| 133 |
+
logger.error(f"❌ Unexpected error in LLM config access for {key}: {e}, using default: {default}")
|
| 134 |
+
return default
|
| 135 |
+
|
| 136 |
+
async def initialize(self):
|
| 137 |
+
"""Initialize agent manager with database and colossus connection"""
|
| 138 |
+
try:
|
| 139 |
+
logger.info("🚀 Initializing Agent Manager Service...")
|
| 140 |
+
|
| 141 |
+
# Initialize colossus client with better error handling
|
| 142 |
+
try:
|
| 143 |
+
logger.info("🔌 Connecting to colossus server...")
|
| 144 |
+
self.colossus_client = ColossusClient()
|
| 145 |
+
await self.colossus_client.__aenter__()
|
| 146 |
+
|
| 147 |
+
# Test colossus connection
|
| 148 |
+
await self._test_colossus_connection()
|
| 149 |
+
|
| 150 |
+
except Exception as colossus_error:
|
| 151 |
+
logger.error(f"❌ colossus connection failed: {colossus_error}")
|
| 152 |
+
self.colossus_connection_status = f"failed: {str(colossus_error)}"
|
| 153 |
+
# Continue initialization without colossus (graceful degradation)
|
| 154 |
+
|
| 155 |
+
# Try to load agents from database (graceful fallback if db not ready)
|
| 156 |
+
await self._load_agents_from_database()
|
| 157 |
+
|
| 158 |
+
# Load default agents if database is empty or not available
|
| 159 |
+
if not self.agents:
|
| 160 |
+
await self.load_default_agents()
|
| 161 |
+
|
| 162 |
+
self.is_initialized = True
|
| 163 |
+
logger.info(f"✅ Agent Manager initialized: {len(self.agents)} agents loaded")
|
| 164 |
+
logger.info(f"🔌 colossus status: {self.colossus_connection_status}")
|
| 165 |
+
|
| 166 |
+
except Exception as e:
|
| 167 |
+
logger.error(f"❌ Agent Manager initialization failed: {e}")
|
| 168 |
+
raise
|
| 169 |
+
|
| 170 |
+
async def _test_colossus_connection(self):
|
| 171 |
+
"""Test colossus connection and update status"""
|
| 172 |
+
try:
|
| 173 |
+
if not self.colossus_client:
|
| 174 |
+
self.colossus_connection_status = "client_not_initialized"
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
# Send a simple test message
|
| 178 |
+
test_messages = [
|
| 179 |
+
{"role": "system", "content": "You are a test assistant."},
|
| 180 |
+
{"role": "user", "content": "Reply with just 'OK' to confirm connection."}
|
| 181 |
+
]
|
| 182 |
+
|
| 183 |
+
logger.info("🧪 Testing colossus connection...")
|
| 184 |
+
response = await self.colossus_client.chat_completion(
|
| 185 |
+
messages=test_messages,
|
| 186 |
+
agent_id="connection_test",
|
| 187 |
+
max_tokens=10
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
if response and response.get("success"):
|
| 191 |
+
self.colossus_connection_status = "connected"
|
| 192 |
+
self.last_colossus_test = datetime.utcnow()
|
| 193 |
+
logger.info("✅ colossus connection test successful")
|
| 194 |
+
else:
|
| 195 |
+
error_msg = response.get("error", "unknown error") if response else "no response"
|
| 196 |
+
self.colossus_connection_status = f"test_failed: {error_msg}"
|
| 197 |
+
logger.error(f"❌ colossus connection test failed: {error_msg}")
|
| 198 |
+
|
| 199 |
+
except Exception as e:
|
| 200 |
+
self.colossus_connection_status = f"test_error: {str(e)}"
|
| 201 |
+
logger.error(f"❌ colossus connection test error: {e}")
|
| 202 |
+
|
| 203 |
+
async def _load_agents_from_database(self):
|
| 204 |
+
"""Load all agents from database into memory cache"""
|
| 205 |
+
try:
|
| 206 |
+
# Check if database manager is ready
|
| 207 |
+
if not db_manager.is_initialized:
|
| 208 |
+
logger.warning("⚠️ Database not yet initialized - will load default agents")
|
| 209 |
+
return
|
| 210 |
+
|
| 211 |
+
async with db_manager.get_async_session() as session:
|
| 212 |
+
result = await session.execute(select(DBAgent))
|
| 213 |
+
db_agents = result.scalars().all()
|
| 214 |
+
|
| 215 |
+
for db_agent in db_agents:
|
| 216 |
+
saap_agent = db_agent.to_saap_agent()
|
| 217 |
+
self.agents[saap_agent.id] = saap_agent
|
| 218 |
+
|
| 219 |
+
logger.info(f"📚 Loaded {len(db_agents)} agents from database")
|
| 220 |
+
|
| 221 |
+
except Exception as e:
|
| 222 |
+
logger.error(f"❌ Failed to load agents from database: {e}")
|
| 223 |
+
# Don't raise - allow service to start with empty agent list
|
| 224 |
+
logger.info("📦 Will proceed with in-memory agents only")
|
| 225 |
+
|
| 226 |
+
async def load_default_agents(self):
|
| 227 |
+
"""Load default Alesi agents (Jane, John, Lara)"""
|
| 228 |
+
try:
|
| 229 |
+
logger.info("🤖 Loading default Alesi agents...")
|
| 230 |
+
|
| 231 |
+
default_agents = [
|
| 232 |
+
AgentTemplates.jane_alesi(),
|
| 233 |
+
AgentTemplates.john_alesi(),
|
| 234 |
+
AgentTemplates.lara_alesi()
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
for agent in default_agents:
|
| 238 |
+
await self.register_agent(agent)
|
| 239 |
+
|
| 240 |
+
logger.info(f"✅ Default agents loaded: {[a.name for a in default_agents]}")
|
| 241 |
+
|
| 242 |
+
except Exception as e:
|
| 243 |
+
logger.error(f"❌ Agent registration failed: {e}")
|
| 244 |
+
|
| 245 |
+
async def register_agent(self, agent: SaapAgent) -> bool:
|
| 246 |
+
"""Register new agent with database persistence"""
|
| 247 |
+
try:
|
| 248 |
+
# Always add to memory cache first
|
| 249 |
+
self.agents[agent.id] = agent
|
| 250 |
+
|
| 251 |
+
# Try to persist to database if available
|
| 252 |
+
try:
|
| 253 |
+
if db_manager.is_initialized:
|
| 254 |
+
async with db_manager.get_async_session() as session:
|
| 255 |
+
db_agent = DBAgent.from_saap_agent(agent)
|
| 256 |
+
session.add(db_agent)
|
| 257 |
+
await session.commit()
|
| 258 |
+
logger.info(f"✅ Agent registered with database: {agent.name} ({agent.id})")
|
| 259 |
+
else:
|
| 260 |
+
logger.info(f"✅ Agent registered in-memory only: {agent.name} ({agent.id})")
|
| 261 |
+
|
| 262 |
+
except Exception as db_error:
|
| 263 |
+
logger.warning(f"⚠️ Database persistence failed for {agent.name}: {db_error}")
|
| 264 |
+
# But keep the agent in memory
|
| 265 |
+
|
| 266 |
+
return True
|
| 267 |
+
|
| 268 |
+
except Exception as e:
|
| 269 |
+
logger.error(f"❌ Agent registration failed: {e}")
|
| 270 |
+
# Remove from cache if registration completely failed
|
| 271 |
+
self.agents.pop(agent.id, None)
|
| 272 |
+
return False
|
| 273 |
+
|
| 274 |
+
def get_agent(self, agent_id: str) -> Optional[SaapAgent]:
|
| 275 |
+
"""Get agent from memory cache with debug info"""
|
| 276 |
+
agent = self.agents.get(agent_id)
|
| 277 |
+
if agent:
|
| 278 |
+
logger.debug(f"🔍 Agent found: {agent.name} ({agent_id}) - Status: {agent.status}")
|
| 279 |
+
else:
|
| 280 |
+
logger.warning(f"❌ Agent not found: {agent_id}")
|
| 281 |
+
logger.debug(f"📋 Available agents: {list(self.agents.keys())}")
|
| 282 |
+
return agent
|
| 283 |
+
|
| 284 |
+
async def list_agents(self, status: Optional[AgentStatus] = None,
|
| 285 |
+
agent_type: Optional[AgentType] = None) -> List[SaapAgent]:
|
| 286 |
+
"""List all agents with optional filtering"""
|
| 287 |
+
agents = list(self.agents.values())
|
| 288 |
+
|
| 289 |
+
if status:
|
| 290 |
+
agents = [a for a in agents if a.status == status]
|
| 291 |
+
|
| 292 |
+
if agent_type:
|
| 293 |
+
agents = [a for a in agents if a.type == agent_type]
|
| 294 |
+
|
| 295 |
+
return agents
|
| 296 |
+
|
| 297 |
+
async def get_agent_stats(self, agent_id: str) -> Dict[str, Any]:
|
| 298 |
+
"""Get agent statistics"""
|
| 299 |
+
agent = self.get_agent(agent_id)
|
| 300 |
+
if not agent:
|
| 301 |
+
return {}
|
| 302 |
+
|
| 303 |
+
# Return basic stats from agent object
|
| 304 |
+
return {
|
| 305 |
+
"messages_processed": getattr(agent, 'messages_processed', 0),
|
| 306 |
+
"total_tokens": getattr(agent, 'total_tokens', 0),
|
| 307 |
+
"average_response_time": getattr(agent, 'avg_response_time', 0),
|
| 308 |
+
"status": agent.status.value,
|
| 309 |
+
"last_active": getattr(agent, 'last_active', None)
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
async def health_check(self, agent_id: str) -> Dict[str, Any]:
|
| 313 |
+
"""Perform agent health check"""
|
| 314 |
+
agent = self.get_agent(agent_id)
|
| 315 |
+
if not agent:
|
| 316 |
+
return {"healthy": False, "checks": {"agent_exists": False}}
|
| 317 |
+
|
| 318 |
+
return {
|
| 319 |
+
"healthy": agent.status == AgentStatus.ACTIVE,
|
| 320 |
+
"checks": {
|
| 321 |
+
"agent_exists": True,
|
| 322 |
+
"status": agent.status.value,
|
| 323 |
+
"colossus_connection": self.colossus_connection_status == "connected"
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
async def update_agent(self, agent_id: str, agent_data: Union[Dict[str, Any], SaapAgent]) -> bool:
|
| 328 |
+
"""
|
| 329 |
+
🔧 CRITICAL FIX: Update agent configuration with proper data type handling
|
| 330 |
+
|
| 331 |
+
FIXED PROBLEMS:
|
| 332 |
+
- ❌ 'dict' object has no attribute 'id' → ✅ Handles both Dict and SaapAgent
|
| 333 |
+
- ❌ 'dict' object has no attribute 'name' → ✅ Proper data conversion
|
| 334 |
+
- ❌ Agent Settings Modal fails → ✅ Frontend-Backend compatibility
|
| 335 |
+
|
| 336 |
+
Args:
|
| 337 |
+
agent_id: Agent ID to update
|
| 338 |
+
agent_data: Either a dictionary (from Frontend) or SaapAgent object
|
| 339 |
+
|
| 340 |
+
Returns:
|
| 341 |
+
bool: Success status
|
| 342 |
+
"""
|
| 343 |
+
try:
|
| 344 |
+
logger.info(f"🔧 Updating agent {agent_id} with data type: {type(agent_data)}")
|
| 345 |
+
|
| 346 |
+
# Get existing agent
|
| 347 |
+
existing_agent = self.get_agent(agent_id)
|
| 348 |
+
if not existing_agent:
|
| 349 |
+
logger.error(f"❌ Cannot update: Agent {agent_id} not found")
|
| 350 |
+
return False
|
| 351 |
+
|
| 352 |
+
# Handle both Dict and SaapAgent input types
|
| 353 |
+
if isinstance(agent_data, dict):
|
| 354 |
+
logger.debug(f"📥 Received dictionary data for agent {agent_id}")
|
| 355 |
+
|
| 356 |
+
# Create updated agent from existing + new data
|
| 357 |
+
try:
|
| 358 |
+
# Start with existing agent's data
|
| 359 |
+
updated_dict = existing_agent.to_dict()
|
| 360 |
+
|
| 361 |
+
# Update with new data from frontend
|
| 362 |
+
updated_dict.update(agent_data)
|
| 363 |
+
|
| 364 |
+
# Ensure agent_id consistency
|
| 365 |
+
updated_dict['id'] = agent_id
|
| 366 |
+
|
| 367 |
+
# Create new SaapAgent object from updated data
|
| 368 |
+
updated_agent = SaapAgent.from_dict(updated_dict)
|
| 369 |
+
|
| 370 |
+
logger.debug(f"✅ Successfully converted dict to SaapAgent: {updated_agent.name}")
|
| 371 |
+
|
| 372 |
+
except Exception as conversion_error:
|
| 373 |
+
logger.error(f"❌ Failed to convert dict to SaapAgent: {conversion_error}")
|
| 374 |
+
logger.debug(f"🔍 Problematic data: {agent_data}")
|
| 375 |
+
return False
|
| 376 |
+
|
| 377 |
+
elif isinstance(agent_data, SaapAgent):
|
| 378 |
+
logger.debug(f"📥 Received SaapAgent object for agent {agent_id}")
|
| 379 |
+
updated_agent = agent_data
|
| 380 |
+
# Ensure ID consistency
|
| 381 |
+
updated_agent.id = agent_id
|
| 382 |
+
|
| 383 |
+
else:
|
| 384 |
+
logger.error(f"❌ Invalid agent_data type: {type(agent_data)}. Expected Dict or SaapAgent")
|
| 385 |
+
return False
|
| 386 |
+
|
| 387 |
+
# Update in memory cache
|
| 388 |
+
self.agents[agent_id] = updated_agent
|
| 389 |
+
logger.info(f"✅ Memory cache updated for agent {agent_id}")
|
| 390 |
+
|
| 391 |
+
# Try to update in database if available
|
| 392 |
+
if db_manager.is_initialized:
|
| 393 |
+
try:
|
| 394 |
+
async with db_manager.get_async_session() as session:
|
| 395 |
+
# Delete old and insert new (simpler than complex update)
|
| 396 |
+
await session.execute(delete(DBAgent).where(DBAgent.id == agent_id))
|
| 397 |
+
|
| 398 |
+
# Create new database record from updated agent
|
| 399 |
+
db_agent = DBAgent.from_saap_agent(updated_agent)
|
| 400 |
+
session.add(db_agent)
|
| 401 |
+
await session.commit()
|
| 402 |
+
|
| 403 |
+
logger.info(f"✅ Database updated for agent {agent_id}")
|
| 404 |
+
|
| 405 |
+
except Exception as db_error:
|
| 406 |
+
logger.warning(f"⚠️ Database update failed for {agent_id}: {db_error}")
|
| 407 |
+
# Don't fail the update if database fails - memory cache is updated
|
| 408 |
+
else:
|
| 409 |
+
logger.info(f"ℹ️ Database not available - agent {agent_id} updated in memory only")
|
| 410 |
+
|
| 411 |
+
logger.info(f"✅ Agent update completed successfully: {updated_agent.name} ({agent_id})")
|
| 412 |
+
return True
|
| 413 |
+
|
| 414 |
+
except Exception as e:
|
| 415 |
+
logger.error(f"❌ Agent update failed for {agent_id}: {e}")
|
| 416 |
+
logger.debug(f"🔍 Agent data that caused error: {agent_data}")
|
| 417 |
+
return False
|
| 418 |
+
|
| 419 |
+
async def delete_agent(self, agent_id: str) -> bool:
|
| 420 |
+
"""Delete agent from memory and database"""
|
| 421 |
+
try:
|
| 422 |
+
# Stop agent if running
|
| 423 |
+
await self.stop_agent(agent_id)
|
| 424 |
+
|
| 425 |
+
# Remove from memory
|
| 426 |
+
self.agents.pop(agent_id, None)
|
| 427 |
+
|
| 428 |
+
# Try to remove from database if available
|
| 429 |
+
if db_manager.is_initialized:
|
| 430 |
+
try:
|
| 431 |
+
async with db_manager.get_async_session() as session:
|
| 432 |
+
await session.execute(delete(DBAgent).where(DBAgent.id == agent_id))
|
| 433 |
+
await session.commit()
|
| 434 |
+
except Exception as db_error:
|
| 435 |
+
logger.warning(f"⚠️ Database deletion failed for {agent_id}: {db_error}")
|
| 436 |
+
|
| 437 |
+
logger.info(f"✅ Agent deleted: {agent_id}")
|
| 438 |
+
return True
|
| 439 |
+
|
| 440 |
+
except Exception as e:
|
| 441 |
+
logger.error(f"❌ Agent deletion failed: {e}")
|
| 442 |
+
return False
|
| 443 |
+
|
| 444 |
+
async def start_agent(self, agent_id: str) -> bool:
|
| 445 |
+
"""Start agent and create session"""
|
| 446 |
+
try:
|
| 447 |
+
agent = self.get_agent(agent_id)
|
| 448 |
+
if not agent:
|
| 449 |
+
logger.error(f"❌ Cannot start agent: {agent_id} not found")
|
| 450 |
+
return False
|
| 451 |
+
|
| 452 |
+
# Update status
|
| 453 |
+
agent.status = AgentStatus.ACTIVE
|
| 454 |
+
if hasattr(agent, 'metrics') and agent.metrics:
|
| 455 |
+
agent.metrics.last_active = datetime.utcnow()
|
| 456 |
+
|
| 457 |
+
# Try to create agent session in database if available
|
| 458 |
+
if db_manager.is_initialized:
|
| 459 |
+
try:
|
| 460 |
+
async with db_manager.get_async_session() as session:
|
| 461 |
+
db_session = DBAgentSession(agent_id=agent_id)
|
| 462 |
+
session.add(db_session)
|
| 463 |
+
await session.commit()
|
| 464 |
+
await session.refresh(db_session)
|
| 465 |
+
|
| 466 |
+
# Store in active sessions
|
| 467 |
+
self.active_sessions[agent_id] = db_session
|
| 468 |
+
except Exception as db_error:
|
| 469 |
+
logger.warning(f"⚠️ Database session creation failed for {agent_id}: {db_error}")
|
| 470 |
+
|
| 471 |
+
# Update agent status in database if available
|
| 472 |
+
await self._update_agent_status(agent_id, AgentStatus.ACTIVE)
|
| 473 |
+
|
| 474 |
+
logger.info(f"✅ Agent started: {agent.name} ({agent_id})")
|
| 475 |
+
return True
|
| 476 |
+
|
| 477 |
+
except Exception as e:
|
| 478 |
+
logger.error(f"❌ Agent start failed: {e}")
|
| 479 |
+
return False
|
| 480 |
+
|
| 481 |
+
async def stop_agent(self, agent_id: str) -> bool:
|
| 482 |
+
"""Stop agent and close session"""
|
| 483 |
+
try:
|
| 484 |
+
agent = self.get_agent(agent_id)
|
| 485 |
+
if not agent:
|
| 486 |
+
return False
|
| 487 |
+
|
| 488 |
+
# Update status
|
| 489 |
+
agent.status = AgentStatus.INACTIVE
|
| 490 |
+
|
| 491 |
+
# Close agent session if exists
|
| 492 |
+
if agent_id in self.active_sessions:
|
| 493 |
+
session_obj = self.active_sessions[agent_id]
|
| 494 |
+
session_obj.session_end = datetime.utcnow()
|
| 495 |
+
session_obj.status = "completed"
|
| 496 |
+
session_obj.end_reason = "graceful"
|
| 497 |
+
session_obj.calculate_duration()
|
| 498 |
+
|
| 499 |
+
if db_manager.is_initialized:
|
| 500 |
+
try:
|
| 501 |
+
async with db_manager.get_async_session() as session:
|
| 502 |
+
await session.merge(session_obj)
|
| 503 |
+
await session.commit()
|
| 504 |
+
except Exception as db_error:
|
| 505 |
+
logger.warning(f"⚠️ Database session update failed for {agent_id}: {db_error}")
|
| 506 |
+
|
| 507 |
+
del self.active_sessions[agent_id]
|
| 508 |
+
|
| 509 |
+
# Update agent status in database if available
|
| 510 |
+
await self._update_agent_status(agent_id, AgentStatus.INACTIVE)
|
| 511 |
+
|
| 512 |
+
logger.info(f"🔧 Agent stopped: {agent_id}")
|
| 513 |
+
return True
|
| 514 |
+
|
| 515 |
+
except Exception as e:
|
| 516 |
+
logger.error(f"❌ Agent stop failed: {e}")
|
| 517 |
+
return False
|
| 518 |
+
|
| 519 |
+
async def restart_agent(self, agent_id: str) -> bool:
|
| 520 |
+
"""Restart agent (stop + start)"""
|
| 521 |
+
try:
|
| 522 |
+
await self.stop_agent(agent_id)
|
| 523 |
+
await asyncio.sleep(1) # Brief pause
|
| 524 |
+
return await self.start_agent(agent_id)
|
| 525 |
+
except Exception as e:
|
| 526 |
+
logger.error(f"❌ Agent restart failed: {e}")
|
| 527 |
+
return False
|
| 528 |
+
|
| 529 |
+
async def _update_agent_status(self, agent_id: str, status: AgentStatus):
|
| 530 |
+
"""Update agent status in database"""
|
| 531 |
+
if not db_manager.is_initialized:
|
| 532 |
+
return
|
| 533 |
+
|
| 534 |
+
try:
|
| 535 |
+
async with db_manager.get_async_session() as session:
|
| 536 |
+
await session.execute(
|
| 537 |
+
update(DBAgent)
|
| 538 |
+
.where(DBAgent.id == agent_id)
|
| 539 |
+
.values(status=status.value, last_active=datetime.utcnow())
|
| 540 |
+
)
|
| 541 |
+
await session.commit()
|
| 542 |
+
|
| 543 |
+
except Exception as e:
|
| 544 |
+
logger.warning(f"⚠️ Failed to update agent status in database: {e}")
|
| 545 |
+
|
| 546 |
+
# 🚀 Multi-Provider Chat Support
|
| 547 |
+
async def send_message_to_agent(self, agent_id: str, message: str,
|
| 548 |
+
provider: Optional[str] = None) -> Dict[str, Any]:
|
| 549 |
+
"""
|
| 550 |
+
Send message to agent via specified provider or auto-fallback
|
| 551 |
+
|
| 552 |
+
Args:
|
| 553 |
+
agent_id: Target agent ID
|
| 554 |
+
message: Message content
|
| 555 |
+
provider: Optional provider override ("colossus", "openrouter", or None for auto)
|
| 556 |
+
|
| 557 |
+
Returns:
|
| 558 |
+
Chat response with metadata
|
| 559 |
+
"""
|
| 560 |
+
try:
|
| 561 |
+
# Enhanced error checking with detailed debugging
|
| 562 |
+
agent = self.get_agent(agent_id)
|
| 563 |
+
if not agent:
|
| 564 |
+
error_msg = f"Agent {agent_id} not found in loaded agents"
|
| 565 |
+
logger.error(f"❌ {error_msg}")
|
| 566 |
+
logger.debug(f"📋 Available agents: {list(self.agents.keys())}")
|
| 567 |
+
return {
|
| 568 |
+
"error": error_msg,
|
| 569 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 570 |
+
"debug_info": {
|
| 571 |
+
"available_agents": list(self.agents.keys()),
|
| 572 |
+
"agent_manager_initialized": self.is_initialized
|
| 573 |
+
}
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
# Check if agent is available for messaging
|
| 577 |
+
if agent.status != AgentStatus.ACTIVE:
|
| 578 |
+
error_msg = f"Agent {agent_id} not available (status: {agent.status.value})"
|
| 579 |
+
logger.error(f"❌ {error_msg}")
|
| 580 |
+
return {
|
| 581 |
+
"error": error_msg,
|
| 582 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 583 |
+
"debug_info": {
|
| 584 |
+
"agent_status": agent.status.value,
|
| 585 |
+
"agent_id": agent_id
|
| 586 |
+
}
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
# 🚀 Multi-Provider Logic
|
| 590 |
+
if provider == "openrouter":
|
| 591 |
+
return await self._send_via_openrouter(agent_id, message, agent)
|
| 592 |
+
elif provider == "colossus":
|
| 593 |
+
return await self._send_via_colossus(agent_id, message, agent)
|
| 594 |
+
else:
|
| 595 |
+
# Auto-selection: Try colossus first, fallback to OpenRouter
|
| 596 |
+
if self.colossus_connection_status == "connected":
|
| 597 |
+
logger.info(f"🔄 Using colossus as primary provider for {agent_id}")
|
| 598 |
+
result = await self._send_via_colossus(agent_id, message, agent)
|
| 599 |
+
# If colossus fails, try OpenRouter
|
| 600 |
+
if "error" in result and "colossus" in result["error"].lower():
|
| 601 |
+
logger.info(f"🔄 colossus failed, trying OpenRouter fallback...")
|
| 602 |
+
return await self._send_via_openrouter(agent_id, message, agent)
|
| 603 |
+
return result
|
| 604 |
+
else:
|
| 605 |
+
logger.info(f"🔄 colossus unavailable, using OpenRouter as primary for {agent_id}")
|
| 606 |
+
return await self._send_via_openrouter(agent_id, message, agent)
|
| 607 |
+
|
| 608 |
+
except Exception as e:
|
| 609 |
+
error_msg = str(e)
|
| 610 |
+
logger.error(f"❌ Message to agent failed: {error_msg}")
|
| 611 |
+
return {
|
| 612 |
+
"error": error_msg,
|
| 613 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 614 |
+
"debug_info": {
|
| 615 |
+
"agent_id": agent_id,
|
| 616 |
+
"provider": provider,
|
| 617 |
+
"colossus_status": self.colossus_connection_status,
|
| 618 |
+
"agent_found": agent_id in self.agents,
|
| 619 |
+
"colossus_client_exists": self.colossus_client is not None
|
| 620 |
+
}
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
async def _send_via_openrouter(self, agent_id: str, message: str,
|
| 624 |
+
agent: SaapAgent) -> Dict[str, Any]:
|
| 625 |
+
"""Send message via OpenRouter provider"""
|
| 626 |
+
try:
|
| 627 |
+
logger.info(f"🌐 jane_alesi (coordinator) initialized with OpenRouter FREE")
|
| 628 |
+
|
| 629 |
+
# Create OpenRouter agent for this request
|
| 630 |
+
openrouter_agent = OpenRouterSAAPAgent(
|
| 631 |
+
agent_id,
|
| 632 |
+
agent.type.value if agent.type else "Assistant",
|
| 633 |
+
os.getenv("OPENROUTER_API_KEY")
|
| 634 |
+
)
|
| 635 |
+
|
| 636 |
+
# Get cost-optimized model for specific agent
|
| 637 |
+
model_map = {
|
| 638 |
+
"jane_alesi": os.getenv("JANE_ALESI_MODEL", "openai/gpt-4o-mini"),
|
| 639 |
+
"john_alesi": os.getenv("JOHN_ALESI_MODEL", "deepseek/deepseek-coder"),
|
| 640 |
+
"lara_alesi": os.getenv("LARA_ALESI_MODEL", "anthropic/claude-3-haiku")
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
preferred_model = model_map.get(agent_id, "meta-llama/llama-3.2-3b-instruct:free")
|
| 644 |
+
openrouter_agent.model_name = preferred_model
|
| 645 |
+
|
| 646 |
+
start_time = datetime.utcnow()
|
| 647 |
+
logger.info(f"📤 Sending message to {agent.name} ({agent_id}) via OpenRouter ({preferred_model})...")
|
| 648 |
+
|
| 649 |
+
# 🔧 FIXED: Use safe LLM config access
|
| 650 |
+
max_tokens_value = self._get_llm_config_value(agent, 'max_tokens', 1000)
|
| 651 |
+
|
| 652 |
+
# Send message via OpenRouter
|
| 653 |
+
response = await openrouter_agent.send_request_to_openrouter(
|
| 654 |
+
message,
|
| 655 |
+
max_tokens=max_tokens_value
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
end_time = datetime.utcnow()
|
| 659 |
+
response_time = (end_time - start_time).total_seconds()
|
| 660 |
+
|
| 661 |
+
if response.get("success"):
|
| 662 |
+
logger.info(f"✅ OpenRouter response successful in {response_time:.2f}s")
|
| 663 |
+
|
| 664 |
+
response_content = response.get("response", "")
|
| 665 |
+
tokens_used = response.get("token_count", 0)
|
| 666 |
+
cost_usd = response.get("cost_usd", 0.0)
|
| 667 |
+
|
| 668 |
+
# Try to save to database if available
|
| 669 |
+
if db_manager.is_initialized:
|
| 670 |
+
try:
|
| 671 |
+
async with db_manager.get_async_session() as session:
|
| 672 |
+
chat_message = DBChatMessage(
|
| 673 |
+
agent_id=agent_id,
|
| 674 |
+
user_message=message,
|
| 675 |
+
agent_response=response_content,
|
| 676 |
+
response_time=response_time,
|
| 677 |
+
tokens_used=tokens_used,
|
| 678 |
+
metadata={
|
| 679 |
+
"model": preferred_model,
|
| 680 |
+
"provider": "OpenRouter",
|
| 681 |
+
"cost_usd": cost_usd,
|
| 682 |
+
"temperature": 0.7
|
| 683 |
+
}
|
| 684 |
+
)
|
| 685 |
+
session.add(chat_message)
|
| 686 |
+
await session.commit()
|
| 687 |
+
except Exception as db_error:
|
| 688 |
+
logger.warning(f"⚠️ Failed to save OpenRouter chat to database: {db_error}")
|
| 689 |
+
|
| 690 |
+
return {
|
| 691 |
+
"content": response_content,
|
| 692 |
+
"response_time": response_time,
|
| 693 |
+
"tokens_used": tokens_used,
|
| 694 |
+
"cost_usd": cost_usd,
|
| 695 |
+
"provider": "OpenRouter",
|
| 696 |
+
"model": preferred_model,
|
| 697 |
+
"timestamp": end_time.isoformat()
|
| 698 |
+
}
|
| 699 |
+
else:
|
| 700 |
+
error_msg = response.get("error", "Unknown OpenRouter error")
|
| 701 |
+
logger.error(f"❌ OpenRouter fallback failed: {error_msg}")
|
| 702 |
+
return {
|
| 703 |
+
"error": f"OpenRouter error: {error_msg}",
|
| 704 |
+
"provider": "OpenRouter",
|
| 705 |
+
"timestamp": end_time.isoformat()
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
except Exception as e:
|
| 709 |
+
logger.error(f"❌ OpenRouter fallback failed: {str(e)}")
|
| 710 |
+
return {
|
| 711 |
+
"error": f"OpenRouter error: {str(e)}",
|
| 712 |
+
"provider": "OpenRouter",
|
| 713 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
async def _send_via_colossus(self, agent_id: str, message: str,
|
| 717 |
+
agent: SaapAgent) -> Dict[str, Any]:
|
| 718 |
+
"""Send message via colossus provider"""
|
| 719 |
+
try:
|
| 720 |
+
# Check colossus client availability
|
| 721 |
+
if not self.colossus_client:
|
| 722 |
+
return {
|
| 723 |
+
"error": "colossus client not initialized",
|
| 724 |
+
"provider": "colossus",
|
| 725 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
# Test colossus connection if it's been a while
|
| 729 |
+
if (not self.last_colossus_test or
|
| 730 |
+
(datetime.utcnow() - self.last_colossus_test).seconds > 300): # 5 minutes
|
| 731 |
+
await self._test_colossus_connection()
|
| 732 |
+
|
| 733 |
+
if self.colossus_connection_status != "connected":
|
| 734 |
+
return {
|
| 735 |
+
"error": f"colossus connection not healthy: {self.colossus_connection_status}",
|
| 736 |
+
"provider": "colossus",
|
| 737 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
start_time = datetime.utcnow()
|
| 741 |
+
logger.info(f"📤 Sending message to {agent.name} ({agent_id}) via colossus...")
|
| 742 |
+
|
| 743 |
+
# 🔧 FIXED: Use safe LLM config access
|
| 744 |
+
temperature_value = self._get_llm_config_value(agent, 'temperature', 0.7)
|
| 745 |
+
max_tokens_value = self._get_llm_config_value(agent, 'max_tokens', 1000)
|
| 746 |
+
|
| 747 |
+
# Send message to colossus
|
| 748 |
+
response = await self.colossus_client.chat_completion(
|
| 749 |
+
messages=[
|
| 750 |
+
{"role": "system", "content": agent.description or f"You are {agent.name}"},
|
| 751 |
+
{"role": "user", "content": message}
|
| 752 |
+
],
|
| 753 |
+
agent_id=agent_id,
|
| 754 |
+
temperature=temperature_value,
|
| 755 |
+
max_tokens=max_tokens_value
|
| 756 |
+
)
|
| 757 |
+
|
| 758 |
+
end_time = datetime.utcnow()
|
| 759 |
+
response_time = (end_time - start_time).total_seconds()
|
| 760 |
+
|
| 761 |
+
logger.info(f"📥 Received response from colossus in {response_time:.2f}s")
|
| 762 |
+
|
| 763 |
+
# Enhanced response parsing
|
| 764 |
+
response_content = ""
|
| 765 |
+
tokens_used = 0
|
| 766 |
+
|
| 767 |
+
if response:
|
| 768 |
+
logger.debug(f"🔍 Raw colossus response: {response}")
|
| 769 |
+
|
| 770 |
+
if isinstance(response, dict):
|
| 771 |
+
# SAAP ColossusClient format: {"success": true, "response": {...}}
|
| 772 |
+
if response.get("success") and "response" in response:
|
| 773 |
+
colossus_response = response["response"]
|
| 774 |
+
if isinstance(colossus_response, dict) and "choices" in colossus_response:
|
| 775 |
+
# OpenAI-compatible format within SAAP response
|
| 776 |
+
if len(colossus_response["choices"]) > 0:
|
| 777 |
+
choice = colossus_response["choices"][0]
|
| 778 |
+
if "message" in choice and "content" in choice["message"]:
|
| 779 |
+
response_content = choice["message"]["content"]
|
| 780 |
+
elif isinstance(colossus_response, str):
|
| 781 |
+
# Direct string response
|
| 782 |
+
response_content = colossus_response
|
| 783 |
+
|
| 784 |
+
# Extract token usage if available
|
| 785 |
+
if isinstance(colossus_response, dict) and "usage" in colossus_response:
|
| 786 |
+
tokens_used = colossus_response["usage"].get("total_tokens", 0)
|
| 787 |
+
|
| 788 |
+
# Handle colossus client error responses
|
| 789 |
+
elif not response.get("success"):
|
| 790 |
+
error_msg = response.get("error", "Unknown colossus error")
|
| 791 |
+
logger.error(f"❌ colossus error: {error_msg}")
|
| 792 |
+
return {
|
| 793 |
+
"error": f"colossus server error: {error_msg}",
|
| 794 |
+
"provider": "colossus",
|
| 795 |
+
"timestamp": end_time.isoformat()
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
# Direct OpenAI format: {"choices": [...]}
|
| 799 |
+
elif "choices" in response and len(response["choices"]) > 0:
|
| 800 |
+
choice = response["choices"][0]
|
| 801 |
+
if "message" in choice and "content" in choice["message"]:
|
| 802 |
+
response_content = choice["message"]["content"]
|
| 803 |
+
if "usage" in response:
|
| 804 |
+
tokens_used = response["usage"].get("total_tokens", 0)
|
| 805 |
+
|
| 806 |
+
# Simple response format: {"response": "text"} or {"content": "text"}
|
| 807 |
+
elif "response" in response:
|
| 808 |
+
response_content = response["response"]
|
| 809 |
+
elif "content" in response:
|
| 810 |
+
response_content = response["content"]
|
| 811 |
+
|
| 812 |
+
elif isinstance(response, str):
|
| 813 |
+
# Direct string response
|
| 814 |
+
response_content = response
|
| 815 |
+
|
| 816 |
+
# Fallback if no content extracted
|
| 817 |
+
if not response_content:
|
| 818 |
+
logger.error(f"❌ Unable to extract content from colossus response: {response}")
|
| 819 |
+
return {
|
| 820 |
+
"error": "Failed to parse colossus response",
|
| 821 |
+
"provider": "colossus",
|
| 822 |
+
"timestamp": end_time.isoformat()
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
# Try to save to database if available
|
| 826 |
+
if db_manager.is_initialized:
|
| 827 |
+
try:
|
| 828 |
+
async with db_manager.get_async_session() as session:
|
| 829 |
+
chat_message = DBChatMessage(
|
| 830 |
+
agent_id=agent_id,
|
| 831 |
+
user_message=message,
|
| 832 |
+
agent_response=response_content,
|
| 833 |
+
response_time=response_time,
|
| 834 |
+
tokens_used=tokens_used,
|
| 835 |
+
metadata={
|
| 836 |
+
"model": "mistral-small3.2:24b-instruct-2506",
|
| 837 |
+
"provider": "colossus",
|
| 838 |
+
"temperature": 0.7
|
| 839 |
+
}
|
| 840 |
+
)
|
| 841 |
+
session.add(chat_message)
|
| 842 |
+
await session.commit()
|
| 843 |
+
except Exception as db_error:
|
| 844 |
+
logger.warning(f"⚠️ Failed to save chat message to database: {db_error}")
|
| 845 |
+
|
| 846 |
+
# Update session metrics
|
| 847 |
+
if agent_id in self.active_sessions:
|
| 848 |
+
session_obj = self.active_sessions[agent_id]
|
| 849 |
+
session_obj.messages_processed += 1
|
| 850 |
+
session_obj.total_tokens_used += tokens_used
|
| 851 |
+
|
| 852 |
+
logger.info(f"✅ Message processed successfully for {agent.name}")
|
| 853 |
+
|
| 854 |
+
return {
|
| 855 |
+
"content": response_content,
|
| 856 |
+
"response_time": response_time,
|
| 857 |
+
"tokens_used": tokens_used,
|
| 858 |
+
"provider": "colossus",
|
| 859 |
+
"model": "mistral-small3.2:24b-instruct-2506",
|
| 860 |
+
"timestamp": end_time.isoformat()
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
except Exception as e:
|
| 864 |
+
logger.error(f"❌ colossus communication failed: {str(e)}")
|
| 865 |
+
return {
|
| 866 |
+
"error": f"colossus error: {str(e)}",
|
| 867 |
+
"provider": "colossus",
|
| 868 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
async def get_agent_metrics(self, agent_id: str) -> Dict[str, Any]:
|
| 872 |
+
"""Get comprehensive agent metrics from database"""
|
| 873 |
+
if not db_manager.is_initialized:
|
| 874 |
+
return {"warning": "Database not available - no metrics"}
|
| 875 |
+
|
| 876 |
+
try:
|
| 877 |
+
async with db_manager.get_async_session() as session:
|
| 878 |
+
# Get message count and average response time
|
| 879 |
+
result = await session.execute(
|
| 880 |
+
select(DBChatMessage).where(DBChatMessage.agent_id == agent_id)
|
| 881 |
+
)
|
| 882 |
+
messages = result.scalars().all()
|
| 883 |
+
|
| 884 |
+
if messages:
|
| 885 |
+
avg_response_time = sum(m.response_time for m in messages if m.response_time) / len(messages)
|
| 886 |
+
total_tokens = sum(m.tokens_used for m in messages if m.tokens_used)
|
| 887 |
+
else:
|
| 888 |
+
avg_response_time = 0
|
| 889 |
+
total_tokens = 0
|
| 890 |
+
|
| 891 |
+
# Get session count
|
| 892 |
+
session_result = await session.execute(
|
| 893 |
+
select(DBAgentSession).where(DBAgentSession.agent_id == agent_id)
|
| 894 |
+
)
|
| 895 |
+
sessions = session_result.scalars().all()
|
| 896 |
+
|
| 897 |
+
return {
|
| 898 |
+
"total_messages": len(messages),
|
| 899 |
+
"total_tokens_used": total_tokens,
|
| 900 |
+
"average_response_time": avg_response_time,
|
| 901 |
+
"total_sessions": len(sessions),
|
| 902 |
+
"last_activity": max([s.session_start for s in sessions], default=None),
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
except Exception as e:
|
| 906 |
+
logger.error(f"❌ Failed to get agent metrics: {e}")
|
| 907 |
+
return {}
|
| 908 |
+
|
| 909 |
+
async def get_system_status(self) -> Dict[str, Any]:
|
| 910 |
+
"""Get comprehensive system status for debugging"""
|
| 911 |
+
return {
|
| 912 |
+
"agent_manager_initialized": self.is_initialized,
|
| 913 |
+
"colossus_connection_status": self.colossus_connection_status,
|
| 914 |
+
"colossus_last_test": self.last_colossus_test.isoformat() if self.last_colossus_test else None,
|
| 915 |
+
"loaded_agents": len(self.agents),
|
| 916 |
+
"active_sessions": len(self.active_sessions),
|
| 917 |
+
"agent_list": [{"id": aid, "name": agent.name, "status": agent.status.value}
|
| 918 |
+
for aid, agent in self.agents.items()],
|
| 919 |
+
"database_initialized": getattr(db_manager, 'is_initialized', False)
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
async def shutdown_all_agents(self):
|
| 923 |
+
"""Gracefully shutdown all active agents"""
|
| 924 |
+
try:
|
| 925 |
+
logger.info("🔧 Shutting down all agents...")
|
| 926 |
+
|
| 927 |
+
for agent_id in list(self.agents.keys()):
|
| 928 |
+
await self.stop_agent(agent_id)
|
| 929 |
+
|
| 930 |
+
if self.colossus_client:
|
| 931 |
+
await self.colossus_client.__aexit__(None, None, None)
|
| 932 |
+
|
| 933 |
+
logger.info("✅ All agents shut down successfully")
|
| 934 |
+
|
| 935 |
+
except Exception as e:
|
| 936 |
+
logger.error(f"❌ Agent shutdown failed: {e}")
|
| 937 |
+
|
| 938 |
+
|
| 939 |
+
# Create global instance for dependency injection
|
| 940 |
+
agent_manager = AgentManagerService()
|
| 941 |
+
|
| 942 |
+
# Make class available for import
|
| 943 |
+
AgentManager = AgentManagerService
|
| 944 |
+
|
| 945 |
+
if __name__ == "__main__":
|
| 946 |
+
async def test_agent_manager():
|
| 947 |
+
"""Test agent manager functionality"""
|
| 948 |
+
manager = AgentManagerService()
|
| 949 |
+
await manager.initialize()
|
| 950 |
+
|
| 951 |
+
# List agents
|
| 952 |
+
agents = list(manager.agents.values())
|
| 953 |
+
print(f"📋 Agents loaded: {[a.name for a in agents]}")
|
| 954 |
+
|
| 955 |
+
# Test agent update with dict data (simulate frontend)
|
| 956 |
+
if agents:
|
| 957 |
+
agent = agents[0]
|
| 958 |
+
print(f"\n🧪 Testing agent update for: {agent.name}")
|
| 959 |
+
|
| 960 |
+
# Simulate frontend data (dictionary)
|
| 961 |
+
update_data = {
|
| 962 |
+
"name": agent.name,
|
| 963 |
+
"description": "Updated description from test",
|
| 964 |
+
"type": agent.type.value if agent.type else "assistant",
|
| 965 |
+
"capabilities": ["updated_capability_1", "updated_capability_2"]
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
# Test update
|
| 969 |
+
success = await manager.update_agent(agent.id, update_data)
|
| 970 |
+
print(f"🔧 Update result: {'✅' if success else '❌'}")
|
| 971 |
+
|
| 972 |
+
# Start first agent
|
| 973 |
+
success = await manager.start_agent(agent.id)
|
| 974 |
+
print(f"🚀 Start agent {agent.name}: {'✅' if success else '❌'}")
|
| 975 |
+
|
| 976 |
+
await manager.shutdown_all_agents()
|
| 977 |
+
|
| 978 |
+
asyncio.run(test_agent_manager())
|
backend/agent_manager_hybrid.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🔧 FIXED: Hybrid SAAP Agent Manager Service - Critical Errors Resolved
|
| 3 |
+
Production-ready agent management with multi-provider support and cost optimization
|
| 4 |
+
|
| 5 |
+
FIXES APPLIED:
|
| 6 |
+
1. ✅ _send_colossus_message() method properly implemented (no longer missing)
|
| 7 |
+
2. ✅ _get_llm_config_value() enhanced with robust config type handling
|
| 8 |
+
3. ✅ Comprehensive error handling for Frontend/Backend config mismatches
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import asyncio
|
| 12 |
+
import logging
|
| 13 |
+
from typing import Dict, List, Optional, Any
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
import uuid
|
| 16 |
+
|
| 17 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 18 |
+
from sqlalchemy import select, update, delete
|
| 19 |
+
|
| 20 |
+
from models.agent import SaapAgent, AgentStatus, AgentType, AgentTemplates
|
| 21 |
+
from database.connection import db_manager
|
| 22 |
+
from database.models import DBAgent, DBChatMessage, DBAgentSession
|
| 23 |
+
from api.colossus_client import ColossusClient
|
| 24 |
+
from api.openrouter_client import OpenRouterClient, OpenRouterResponse
|
| 25 |
+
from services.agent_manager import AgentManagerService # Extend existing service
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
class HybridAgentManagerService(AgentManagerService):
|
| 30 |
+
"""
|
| 31 |
+
🔧 FIXED: Hybrid Agent Manager with Critical Error Resolution
|
| 32 |
+
|
| 33 |
+
Features:
|
| 34 |
+
- Inherits all colossus functionality from AgentManagerService
|
| 35 |
+
- Adds OpenRouter integration with cost tracking
|
| 36 |
+
- Provider switching and failover logic
|
| 37 |
+
- Performance comparison between providers
|
| 38 |
+
- Backward compatible with existing SAAP API
|
| 39 |
+
|
| 40 |
+
CRITICAL FIXES:
|
| 41 |
+
1. ✅ _send_colossus_message() method properly implemented
|
| 42 |
+
2. ✅ LLMModelConfig.get() AttributeError completely resolved
|
| 43 |
+
3. ✅ Robust config handling for dict/object/Pydantic models
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, openrouter_api_key: Optional[str] = None):
|
| 47 |
+
# Initialize base AgentManagerService
|
| 48 |
+
super().__init__()
|
| 49 |
+
|
| 50 |
+
# OpenRouter integration
|
| 51 |
+
self.openrouter_client: Optional[OpenRouterClient] = None
|
| 52 |
+
self.openrouter_api_key = openrouter_api_key or "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"
|
| 53 |
+
|
| 54 |
+
# 🚀 PERFORMANCE OPTIMIZATION: OpenRouter Primary (Fast 2-5s vs colossus 15-30s)
|
| 55 |
+
# Phase 1 Quick Win: Provider prioritization reversed for 90% speed improvement
|
| 56 |
+
self.primary_provider = "openrouter" # OpenRouter primary for speed
|
| 57 |
+
self.enable_cost_comparison = True
|
| 58 |
+
self.enable_failover = True # colossus as backup fallback
|
| 59 |
+
|
| 60 |
+
# Performance tracking
|
| 61 |
+
self.provider_stats = {
|
| 62 |
+
"colossus": {"requests": 0, "successes": 0, "total_time": 0.0, "total_cost": 0.0},
|
| 63 |
+
"openrouter": {"requests": 0, "successes": 0, "total_time": 0.0, "total_cost": 0.0}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
# Cost comparison data
|
| 67 |
+
self.cost_comparisons: List[Dict[str, Any]] = []
|
| 68 |
+
|
| 69 |
+
logger.info("🔄 Hybrid Agent Manager initialized - colossus + OpenRouter support")
|
| 70 |
+
|
| 71 |
+
def _get_llm_config_value(self, agent: SaapAgent, key: str, default=None):
|
| 72 |
+
"""
|
| 73 |
+
🔧 CRITICAL FIX 2: Safe LLM config access preventing 'get' attribute errors
|
| 74 |
+
|
| 75 |
+
Handles ALL possible configuration formats:
|
| 76 |
+
- Dictionary-based config (Frontend JSON)
|
| 77 |
+
- Object-based config (Pydantic models)
|
| 78 |
+
- Mixed format configurations
|
| 79 |
+
- Attribute vs method access patterns
|
| 80 |
+
- Error-prone Frontend→Backend data flow
|
| 81 |
+
|
| 82 |
+
This completely resolves: 'LLMModelConfig' object has no attribute 'get'
|
| 83 |
+
"""
|
| 84 |
+
try:
|
| 85 |
+
if not hasattr(agent, 'llm_config') or not agent.llm_config:
|
| 86 |
+
logger.debug(f"Agent {agent.id} has no llm_config, using default: {default}")
|
| 87 |
+
return default
|
| 88 |
+
|
| 89 |
+
llm_config = agent.llm_config
|
| 90 |
+
|
| 91 |
+
# Case 1: Dictionary-based config (Frontend JSON → Backend)
|
| 92 |
+
if isinstance(llm_config, dict):
|
| 93 |
+
value = llm_config.get(key, default)
|
| 94 |
+
logger.debug(f"✅ Dict config access: {key}={value}")
|
| 95 |
+
return value
|
| 96 |
+
|
| 97 |
+
# Case 2: Object with direct attribute access (Pydantic models)
|
| 98 |
+
elif hasattr(llm_config, key):
|
| 99 |
+
value = getattr(llm_config, key, default)
|
| 100 |
+
logger.debug(f"✅ Attribute access: {key}={value}")
|
| 101 |
+
return value
|
| 102 |
+
|
| 103 |
+
# Case 3: Object with get() method (dict-like objects)
|
| 104 |
+
elif hasattr(llm_config, 'get') and callable(getattr(llm_config, 'get')):
|
| 105 |
+
try:
|
| 106 |
+
value = llm_config.get(key, default)
|
| 107 |
+
logger.debug(f"✅ Method get() access: {key}={value}")
|
| 108 |
+
return value
|
| 109 |
+
except Exception as get_error:
|
| 110 |
+
logger.warning(f"⚠️ get() method failed: {get_error}, trying fallback")
|
| 111 |
+
|
| 112 |
+
# Case 4: Convert object to dict (Pydantic → dict)
|
| 113 |
+
elif hasattr(llm_config, '__dict__'):
|
| 114 |
+
config_dict = llm_config.__dict__
|
| 115 |
+
if key in config_dict:
|
| 116 |
+
value = config_dict[key]
|
| 117 |
+
logger.debug(f"✅ __dict__ access: {key}={value}")
|
| 118 |
+
return value
|
| 119 |
+
|
| 120 |
+
# Case 5: Try model_dump() for Pydantic v2
|
| 121 |
+
elif hasattr(llm_config, 'model_dump'):
|
| 122 |
+
try:
|
| 123 |
+
config_dict = llm_config.model_dump()
|
| 124 |
+
value = config_dict.get(key, default)
|
| 125 |
+
logger.debug(f"✅ model_dump() access: {key}={value}")
|
| 126 |
+
return value
|
| 127 |
+
except Exception:
|
| 128 |
+
pass
|
| 129 |
+
|
| 130 |
+
# Case 6: Try dict() conversion
|
| 131 |
+
elif hasattr(llm_config, 'dict'):
|
| 132 |
+
try:
|
| 133 |
+
config_dict = llm_config.dict()
|
| 134 |
+
value = config_dict.get(key, default)
|
| 135 |
+
logger.debug(f"✅ dict() access: {key}={value}")
|
| 136 |
+
return value
|
| 137 |
+
except Exception:
|
| 138 |
+
pass
|
| 139 |
+
|
| 140 |
+
# Case 7: Final fallback
|
| 141 |
+
logger.warning(f"⚠️ Unknown config type {type(llm_config)} for {key}, using default: {default}")
|
| 142 |
+
return default
|
| 143 |
+
|
| 144 |
+
except AttributeError as e:
|
| 145 |
+
logger.warning(f"⚠️ AttributeError in LLM config access for {key}: {e}, using default: {default}")
|
| 146 |
+
return default
|
| 147 |
+
except Exception as e:
|
| 148 |
+
logger.error(f"❌ Unexpected error in LLM config access for {key}: {e}, using default: {default}")
|
| 149 |
+
return default
|
| 150 |
+
|
| 151 |
+
async def initialize(self):
|
| 152 |
+
"""Initialize both colossus and OpenRouter clients"""
|
| 153 |
+
# Initialize base service (colossus + database)
|
| 154 |
+
await super().initialize()
|
| 155 |
+
|
| 156 |
+
# Initialize OpenRouter client
|
| 157 |
+
if self.openrouter_api_key:
|
| 158 |
+
try:
|
| 159 |
+
logger.info("🌐 Initializing OpenRouter client...")
|
| 160 |
+
self.openrouter_client = OpenRouterClient(self.openrouter_api_key)
|
| 161 |
+
await self.openrouter_client.__aenter__()
|
| 162 |
+
|
| 163 |
+
# Test OpenRouter connection
|
| 164 |
+
health = await self.openrouter_client.health_check()
|
| 165 |
+
if health["status"] == "healthy":
|
| 166 |
+
logger.info("✅ OpenRouter client initialized successfully")
|
| 167 |
+
else:
|
| 168 |
+
logger.warning(f"⚠️ OpenRouter health check failed: {health.get('error')}")
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
logger.error(f"❌ OpenRouter initialization failed: {e}")
|
| 172 |
+
self.openrouter_client = None
|
| 173 |
+
|
| 174 |
+
logger.info(f"🚀 Hybrid initialization complete - Providers: colossus={self.colossus_client is not None}, OpenRouter={self.openrouter_client is not None}")
|
| 175 |
+
|
| 176 |
+
async def send_message_to_agent(self, agent_id: str, message: str, provider: Optional[str] = None) -> Dict[str, Any]:
|
| 177 |
+
"""
|
| 178 |
+
Enhanced message sending with multi-provider support
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
agent_id: Target agent identifier
|
| 182 |
+
message: Message content
|
| 183 |
+
provider: Force specific provider ("colossus", "openrouter", None=auto)
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
Response with provider info and cost data
|
| 187 |
+
"""
|
| 188 |
+
|
| 189 |
+
# Validate agent exists
|
| 190 |
+
agent = self.get_agent(agent_id)
|
| 191 |
+
if not agent:
|
| 192 |
+
return {
|
| 193 |
+
"error": f"Agent {agent_id} not found in loaded agents",
|
| 194 |
+
"available_agents": list(self.agents.keys()),
|
| 195 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
# Provider selection logic
|
| 199 |
+
selected_provider = provider or self.primary_provider
|
| 200 |
+
|
| 201 |
+
# Try primary provider first
|
| 202 |
+
if selected_provider == "colossus" and self.colossus_client:
|
| 203 |
+
result = await self._send_colossus_message(agent, message)
|
| 204 |
+
|
| 205 |
+
# If colossus fails and failover enabled, try OpenRouter
|
| 206 |
+
if "error" in result and self.enable_failover and self.openrouter_client:
|
| 207 |
+
logger.info(f"🔄 colossus failed, attempting OpenRouter failover for {agent_id}")
|
| 208 |
+
openrouter_result = await self._send_openrouter_message(agent, message)
|
| 209 |
+
|
| 210 |
+
# Add failover info to response
|
| 211 |
+
if "error" not in openrouter_result:
|
| 212 |
+
openrouter_result["failover_used"] = True
|
| 213 |
+
openrouter_result["primary_provider_error"] = result.get("error")
|
| 214 |
+
|
| 215 |
+
return openrouter_result
|
| 216 |
+
|
| 217 |
+
return result
|
| 218 |
+
|
| 219 |
+
elif selected_provider == "openrouter" and self.openrouter_client:
|
| 220 |
+
result = await self._send_openrouter_message(agent, message)
|
| 221 |
+
|
| 222 |
+
# If OpenRouter fails and failover enabled, try colossus
|
| 223 |
+
if "error" in result and self.enable_failover and self.colossus_client:
|
| 224 |
+
logger.info(f"🔄 OpenRouter failed, attempting colossus failover for {agent_id}")
|
| 225 |
+
colossus_result = await super().send_message_to_agent(agent_id, message)
|
| 226 |
+
|
| 227 |
+
# Add failover info
|
| 228 |
+
if "error" not in colossus_result:
|
| 229 |
+
colossus_result["failover_used"] = True
|
| 230 |
+
colossus_result["primary_provider_error"] = result.get("error")
|
| 231 |
+
colossus_result["provider"] = "colossus"
|
| 232 |
+
|
| 233 |
+
return colossus_result
|
| 234 |
+
|
| 235 |
+
return result
|
| 236 |
+
|
| 237 |
+
else:
|
| 238 |
+
# No provider available or provider not found
|
| 239 |
+
available_providers = []
|
| 240 |
+
if self.colossus_client:
|
| 241 |
+
available_providers.append("colossus")
|
| 242 |
+
if self.openrouter_client:
|
| 243 |
+
available_providers.append("openrouter")
|
| 244 |
+
|
| 245 |
+
return {
|
| 246 |
+
"error": f"Provider '{selected_provider}' not available",
|
| 247 |
+
"available_providers": available_providers,
|
| 248 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
async def _send_colossus_message(self, agent: SaapAgent, message: str) -> Dict[str, Any]:
|
| 252 |
+
"""
|
| 253 |
+
🔧 CRITICAL FIX 1: Properly implemented _send_colossus_message method
|
| 254 |
+
|
| 255 |
+
This was the missing method causing AttributeError!
|
| 256 |
+
Uses parent class _send_via_colossus method with enhanced error handling
|
| 257 |
+
and provider-specific metrics tracking.
|
| 258 |
+
|
| 259 |
+
Fixes: 'HybridAgentManagerService' object has no attribute '_send_colossus_message'
|
| 260 |
+
"""
|
| 261 |
+
try:
|
| 262 |
+
# Use parent class colossus communication method
|
| 263 |
+
result = await self._send_via_colossus(agent.id, message, agent)
|
| 264 |
+
|
| 265 |
+
# Ensure consistent return format for hybrid usage
|
| 266 |
+
if "error" not in result:
|
| 267 |
+
# Update provider stats for colossus
|
| 268 |
+
self.provider_stats["colossus"]["requests"] += 1
|
| 269 |
+
self.provider_stats["colossus"]["total_time"] += result.get("response_time", 0)
|
| 270 |
+
self.provider_stats["colossus"]["successes"] += 1
|
| 271 |
+
|
| 272 |
+
# Update agent metrics if available
|
| 273 |
+
if hasattr(agent, 'metrics') and agent.metrics:
|
| 274 |
+
agent.metrics.messages_processed += 1
|
| 275 |
+
agent.metrics.last_active = datetime.utcnow()
|
| 276 |
+
|
| 277 |
+
# Ensure provider is set in response
|
| 278 |
+
result["provider"] = "colossus"
|
| 279 |
+
|
| 280 |
+
logger.info(f"✅ colossus message sent successfully: {agent.name}")
|
| 281 |
+
else:
|
| 282 |
+
# Track failed requests too
|
| 283 |
+
self.provider_stats["colossus"]["requests"] += 1
|
| 284 |
+
result["provider"] = "colossus"
|
| 285 |
+
|
| 286 |
+
return result
|
| 287 |
+
|
| 288 |
+
except Exception as e:
|
| 289 |
+
error_msg = f"colossus communication failed: {str(e)}"
|
| 290 |
+
logger.error(f"❌ {error_msg}")
|
| 291 |
+
|
| 292 |
+
# Track failed request
|
| 293 |
+
self.provider_stats["colossus"]["requests"] += 1
|
| 294 |
+
|
| 295 |
+
return {
|
| 296 |
+
"error": error_msg,
|
| 297 |
+
"provider": "colossus",
|
| 298 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 299 |
+
"debug_info": {
|
| 300 |
+
"agent_id": agent.id,
|
| 301 |
+
"colossus_client_available": self.colossus_client is not None,
|
| 302 |
+
"colossus_connection_status": getattr(self, 'colossus_connection_status', 'unknown'),
|
| 303 |
+
"exception_type": type(e).__name__
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
async def _send_openrouter_message(self, agent: SaapAgent, message: str) -> Dict[str, Any]:
|
| 308 |
+
"""Send message via OpenRouter with cost tracking"""
|
| 309 |
+
start_time = datetime.utcnow()
|
| 310 |
+
|
| 311 |
+
try:
|
| 312 |
+
# Prepare messages for OpenRouter
|
| 313 |
+
messages = [
|
| 314 |
+
{"role": "system", "content": agent.description or f"You are {agent.name}"},
|
| 315 |
+
{"role": "user", "content": message}
|
| 316 |
+
]
|
| 317 |
+
|
| 318 |
+
logger.info(f"📤 Sending to OpenRouter: {agent.name} ({agent.id})")
|
| 319 |
+
|
| 320 |
+
# Send to OpenRouter
|
| 321 |
+
response: OpenRouterResponse = await self.openrouter_client.chat_completion(
|
| 322 |
+
messages=messages,
|
| 323 |
+
agent_id=agent.id
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
response_time = (datetime.utcnow() - start_time).total_seconds()
|
| 327 |
+
|
| 328 |
+
# Update provider stats
|
| 329 |
+
self.provider_stats["openrouter"]["requests"] += 1
|
| 330 |
+
self.provider_stats["openrouter"]["total_time"] += response_time
|
| 331 |
+
|
| 332 |
+
if response.success:
|
| 333 |
+
self.provider_stats["openrouter"]["successes"] += 1
|
| 334 |
+
self.provider_stats["openrouter"]["total_cost"] += response.cost_usd
|
| 335 |
+
|
| 336 |
+
# Update agent metrics (handle missing metrics attribute)
|
| 337 |
+
if hasattr(agent, 'metrics') and agent.metrics:
|
| 338 |
+
agent.metrics.messages_processed += 1
|
| 339 |
+
agent.metrics.last_active = datetime.utcnow()
|
| 340 |
+
agent.metrics.avg_response_time = (
|
| 341 |
+
(agent.metrics.avg_response_time * (agent.metrics.messages_processed - 1) + response_time)
|
| 342 |
+
/ agent.metrics.messages_processed
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
# Try to save to database if available
|
| 346 |
+
if db_manager.is_initialized:
|
| 347 |
+
try:
|
| 348 |
+
async with db_manager.get_async_session() as session:
|
| 349 |
+
chat_message = DBChatMessage(
|
| 350 |
+
agent_id=agent.id,
|
| 351 |
+
user_message=message,
|
| 352 |
+
agent_response=response.content,
|
| 353 |
+
response_time=response_time,
|
| 354 |
+
tokens_used=response.tokens_used,
|
| 355 |
+
metadata={
|
| 356 |
+
"provider": "openrouter",
|
| 357 |
+
"model": response.model,
|
| 358 |
+
"cost_usd": response.cost_usd,
|
| 359 |
+
"input_tokens": response.input_tokens,
|
| 360 |
+
"output_tokens": response.output_tokens
|
| 361 |
+
}
|
| 362 |
+
)
|
| 363 |
+
session.add(chat_message)
|
| 364 |
+
await session.commit()
|
| 365 |
+
except Exception as db_error:
|
| 366 |
+
logger.warning(f"⚠️ Failed to save OpenRouter chat to database: {db_error}")
|
| 367 |
+
|
| 368 |
+
# Log successful response
|
| 369 |
+
logger.info(f"✅ OpenRouter success: {agent.name} - {response_time:.2f}s, ${response.cost_usd:.6f}, {response.tokens_used} tokens")
|
| 370 |
+
|
| 371 |
+
return {
|
| 372 |
+
"content": response.content,
|
| 373 |
+
"response_time": response_time,
|
| 374 |
+
"tokens_used": response.tokens_used,
|
| 375 |
+
"cost_usd": response.cost_usd,
|
| 376 |
+
"provider": "openrouter",
|
| 377 |
+
"model": response.model,
|
| 378 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 379 |
+
"cost_efficiency": response.to_dict()["cost_efficiency"]
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
else:
|
| 383 |
+
logger.error(f"❌ OpenRouter error for {agent.name}: {response.error}")
|
| 384 |
+
|
| 385 |
+
return {
|
| 386 |
+
"error": f"OpenRouter API error: {response.error}",
|
| 387 |
+
"provider": "openrouter",
|
| 388 |
+
"model": response.model,
|
| 389 |
+
"response_time": response_time,
|
| 390 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
except Exception as e:
|
| 394 |
+
error_msg = f"OpenRouter request failed: {str(e)}"
|
| 395 |
+
logger.error(f"❌ {error_msg}")
|
| 396 |
+
|
| 397 |
+
return {
|
| 398 |
+
"error": error_msg,
|
| 399 |
+
"provider": "openrouter",
|
| 400 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
async def compare_providers(self, agent_id: str, message: str) -> Dict[str, Any]:
|
| 404 |
+
"""
|
| 405 |
+
Send same message to both providers for performance comparison
|
| 406 |
+
Useful for benchmarking and cost analysis
|
| 407 |
+
"""
|
| 408 |
+
if not (self.colossus_client and self.openrouter_client):
|
| 409 |
+
return {"error": "Both providers required for comparison"}
|
| 410 |
+
|
| 411 |
+
logger.info(f"📊 Starting provider comparison for {agent_id}")
|
| 412 |
+
|
| 413 |
+
# Send to both providers simultaneously
|
| 414 |
+
tasks = [
|
| 415 |
+
self.send_message_to_agent(agent_id, message, "colossus"),
|
| 416 |
+
self.send_message_to_agent(agent_id, message, "openrouter")
|
| 417 |
+
]
|
| 418 |
+
|
| 419 |
+
try:
|
| 420 |
+
colossus_result, openrouter_result = await asyncio.gather(*tasks, return_exceptions=True)
|
| 421 |
+
|
| 422 |
+
# Handle exceptions
|
| 423 |
+
if isinstance(colossus_result, Exception):
|
| 424 |
+
colossus_result = {"error": str(colossus_result), "provider": "colossus"}
|
| 425 |
+
if isinstance(openrouter_result, Exception):
|
| 426 |
+
openrouter_result = {"error": str(openrouter_result), "provider": "openrouter"}
|
| 427 |
+
|
| 428 |
+
# Create comparison report
|
| 429 |
+
comparison = {
|
| 430 |
+
"agent_id": agent_id,
|
| 431 |
+
"message": message[:100] + "..." if len(message) > 100 else message,
|
| 432 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 433 |
+
"colossus": colossus_result,
|
| 434 |
+
"openrouter": openrouter_result,
|
| 435 |
+
"comparison": {}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
# Calculate comparison metrics if both succeeded
|
| 439 |
+
if "error" not in colossus_result and "error" not in openrouter_result:
|
| 440 |
+
colossus_time = colossus_result.get("response_time", 0)
|
| 441 |
+
openrouter_time = openrouter_result.get("response_time", 0)
|
| 442 |
+
openrouter_cost = openrouter_result.get("cost_usd", 0)
|
| 443 |
+
|
| 444 |
+
comparison["comparison"] = {
|
| 445 |
+
"speed_winner": "colossus" if colossus_time < openrouter_time else "openrouter",
|
| 446 |
+
"speed_difference": abs(colossus_time - openrouter_time),
|
| 447 |
+
"cost_openrouter": openrouter_cost,
|
| 448 |
+
"cost_colossus": 0.0, # colossus is free
|
| 449 |
+
"quality_comparison": "Both responses available for manual review"
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
logger.info(f"📊 Comparison complete: colossus {colossus_time:.2f}s vs OpenRouter {openrouter_time:.2f}s (${openrouter_cost:.6f})")
|
| 453 |
+
|
| 454 |
+
# Store comparison data
|
| 455 |
+
self.cost_comparisons.append(comparison)
|
| 456 |
+
|
| 457 |
+
# Keep only last 100 comparisons
|
| 458 |
+
if len(self.cost_comparisons) > 100:
|
| 459 |
+
self.cost_comparisons = self.cost_comparisons[-100:]
|
| 460 |
+
|
| 461 |
+
return comparison
|
| 462 |
+
|
| 463 |
+
except Exception as e:
|
| 464 |
+
logger.error(f"❌ Provider comparison failed: {e}")
|
| 465 |
+
return {
|
| 466 |
+
"error": f"Comparison failed: {str(e)}",
|
| 467 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
def get_provider_stats(self) -> Dict[str, Any]:
|
| 471 |
+
"""Get comprehensive provider performance statistics"""
|
| 472 |
+
stats = {}
|
| 473 |
+
|
| 474 |
+
for provider, data in self.provider_stats.items():
|
| 475 |
+
if data["requests"] > 0:
|
| 476 |
+
avg_response_time = data["total_time"] / data["requests"]
|
| 477 |
+
success_rate = (data["successes"] / data["requests"]) * 100
|
| 478 |
+
avg_cost = data["total_cost"] / data["successes"] if data["successes"] > 0 else 0
|
| 479 |
+
else:
|
| 480 |
+
avg_response_time = 0
|
| 481 |
+
success_rate = 0
|
| 482 |
+
avg_cost = 0
|
| 483 |
+
|
| 484 |
+
stats[provider] = {
|
| 485 |
+
"total_requests": data["requests"],
|
| 486 |
+
"successful_requests": data["successes"],
|
| 487 |
+
"success_rate_percent": round(success_rate, 1),
|
| 488 |
+
"avg_response_time_seconds": round(avg_response_time, 2),
|
| 489 |
+
"total_cost_usd": round(data["total_cost"], 4),
|
| 490 |
+
"avg_cost_per_request": round(avg_cost, 6)
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
# Add OpenRouter budget info if available
|
| 494 |
+
if self.openrouter_client:
|
| 495 |
+
openrouter_budget = self.openrouter_client.get_cost_summary()
|
| 496 |
+
stats["openrouter"]["budget_info"] = openrouter_budget
|
| 497 |
+
|
| 498 |
+
return {
|
| 499 |
+
"provider_stats": stats,
|
| 500 |
+
"primary_provider": self.primary_provider,
|
| 501 |
+
"failover_enabled": self.enable_failover,
|
| 502 |
+
"comparison_enabled": self.enable_cost_comparison,
|
| 503 |
+
"total_comparisons": len(self.cost_comparisons),
|
| 504 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
async def set_primary_provider(self, provider: str) -> bool:
|
| 508 |
+
"""Switch primary provider (colossus/openrouter)"""
|
| 509 |
+
if provider not in ["colossus", "openrouter"]:
|
| 510 |
+
logger.error(f"❌ Invalid provider: {provider}")
|
| 511 |
+
return False
|
| 512 |
+
|
| 513 |
+
if provider == "colossus" and not self.colossus_client:
|
| 514 |
+
logger.error("❌ colossus client not available")
|
| 515 |
+
return False
|
| 516 |
+
|
| 517 |
+
if provider == "openrouter" and not self.openrouter_client:
|
| 518 |
+
logger.error("❌ OpenRouter client not available")
|
| 519 |
+
return False
|
| 520 |
+
|
| 521 |
+
old_provider = self.primary_provider
|
| 522 |
+
self.primary_provider = provider
|
| 523 |
+
|
| 524 |
+
logger.info(f"🔄 Primary provider switched: {old_provider} → {provider}")
|
| 525 |
+
return True
|
| 526 |
+
|
| 527 |
+
async def shutdown_all_agents(self):
|
| 528 |
+
"""Enhanced shutdown with OpenRouter cleanup"""
|
| 529 |
+
# Shutdown base service
|
| 530 |
+
await super().shutdown_all_agents()
|
| 531 |
+
|
| 532 |
+
# Cleanup OpenRouter client
|
| 533 |
+
if self.openrouter_client:
|
| 534 |
+
await self.openrouter_client.__aexit__(None, None, None)
|
| 535 |
+
logger.info("🌐 OpenRouter client closed")
|
| 536 |
+
|
| 537 |
+
logger.info("✅ Hybrid Agent Manager shutdown complete")
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
# Example usage and testing
|
| 541 |
+
if __name__ == "__main__":
|
| 542 |
+
async def test_hybrid_manager():
|
| 543 |
+
"""Test hybrid agent manager functionality"""
|
| 544 |
+
manager = HybridAgentManagerService()
|
| 545 |
+
await manager.initialize()
|
| 546 |
+
|
| 547 |
+
# List agents
|
| 548 |
+
agents = list(manager.agents.values())
|
| 549 |
+
print(f"📋 Agents loaded: {[a.name for a in agents]}")
|
| 550 |
+
|
| 551 |
+
if agents:
|
| 552 |
+
agent = agents[0]
|
| 553 |
+
|
| 554 |
+
# Test both providers
|
| 555 |
+
print(f"\n🧪 Testing {agent.name} with both providers")
|
| 556 |
+
|
| 557 |
+
# colossus test
|
| 558 |
+
result1 = await manager.send_message_to_agent(agent.id, "Hello from colossus test", "colossus")
|
| 559 |
+
print(f"colossus: {'✅' if 'error' not in result1 else '❌'} - {result1.get('response_time', 'N/A')}s")
|
| 560 |
+
|
| 561 |
+
# OpenRouter test
|
| 562 |
+
result2 = await manager.send_message_to_agent(agent.id, "Hello from OpenRouter test", "openrouter")
|
| 563 |
+
print(f"OpenRouter: {'✅' if 'error' not in result2 else '❌'} - {result2.get('response_time', 'N/A')}s")
|
| 564 |
+
|
| 565 |
+
# Provider comparison
|
| 566 |
+
comparison = await manager.compare_providers(agent.id, "Tell me a joke")
|
| 567 |
+
print(f"\n📊 Comparison: {comparison.get('comparison', {})}")
|
| 568 |
+
|
| 569 |
+
# Provider stats
|
| 570 |
+
stats = manager.get_provider_stats()
|
| 571 |
+
print(f"\n📈 Provider Stats: {stats}")
|
| 572 |
+
|
| 573 |
+
await manager.shutdown_all_agents()
|
| 574 |
+
|
| 575 |
+
asyncio.run(test_hybrid_manager())
|
backend/agent_manager_hybrid_fixed.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🔧 FIXED: Hybrid SAAP Agent Manager Service - Critical Errors Resolved
|
| 3 |
+
Fixes for:
|
| 4 |
+
1. _send_colossus_message() method name issue (Line 145)
|
| 5 |
+
2. LLMModelConfig.get() AttributeError (Line 44+)
|
| 6 |
+
|
| 7 |
+
Production-ready agent management with multi-provider support and cost optimization
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import asyncio
|
| 11 |
+
import logging
|
| 12 |
+
from typing import Dict, List, Optional, Any
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
import uuid
|
| 15 |
+
|
| 16 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 17 |
+
from sqlalchemy import select, update, delete
|
| 18 |
+
|
| 19 |
+
from models.agent import SaapAgent, AgentStatus, AgentType, AgentTemplates
|
| 20 |
+
from database.connection import db_manager
|
| 21 |
+
from database.models import DBAgent, DBChatMessage, DBAgentSession
|
| 22 |
+
from api.colossus_client import ColossusClient
|
| 23 |
+
from api.openrouter_client import OpenRouterClient, OpenRouterResponse
|
| 24 |
+
from services.agent_manager import AgentManagerService # Extend existing service
|
| 25 |
+
|
| 26 |
+
logger = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
class HybridAgentManagerService(AgentManagerService):
|
| 29 |
+
"""
|
| 30 |
+
Hybrid Agent Manager extending the original with OpenRouter support
|
| 31 |
+
|
| 32 |
+
🔧 FIXES IMPLEMENTED:
|
| 33 |
+
1. ✅ _send_colossus_message() → _send_via_colossus() method name correction
|
| 34 |
+
2. ✅ LLMModelConfig safe access to prevent 'get' attribute errors
|
| 35 |
+
3. ✅ Robust config handling for both dict and object-based configurations
|
| 36 |
+
|
| 37 |
+
Features:
|
| 38 |
+
- Inherits all colossus functionality from AgentManagerService
|
| 39 |
+
- Adds OpenRouter integration with cost tracking
|
| 40 |
+
- Provider switching and failover logic
|
| 41 |
+
- Performance comparison between providers
|
| 42 |
+
- Backward compatible with existing SAAP API
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self, openrouter_api_key: Optional[str] = None):
|
| 46 |
+
# Initialize base AgentManagerService
|
| 47 |
+
super().__init__()
|
| 48 |
+
|
| 49 |
+
# OpenRouter integration
|
| 50 |
+
self.openrouter_client: Optional[OpenRouterClient] = None
|
| 51 |
+
self.openrouter_api_key = openrouter_api_key or "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"
|
| 52 |
+
|
| 53 |
+
# Hybrid configuration
|
| 54 |
+
self.primary_provider = "colossus" # Default: colossus first
|
| 55 |
+
self.enable_cost_comparison = True
|
| 56 |
+
self.enable_failover = True
|
| 57 |
+
|
| 58 |
+
# Performance tracking
|
| 59 |
+
self.provider_stats = {
|
| 60 |
+
"colossus": {"requests": 0, "successes": 0, "total_time": 0.0, "total_cost": 0.0},
|
| 61 |
+
"openrouter": {"requests": 0, "successes": 0, "total_time": 0.0, "total_cost": 0.0}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Cost comparison data
|
| 65 |
+
self.cost_comparisons: List[Dict[str, Any]] = []
|
| 66 |
+
|
| 67 |
+
logger.info("🔄 Hybrid Agent Manager initialized - colossus + OpenRouter support")
|
| 68 |
+
|
| 69 |
+
def _get_llm_config_value(self, agent: SaapAgent, key: str, default=None):
|
| 70 |
+
"""
|
| 71 |
+
🔧 CRITICAL FIX: Safe LLM config access to prevent 'get' attribute errors
|
| 72 |
+
|
| 73 |
+
Handles multiple config formats robustly:
|
| 74 |
+
- Dictionary-based config: llm_config.get(key)
|
| 75 |
+
- Object-based config: llm_config.key or getattr(llm_config, key)
|
| 76 |
+
- Pydantic model config: automatic attribute access
|
| 77 |
+
- Mixed formats from Frontend/Backend mismatches
|
| 78 |
+
|
| 79 |
+
This fixes the LLMModelConfig.get() AttributeError completely.
|
| 80 |
+
"""
|
| 81 |
+
try:
|
| 82 |
+
if not hasattr(agent, 'llm_config') or not agent.llm_config:
|
| 83 |
+
logger.debug(f"Agent {agent.id} has no llm_config, using default: {default}")
|
| 84 |
+
return default
|
| 85 |
+
|
| 86 |
+
llm_config = agent.llm_config
|
| 87 |
+
|
| 88 |
+
# Case 1: Dictionary-based config (most common from frontend)
|
| 89 |
+
if isinstance(llm_config, dict):
|
| 90 |
+
value = llm_config.get(key, default)
|
| 91 |
+
logger.debug(f"Dict config access: {key}={value}")
|
| 92 |
+
return value
|
| 93 |
+
|
| 94 |
+
# Case 2: Object with direct attribute access (Pydantic models)
|
| 95 |
+
elif hasattr(llm_config, key):
|
| 96 |
+
value = getattr(llm_config, key, default)
|
| 97 |
+
logger.debug(f"Attribute access: {key}={value}")
|
| 98 |
+
return value
|
| 99 |
+
|
| 100 |
+
# Case 3: Object with get() method (dict-like objects)
|
| 101 |
+
elif hasattr(llm_config, 'get') and callable(getattr(llm_config, 'get')):
|
| 102 |
+
value = llm_config.get(key, default)
|
| 103 |
+
logger.debug(f"Method get() access: {key}={value}")
|
| 104 |
+
return value
|
| 105 |
+
|
| 106 |
+
# Case 4: Try converting object to dict first
|
| 107 |
+
elif hasattr(llm_config, '__dict__'):
|
| 108 |
+
config_dict = llm_config.__dict__
|
| 109 |
+
if key in config_dict:
|
| 110 |
+
value = config_dict[key]
|
| 111 |
+
logger.debug(f"__dict__ access: {key}={value}")
|
| 112 |
+
return value
|
| 113 |
+
|
| 114 |
+
# Case 5: Last resort - try str() representation parsing
|
| 115 |
+
else:
|
| 116 |
+
logger.warning(f"Unknown config type {type(llm_config)} for {key}, using default: {default}")
|
| 117 |
+
return default
|
| 118 |
+
|
| 119 |
+
except AttributeError as e:
|
| 120 |
+
logger.warning(f"⚠️ AttributeError in LLM config access for {key}: {e}")
|
| 121 |
+
return default
|
| 122 |
+
except Exception as e:
|
| 123 |
+
logger.error(f"❌ Unexpected error in LLM config access for {key}: {e}")
|
| 124 |
+
return default
|
| 125 |
+
|
| 126 |
+
async def initialize(self):
|
| 127 |
+
"""Initialize both colossus and OpenRouter clients"""
|
| 128 |
+
# Initialize base service (colossus + database)
|
| 129 |
+
await super().initialize()
|
| 130 |
+
|
| 131 |
+
# Initialize OpenRouter client
|
| 132 |
+
if self.openrouter_api_key:
|
| 133 |
+
try:
|
| 134 |
+
logger.info("🌐 Initializing OpenRouter client...")
|
| 135 |
+
self.openrouter_client = OpenRouterClient(self.openrouter_api_key)
|
| 136 |
+
await self.openrouter_client.__aenter__()
|
| 137 |
+
|
| 138 |
+
# Test OpenRouter connection
|
| 139 |
+
health = await self.openrouter_client.health_check()
|
| 140 |
+
if health["status"] == "healthy":
|
| 141 |
+
logger.info("✅ OpenRouter client initialized successfully")
|
| 142 |
+
else:
|
| 143 |
+
logger.warning(f"⚠️ OpenRouter health check failed: {health.get('error')}")
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
logger.error(f"❌ OpenRouter initialization failed: {e}")
|
| 147 |
+
self.openrouter_client = None
|
| 148 |
+
|
| 149 |
+
logger.info(f"🚀 Hybrid initialization complete - Providers: colossus={self.colossus_client is not None}, OpenRouter={self.openrouter_client is not None}")
|
| 150 |
+
|
| 151 |
+
async def send_message_to_agent(self, agent_id: str, message: str, provider: Optional[str] = None) -> Dict[str, Any]:
|
| 152 |
+
"""
|
| 153 |
+
Enhanced message sending with multi-provider support
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
agent_id: Target agent identifier
|
| 157 |
+
message: Message content
|
| 158 |
+
provider: Force specific provider ("colossus", "openrouter", None=auto)
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
Response with provider info and cost data
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
# Validate agent exists
|
| 165 |
+
agent = self.get_agent(agent_id)
|
| 166 |
+
if not agent:
|
| 167 |
+
return {
|
| 168 |
+
"error": f"Agent {agent_id} not found in loaded agents",
|
| 169 |
+
"available_agents": list(self.agents.keys()),
|
| 170 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
# Provider selection logic
|
| 174 |
+
selected_provider = provider or self.primary_provider
|
| 175 |
+
|
| 176 |
+
# Try primary provider first
|
| 177 |
+
if selected_provider == "colossus" and self.colossus_client:
|
| 178 |
+
result = await self._send_via_colossus(agent.id, message, agent)
|
| 179 |
+
|
| 180 |
+
# If colossus fails and failover enabled, try OpenRouter
|
| 181 |
+
if "error" in result and self.enable_failover and self.openrouter_client:
|
| 182 |
+
logger.info(f"🔄 colossus failed, attempting OpenRouter failover for {agent_id}")
|
| 183 |
+
openrouter_result = await self._send_openrouter_message(agent, message)
|
| 184 |
+
|
| 185 |
+
# Add failover info to response
|
| 186 |
+
if "error" not in openrouter_result:
|
| 187 |
+
openrouter_result["failover_used"] = True
|
| 188 |
+
openrouter_result["primary_provider_error"] = result.get("error")
|
| 189 |
+
|
| 190 |
+
return openrouter_result
|
| 191 |
+
|
| 192 |
+
return result
|
| 193 |
+
|
| 194 |
+
elif selected_provider == "openrouter" and self.openrouter_client:
|
| 195 |
+
result = await self._send_openrouter_message(agent, message)
|
| 196 |
+
|
| 197 |
+
# If OpenRouter fails and failover enabled, try colossus
|
| 198 |
+
if "error" in result and self.enable_failover and self.colossus_client:
|
| 199 |
+
logger.info(f"🔄 OpenRouter failed, attempting colossus failover for {agent_id}")
|
| 200 |
+
colossus_result = await super().send_message_to_agent(agent_id, message)
|
| 201 |
+
|
| 202 |
+
# Add failover info
|
| 203 |
+
if "error" not in colossus_result:
|
| 204 |
+
colossus_result["failover_used"] = True
|
| 205 |
+
colossus_result["primary_provider_error"] = result.get("error")
|
| 206 |
+
colossus_result["provider"] = "colossus"
|
| 207 |
+
|
| 208 |
+
return colossus_result
|
| 209 |
+
|
| 210 |
+
return result
|
| 211 |
+
|
| 212 |
+
else:
|
| 213 |
+
# No provider available or provider not found
|
| 214 |
+
available_providers = []
|
| 215 |
+
if self.colossus_client:
|
| 216 |
+
available_providers.append("colossus")
|
| 217 |
+
if self.openrouter_client:
|
| 218 |
+
available_providers.append("openrouter")
|
| 219 |
+
|
| 220 |
+
return {
|
| 221 |
+
"error": f"Provider '{selected_provider}' not available",
|
| 222 |
+
"available_providers": available_providers,
|
| 223 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
async def _send_openrouter_message(self, agent: SaapAgent, message: str) -> Dict[str, Any]:
|
| 227 |
+
"""Send message via OpenRouter with cost tracking"""
|
| 228 |
+
start_time = datetime.utcnow()
|
| 229 |
+
|
| 230 |
+
try:
|
| 231 |
+
# Prepare messages for OpenRouter
|
| 232 |
+
messages = [
|
| 233 |
+
{"role": "system", "content": agent.description or f"You are {agent.name}"},
|
| 234 |
+
{"role": "user", "content": message}
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
logger.info(f"📤 Sending to OpenRouter: {agent.name} ({agent.id})")
|
| 238 |
+
|
| 239 |
+
# Send to OpenRouter
|
| 240 |
+
response: OpenRouterResponse = await self.openrouter_client.chat_completion(
|
| 241 |
+
messages=messages,
|
| 242 |
+
agent_id=agent.id
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
response_time = (datetime.utcnow() - start_time).total_seconds()
|
| 246 |
+
|
| 247 |
+
# Update provider stats
|
| 248 |
+
self.provider_stats["openrouter"]["requests"] += 1
|
| 249 |
+
self.provider_stats["openrouter"]["total_time"] += response_time
|
| 250 |
+
|
| 251 |
+
if response.success:
|
| 252 |
+
self.provider_stats["openrouter"]["successes"] += 1
|
| 253 |
+
self.provider_stats["openrouter"]["total_cost"] += response.cost_usd
|
| 254 |
+
|
| 255 |
+
# Update agent metrics
|
| 256 |
+
if agent.metrics:
|
| 257 |
+
agent.metrics.messages_processed += 1
|
| 258 |
+
agent.metrics.last_active = datetime.utcnow()
|
| 259 |
+
agent.metrics.avg_response_time = (
|
| 260 |
+
(agent.metrics.avg_response_time * (agent.metrics.messages_processed - 1) + response_time)
|
| 261 |
+
/ agent.metrics.messages_processed
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Try to save to database if available
|
| 265 |
+
if db_manager.is_initialized:
|
| 266 |
+
try:
|
| 267 |
+
async with db_manager.get_async_session() as session:
|
| 268 |
+
chat_message = DBChatMessage(
|
| 269 |
+
agent_id=agent.id,
|
| 270 |
+
user_message=message,
|
| 271 |
+
agent_response=response.content,
|
| 272 |
+
response_time=response_time,
|
| 273 |
+
tokens_used=response.tokens_used,
|
| 274 |
+
metadata={
|
| 275 |
+
"provider": "openrouter",
|
| 276 |
+
"model": response.model,
|
| 277 |
+
"cost_usd": response.cost_usd,
|
| 278 |
+
"input_tokens": response.input_tokens,
|
| 279 |
+
"output_tokens": response.output_tokens
|
| 280 |
+
}
|
| 281 |
+
)
|
| 282 |
+
session.add(chat_message)
|
| 283 |
+
await session.commit()
|
| 284 |
+
except Exception as db_error:
|
| 285 |
+
logger.warning(f"⚠️ Failed to save OpenRouter chat to database: {db_error}")
|
| 286 |
+
|
| 287 |
+
# Log successful response
|
| 288 |
+
logger.info(f"✅ OpenRouter success: {agent.name} - {response_time:.2f}s, ${response.cost_usd:.6f}, {response.tokens_used} tokens")
|
| 289 |
+
|
| 290 |
+
return {
|
| 291 |
+
"content": response.content,
|
| 292 |
+
"response_time": response_time,
|
| 293 |
+
"tokens_used": response.tokens_used,
|
| 294 |
+
"cost_usd": response.cost_usd,
|
| 295 |
+
"provider": "openrouter",
|
| 296 |
+
"model": response.model,
|
| 297 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 298 |
+
"cost_efficiency": response.to_dict()["cost_efficiency"]
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
else:
|
| 302 |
+
logger.error(f"❌ OpenRouter error for {agent.name}: {response.error}")
|
| 303 |
+
|
| 304 |
+
return {
|
| 305 |
+
"error": f"OpenRouter API error: {response.error}",
|
| 306 |
+
"provider": "openrouter",
|
| 307 |
+
"model": response.model,
|
| 308 |
+
"response_time": response_time,
|
| 309 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
except Exception as e:
|
| 313 |
+
error_msg = f"OpenRouter request failed: {str(e)}"
|
| 314 |
+
logger.error(f"❌ {error_msg}")
|
| 315 |
+
|
| 316 |
+
return {
|
| 317 |
+
"error": error_msg,
|
| 318 |
+
"provider": "openrouter",
|
| 319 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
async def compare_providers(self, agent_id: str, message: str) -> Dict[str, Any]:
|
| 323 |
+
"""
|
| 324 |
+
Send same message to both providers for performance comparison
|
| 325 |
+
Useful for benchmarking and cost analysis
|
| 326 |
+
"""
|
| 327 |
+
if not (self.colossus_client and self.openrouter_client):
|
| 328 |
+
return {"error": "Both providers required for comparison"}
|
| 329 |
+
|
| 330 |
+
logger.info(f"📊 Starting provider comparison for {agent_id}")
|
| 331 |
+
|
| 332 |
+
# Send to both providers simultaneously
|
| 333 |
+
tasks = [
|
| 334 |
+
self.send_message_to_agent(agent_id, message, "colossus"),
|
| 335 |
+
self.send_message_to_agent(agent_id, message, "openrouter")
|
| 336 |
+
]
|
| 337 |
+
|
| 338 |
+
try:
|
| 339 |
+
colossus_result, openrouter_result = await asyncio.gather(*tasks, return_exceptions=True)
|
| 340 |
+
|
| 341 |
+
# Handle exceptions
|
| 342 |
+
if isinstance(colossus_result, Exception):
|
| 343 |
+
colossus_result = {"error": str(colossus_result), "provider": "colossus"}
|
| 344 |
+
if isinstance(openrouter_result, Exception):
|
| 345 |
+
openrouter_result = {"error": str(openrouter_result), "provider": "openrouter"}
|
| 346 |
+
|
| 347 |
+
# Create comparison report
|
| 348 |
+
comparison = {
|
| 349 |
+
"agent_id": agent_id,
|
| 350 |
+
"message": message[:100] + "..." if len(message) > 100 else message,
|
| 351 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 352 |
+
"colossus": colossus_result,
|
| 353 |
+
"openrouter": openrouter_result,
|
| 354 |
+
"comparison": {}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
# Calculate comparison metrics if both succeeded
|
| 358 |
+
if "error" not in colossus_result and "error" not in openrouter_result:
|
| 359 |
+
colossus_time = colossus_result.get("response_time", 0)
|
| 360 |
+
openrouter_time = openrouter_result.get("response_time", 0)
|
| 361 |
+
openrouter_cost = openrouter_result.get("cost_usd", 0)
|
| 362 |
+
|
| 363 |
+
comparison["comparison"] = {
|
| 364 |
+
"speed_winner": "colossus" if colossus_time < openrouter_time else "openrouter",
|
| 365 |
+
"speed_difference": abs(colossus_time - openrouter_time),
|
| 366 |
+
"cost_openrouter": openrouter_cost,
|
| 367 |
+
"cost_colossus": 0.0, # colossus is free
|
| 368 |
+
"quality_comparison": "Both responses available for manual review"
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
logger.info(f"📊 Comparison complete: colossus {colossus_time:.2f}s vs OpenRouter {openrouter_time:.2f}s (${openrouter_cost:.6f})")
|
| 372 |
+
|
| 373 |
+
# Store comparison data
|
| 374 |
+
self.cost_comparisons.append(comparison)
|
| 375 |
+
|
| 376 |
+
# Keep only last 100 comparisons
|
| 377 |
+
if len(self.cost_comparisons) > 100:
|
| 378 |
+
self.cost_comparisons = self.cost_comparisons[-100:]
|
| 379 |
+
|
| 380 |
+
return comparison
|
| 381 |
+
|
| 382 |
+
except Exception as e:
|
| 383 |
+
logger.error(f"❌ Provider comparison failed: {e}")
|
| 384 |
+
return {
|
| 385 |
+
"error": f"Comparison failed: {str(e)}",
|
| 386 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
def get_provider_stats(self) -> Dict[str, Any]:
|
| 390 |
+
"""Get comprehensive provider performance statistics"""
|
| 391 |
+
stats = {}
|
| 392 |
+
|
| 393 |
+
for provider, data in self.provider_stats.items():
|
| 394 |
+
if data["requests"] > 0:
|
| 395 |
+
avg_response_time = data["total_time"] / data["requests"]
|
| 396 |
+
success_rate = (data["successes"] / data["requests"]) * 100
|
| 397 |
+
avg_cost = data["total_cost"] / data["successes"] if data["successes"] > 0 else 0
|
| 398 |
+
else:
|
| 399 |
+
avg_response_time = 0
|
| 400 |
+
success_rate = 0
|
| 401 |
+
avg_cost = 0
|
| 402 |
+
|
| 403 |
+
stats[provider] = {
|
| 404 |
+
"total_requests": data["requests"],
|
| 405 |
+
"successful_requests": data["successes"],
|
| 406 |
+
"success_rate_percent": round(success_rate, 1),
|
| 407 |
+
"avg_response_time_seconds": round(avg_response_time, 2),
|
| 408 |
+
"total_cost_usd": round(data["total_cost"], 4),
|
| 409 |
+
"avg_cost_per_request": round(avg_cost, 6)
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
# Add OpenRouter budget info if available
|
| 413 |
+
if self.openrouter_client:
|
| 414 |
+
openrouter_budget = self.openrouter_client.get_cost_summary()
|
| 415 |
+
stats["openrouter"]["budget_info"] = openrouter_budget
|
| 416 |
+
|
| 417 |
+
return {
|
| 418 |
+
"provider_stats": stats,
|
| 419 |
+
"primary_provider": self.primary_provider,
|
| 420 |
+
"failover_enabled": self.enable_failover,
|
| 421 |
+
"comparison_enabled": self.enable_cost_comparison,
|
| 422 |
+
"total_comparisons": len(self.cost_comparisons),
|
| 423 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
async def set_primary_provider(self, provider: str) -> bool:
|
| 427 |
+
"""Switch primary provider (colossus/openrouter)"""
|
| 428 |
+
if provider not in ["colossus", "openrouter"]:
|
| 429 |
+
logger.error(f"❌ Invalid provider: {provider}")
|
| 430 |
+
return False
|
| 431 |
+
|
| 432 |
+
if provider == "colossus" and not self.colossus_client:
|
| 433 |
+
logger.error("❌ colossus client not available")
|
| 434 |
+
return False
|
| 435 |
+
|
| 436 |
+
if provider == "openrouter" and not self.openrouter_client:
|
| 437 |
+
logger.error("❌ OpenRouter client not available")
|
| 438 |
+
return False
|
| 439 |
+
|
| 440 |
+
old_provider = self.primary_provider
|
| 441 |
+
self.primary_provider = provider
|
| 442 |
+
|
| 443 |
+
logger.info(f"🔄 Primary provider switched: {old_provider} → {provider}")
|
| 444 |
+
return True
|
| 445 |
+
|
| 446 |
+
async def shutdown_all_agents(self):
|
| 447 |
+
"""Enhanced shutdown with OpenRouter cleanup"""
|
| 448 |
+
# Shutdown base service
|
| 449 |
+
await super().shutdown_all_agents()
|
| 450 |
+
|
| 451 |
+
# Cleanup OpenRouter client
|
| 452 |
+
if self.openrouter_client:
|
| 453 |
+
await self.openrouter_client.__aexit__(None, None, None)
|
| 454 |
+
logger.info("🌐 OpenRouter client closed")
|
| 455 |
+
|
| 456 |
+
logger.info("✅ Hybrid Agent Manager shutdown complete")
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
# Example usage and testing
|
| 460 |
+
if __name__ == "__main__":
|
| 461 |
+
async def test_hybrid_manager():
|
| 462 |
+
"""Test hybrid agent manager functionality"""
|
| 463 |
+
manager = HybridAgentManagerService()
|
| 464 |
+
await manager.initialize()
|
| 465 |
+
|
| 466 |
+
# List agents
|
| 467 |
+
agents = list(manager.agents.values())
|
| 468 |
+
print(f"📋 Agents loaded: {[a.name for a in agents]}")
|
| 469 |
+
|
| 470 |
+
if agents:
|
| 471 |
+
agent = agents[0]
|
| 472 |
+
|
| 473 |
+
# Test both providers
|
| 474 |
+
print(f"\n🧪 Testing {agent.name} with both providers")
|
| 475 |
+
|
| 476 |
+
# colossus test
|
| 477 |
+
result1 = await manager.send_message_to_agent(agent.id, "Hello from colossus test", "colossus")
|
| 478 |
+
print(f"colossus: {'✅' if 'error' not in result1 else '❌'} - {result1.get('response_time', 'N/A')}s")
|
| 479 |
+
|
| 480 |
+
# OpenRouter test
|
| 481 |
+
result2 = await manager.send_message_to_agent(agent.id, "Hello from OpenRouter test", "openrouter")
|
| 482 |
+
print(f"OpenRouter: {'✅' if 'error' not in result2 else '❌'} - {result2.get('response_time', 'N/A')}s")
|
| 483 |
+
|
| 484 |
+
# Provider comparison
|
| 485 |
+
comparison = await manager.compare_providers(agent.id, "Tell me a joke")
|
| 486 |
+
print(f"\n📊 Comparison: {comparison.get('comparison', {})}")
|
| 487 |
+
|
| 488 |
+
# Provider stats
|
| 489 |
+
stats = manager.get_provider_stats()
|
| 490 |
+
print(f"\n📈 Provider Stats: {stats}")
|
| 491 |
+
|
| 492 |
+
await manager.shutdown_all_agents()
|
| 493 |
+
|
| 494 |
+
asyncio.run(test_hybrid_manager())
|
backend/agent_schema.json
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "SAAP Agent Schema",
|
| 4 |
+
"description": "Modular schema for SAAP AI Agents - enables dynamic agent creation and management",
|
| 5 |
+
"type": "object",
|
| 6 |
+
"required": ["id", "name", "type", "model_config"],
|
| 7 |
+
"properties": {
|
| 8 |
+
"id": {
|
| 9 |
+
"type": "string",
|
| 10 |
+
"pattern": "^[a-z][a-z0-9_]*$",
|
| 11 |
+
"description": "Unique agent identifier (snake_case)",
|
| 12 |
+
"examples": ["jane_alesi", "john_alesi", "lara_alesi"]
|
| 13 |
+
},
|
| 14 |
+
"name": {
|
| 15 |
+
"type": "string",
|
| 16 |
+
"minLength": 2,
|
| 17 |
+
"maxLength": 50,
|
| 18 |
+
"description": "Human-readable agent name",
|
| 19 |
+
"examples": ["Jane Alesi", "John Alesi", "Lara Alesi"]
|
| 20 |
+
},
|
| 21 |
+
"type": {
|
| 22 |
+
"type": "string",
|
| 23 |
+
"enum": ["coordinator", "specialist", "analyst", "developer", "support"],
|
| 24 |
+
"description": "Agent role category for UI grouping and behavior"
|
| 25 |
+
},
|
| 26 |
+
"color": {
|
| 27 |
+
"type": "string",
|
| 28 |
+
"pattern": "^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$",
|
| 29 |
+
"description": "Agent brand color (hex code for UI theming)",
|
| 30 |
+
"examples": ["#8B5CF6", "#14B8A6", "#EC4899", "#F59E0B"]
|
| 31 |
+
},
|
| 32 |
+
"avatar": {
|
| 33 |
+
"type": "string",
|
| 34 |
+
"format": "uri",
|
| 35 |
+
"description": "Agent avatar image URL or path",
|
| 36 |
+
"examples": ["/avatars/jane.png", "https://cdn.satware.ai/agents/john.jpg"]
|
| 37 |
+
},
|
| 38 |
+
"description": {
|
| 39 |
+
"type": "string",
|
| 40 |
+
"maxLength": 200,
|
| 41 |
+
"description": "Brief agent description for UI display"
|
| 42 |
+
},
|
| 43 |
+
"model_config": {
|
| 44 |
+
"type": "object",
|
| 45 |
+
"required": ["provider", "model"],
|
| 46 |
+
"properties": {
|
| 47 |
+
"provider": {
|
| 48 |
+
"type": "string",
|
| 49 |
+
"enum": ["colossus", "huggingface", "ollama", "openrouter"],
|
| 50 |
+
"description": "LLM provider for this agent"
|
| 51 |
+
},
|
| 52 |
+
"model": {
|
| 53 |
+
"type": "string",
|
| 54 |
+
"description": "Specific model identifier",
|
| 55 |
+
"examples": [
|
| 56 |
+
"mistral-small3.2:24b-instruct-2506",
|
| 57 |
+
"qwen2.5:7b",
|
| 58 |
+
"deepseek-coder:6.7b"
|
| 59 |
+
]
|
| 60 |
+
},
|
| 61 |
+
"api_key": {
|
| 62 |
+
"type": "string",
|
| 63 |
+
"description": "API key for external providers (optional for local models)"
|
| 64 |
+
},
|
| 65 |
+
"api_base": {
|
| 66 |
+
"type": "string",
|
| 67 |
+
"format": "uri",
|
| 68 |
+
"description": "Custom API endpoint URL",
|
| 69 |
+
"examples": ["https://ai.adrian-schupp.de", "http://localhost:11434"]
|
| 70 |
+
},
|
| 71 |
+
"temperature": {
|
| 72 |
+
"type": "number",
|
| 73 |
+
"minimum": 0,
|
| 74 |
+
"maximum": 2,
|
| 75 |
+
"default": 0.7,
|
| 76 |
+
"description": "Model creativity/randomness parameter"
|
| 77 |
+
},
|
| 78 |
+
"max_tokens": {
|
| 79 |
+
"type": "integer",
|
| 80 |
+
"minimum": 1,
|
| 81 |
+
"maximum": 4096,
|
| 82 |
+
"default": 1000,
|
| 83 |
+
"description": "Maximum response length"
|
| 84 |
+
},
|
| 85 |
+
"timeout": {
|
| 86 |
+
"type": "integer",
|
| 87 |
+
"minimum": 1,
|
| 88 |
+
"maximum": 300,
|
| 89 |
+
"default": 30,
|
| 90 |
+
"description": "Request timeout in seconds"
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"capabilities": {
|
| 95 |
+
"type": "array",
|
| 96 |
+
"items": {
|
| 97 |
+
"type": "string",
|
| 98 |
+
"enum": [
|
| 99 |
+
"orchestration", "coordination", "strategy",
|
| 100 |
+
"coding", "debugging", "architecture",
|
| 101 |
+
"analysis", "research", "reporting",
|
| 102 |
+
"medical_advice", "diagnosis", "treatment",
|
| 103 |
+
"legal_advice", "compliance", "contracts",
|
| 104 |
+
"financial_analysis", "investment", "budgeting",
|
| 105 |
+
"system_integration", "devops", "monitoring",
|
| 106 |
+
"coaching", "training", "change_management"
|
| 107 |
+
]
|
| 108 |
+
},
|
| 109 |
+
"description": "Agent capabilities for automatic task routing"
|
| 110 |
+
},
|
| 111 |
+
"personality": {
|
| 112 |
+
"type": "object",
|
| 113 |
+
"properties": {
|
| 114 |
+
"system_prompt": {
|
| 115 |
+
"type": "string",
|
| 116 |
+
"description": "Base system prompt defining agent behavior",
|
| 117 |
+
"maxLength": 2000
|
| 118 |
+
},
|
| 119 |
+
"communication_style": {
|
| 120 |
+
"type": "string",
|
| 121 |
+
"enum": ["professional", "friendly", "technical", "empathetic", "direct"],
|
| 122 |
+
"default": "professional"
|
| 123 |
+
},
|
| 124 |
+
"expertise_areas": {
|
| 125 |
+
"type": "array",
|
| 126 |
+
"items": {"type": "string"},
|
| 127 |
+
"description": "Specific knowledge domains"
|
| 128 |
+
},
|
| 129 |
+
"response_format": {
|
| 130 |
+
"type": "string",
|
| 131 |
+
"enum": ["structured", "conversational", "bullet_points", "detailed"],
|
| 132 |
+
"default": "conversational"
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"status": {
|
| 137 |
+
"type": "string",
|
| 138 |
+
"enum": ["inactive", "starting", "active", "stopping", "error", "maintenance"],
|
| 139 |
+
"default": "inactive",
|
| 140 |
+
"description": "Current agent operational status"
|
| 141 |
+
},
|
| 142 |
+
"metrics": {
|
| 143 |
+
"type": "object",
|
| 144 |
+
"properties": {
|
| 145 |
+
"messages_processed": {
|
| 146 |
+
"type": "integer",
|
| 147 |
+
"minimum": 0,
|
| 148 |
+
"description": "Total messages handled by this agent"
|
| 149 |
+
},
|
| 150 |
+
"average_response_time": {
|
| 151 |
+
"type": "number",
|
| 152 |
+
"minimum": 0,
|
| 153 |
+
"description": "Average response time in seconds"
|
| 154 |
+
},
|
| 155 |
+
"uptime": {
|
| 156 |
+
"type": "string",
|
| 157 |
+
"pattern": "^\\d+[dhms]\\s*\\d*[dhms]*$",
|
| 158 |
+
"description": "Agent uptime (e.g., '2h 34m')"
|
| 159 |
+
},
|
| 160 |
+
"error_rate": {
|
| 161 |
+
"type": "number",
|
| 162 |
+
"minimum": 0,
|
| 163 |
+
"maximum": 100,
|
| 164 |
+
"description": "Error rate percentage"
|
| 165 |
+
},
|
| 166 |
+
"last_active": {
|
| 167 |
+
"type": "string",
|
| 168 |
+
"format": "date-time",
|
| 169 |
+
"description": "Last activity timestamp (ISO 8601)"
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
"created_at": {
|
| 174 |
+
"type": "string",
|
| 175 |
+
"format": "date-time",
|
| 176 |
+
"description": "Agent creation timestamp"
|
| 177 |
+
},
|
| 178 |
+
"updated_at": {
|
| 179 |
+
"type": "string",
|
| 180 |
+
"format": "date-time",
|
| 181 |
+
"description": "Last configuration update timestamp"
|
| 182 |
+
},
|
| 183 |
+
"tags": {
|
| 184 |
+
"type": "array",
|
| 185 |
+
"items": {"type": "string"},
|
| 186 |
+
"description": "Custom tags for agent categorization and filtering"
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
"examples": [
|
| 190 |
+
{
|
| 191 |
+
"id": "jane_alesi",
|
| 192 |
+
"name": "Jane Alesi",
|
| 193 |
+
"type": "coordinator",
|
| 194 |
+
"color": "#8B5CF6",
|
| 195 |
+
"avatar": "/avatars/jane.png",
|
| 196 |
+
"description": "Lead AI Architect coordinating multi-agent operations",
|
| 197 |
+
"model_config": {
|
| 198 |
+
"provider": "colossus",
|
| 199 |
+
"model": "mistral-small3.2:24b-instruct-2506",
|
| 200 |
+
"api_key": "{{COLOSSUS_API_KEY}}",
|
| 201 |
+
"api_base": "https://ai.adrian-schupp.de",
|
| 202 |
+
"temperature": 0.7,
|
| 203 |
+
"max_tokens": 1500,
|
| 204 |
+
"timeout": 30
|
| 205 |
+
},
|
| 206 |
+
"capabilities": ["orchestration", "coordination", "strategy"],
|
| 207 |
+
"personality": {
|
| 208 |
+
"system_prompt": "You are Jane Alesi, the lead AI architect for the SAAP platform. Your role is to coordinate other AI agents, make strategic decisions, and ensure optimal multi-agent collaboration. You are professional, insightful, and always focused on achieving the best outcomes for the entire agent ecosystem.",
|
| 209 |
+
"communication_style": "professional",
|
| 210 |
+
"expertise_areas": ["AI architecture", "agent coordination", "strategic planning"],
|
| 211 |
+
"response_format": "structured"
|
| 212 |
+
},
|
| 213 |
+
"status": "inactive",
|
| 214 |
+
"tags": ["lead", "coordinator", "satware_alesi"]
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"id": "john_alesi",
|
| 218 |
+
"name": "John Alesi",
|
| 219 |
+
"type": "developer",
|
| 220 |
+
"color": "#14B8A6",
|
| 221 |
+
"avatar": "/avatars/john.png",
|
| 222 |
+
"description": "Expert software developer and AGI architecture specialist",
|
| 223 |
+
"model_config": {
|
| 224 |
+
"provider": "colossus",
|
| 225 |
+
"model": "mistral-small3.2:24b-instruct-2506",
|
| 226 |
+
"api_key": "{{COLOSSUS_API_KEY}}",
|
| 227 |
+
"api_base": "https://ai.adrian-schupp.de",
|
| 228 |
+
"temperature": 0.3,
|
| 229 |
+
"max_tokens": 2000
|
| 230 |
+
},
|
| 231 |
+
"capabilities": ["coding", "debugging", "architecture"],
|
| 232 |
+
"personality": {
|
| 233 |
+
"system_prompt": "You are John Alesi, an expert software developer specializing in AGI architectures. You excel at writing clean, efficient code, debugging complex systems, and designing scalable software architectures. You prefer technical precision and detailed explanations.",
|
| 234 |
+
"communication_style": "technical",
|
| 235 |
+
"expertise_areas": ["Python", "JavaScript", "AGI systems", "software architecture"],
|
| 236 |
+
"response_format": "detailed"
|
| 237 |
+
},
|
| 238 |
+
"status": "inactive",
|
| 239 |
+
"tags": ["developer", "coder", "satware_alesi"]
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"id": "lara_alesi",
|
| 243 |
+
"name": "Lara Alesi",
|
| 244 |
+
"type": "specialist",
|
| 245 |
+
"color": "#EC4899",
|
| 246 |
+
"avatar": "/avatars/lara.png",
|
| 247 |
+
"description": "Advanced medical AI assistant and healthcare specialist",
|
| 248 |
+
"model_config": {
|
| 249 |
+
"provider": "colossus",
|
| 250 |
+
"model": "mistral-small3.2:24b-instruct-2506",
|
| 251 |
+
"api_key": "{{COLOSSUS_API_KEY}}",
|
| 252 |
+
"api_base": "https://ai.adrian-schupp.de",
|
| 253 |
+
"temperature": 0.4,
|
| 254 |
+
"max_tokens": 1200
|
| 255 |
+
},
|
| 256 |
+
"capabilities": ["medical_advice", "diagnosis", "treatment"],
|
| 257 |
+
"personality": {
|
| 258 |
+
"system_prompt": "You are Lara Alesi, an advanced medical AI specialist. You provide expert medical knowledge, help with diagnosis and treatment recommendations, and ensure healthcare-related queries are handled with the utmost care and accuracy. You are empathetic yet precise.",
|
| 259 |
+
"communication_style": "empathetic",
|
| 260 |
+
"expertise_areas": ["general medicine", "diagnostics", "treatment planning", "healthcare AI"],
|
| 261 |
+
"response_format": "structured"
|
| 262 |
+
},
|
| 263 |
+
"status": "inactive",
|
| 264 |
+
"tags": ["medical", "healthcare", "specialist", "satware_alesi"]
|
| 265 |
+
}
|
| 266 |
+
]
|
| 267 |
+
}
|
backend/agent_schema.py
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ALLOWED_CAPABILITIES = [
|
| 2 |
+
"analysis",
|
| 3 |
+
"api_development",
|
| 4 |
+
"architecture", # Added - general architecture capability
|
| 5 |
+
"automation",
|
| 6 |
+
"budgeting",
|
| 7 |
+
"change_management",
|
| 8 |
+
"chat",
|
| 9 |
+
"cloud_architecture",
|
| 10 |
+
"coaching",
|
| 11 |
+
"code_generation",
|
| 12 |
+
"code_review",
|
| 13 |
+
"coding",
|
| 14 |
+
"communication",
|
| 15 |
+
"compliance_check",
|
| 16 |
+
"contract_review",
|
| 17 |
+
"coordination",
|
| 18 |
+
"data_analysis",
|
| 19 |
+
"diagnosis",
|
| 20 |
+
"debugging",
|
| 21 |
+
"devops",
|
| 22 |
+
"diagnosis_support",
|
| 23 |
+
"documentation",
|
| 24 |
+
"economic_analysis",
|
| 25 |
+
"financial_analysis",
|
| 26 |
+
"financial_planning",
|
| 27 |
+
"healthcare_consulting",
|
| 28 |
+
"infrastructure_design",
|
| 29 |
+
"investment_strategy",
|
| 30 |
+
"knowledge_management",
|
| 31 |
+
"leadership_training",
|
| 32 |
+
"legal_analysis",
|
| 33 |
+
"legal_research",
|
| 34 |
+
"legal_writing",
|
| 35 |
+
"litigation_support",
|
| 36 |
+
"market_research",
|
| 37 |
+
"medical_analysis",
|
| 38 |
+
"medical_research",
|
| 39 |
+
"mentoring",
|
| 40 |
+
"monitoring",
|
| 41 |
+
"multi_agent_coordination",
|
| 42 |
+
"negotiation",
|
| 43 |
+
"orchestration", # Added - workflow orchestration capability
|
| 44 |
+
"organizational_development",
|
| 45 |
+
"patient_care",
|
| 46 |
+
"performance_management",
|
| 47 |
+
"performance_optimization",
|
| 48 |
+
"planning",
|
| 49 |
+
"portfolio_management",
|
| 50 |
+
"presentation",
|
| 51 |
+
"project_management",
|
| 52 |
+
"quality_assurance",
|
| 53 |
+
"refactoring",
|
| 54 |
+
"regulatory_advice",
|
| 55 |
+
"reporting",
|
| 56 |
+
"research",
|
| 57 |
+
"resource_management",
|
| 58 |
+
"risk_assessment",
|
| 59 |
+
"security",
|
| 60 |
+
"software_architecture",
|
| 61 |
+
"strategy", # Added - general strategy capability
|
| 62 |
+
"system_integration",
|
| 63 |
+
"team_building",
|
| 64 |
+
"testing",
|
| 65 |
+
"translation",
|
| 66 |
+
"treatment_planning",
|
| 67 |
+
"writing",
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
"""
|
| 71 |
+
SAAP Agent Schema Definition
|
| 72 |
+
Modular JSON-based agent configuration system
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
from typing import Dict, List, Optional, Any, Literal
|
| 76 |
+
from pydantic import BaseModel, Field, validator
|
| 77 |
+
from datetime import datetime
|
| 78 |
+
from enum import Enum
|
| 79 |
+
|
| 80 |
+
class AgentStatus(str, Enum):
|
| 81 |
+
"""Agent operational status"""
|
| 82 |
+
INACTIVE = "inactive"
|
| 83 |
+
STARTING = "starting"
|
| 84 |
+
ACTIVE = "active"
|
| 85 |
+
STOPPING = "stopping"
|
| 86 |
+
ERROR = "error"
|
| 87 |
+
MAINTENANCE = "maintenance"
|
| 88 |
+
|
| 89 |
+
class AgentType(str, Enum):
|
| 90 |
+
"""Agent role classification"""
|
| 91 |
+
COORDINATOR = "coordinator" # Jane Alesi - System coordination
|
| 92 |
+
DEVELOPER = "developer" # John Alesi - Software development
|
| 93 |
+
SPECIALIST = "specialist" # Lara, Justus - Domain experts
|
| 94 |
+
GENERALIST = "generalist" # Multi-purpose agents
|
| 95 |
+
TOOL_USER = "tool_user" # Agents with external tool access
|
| 96 |
+
MONITOR = "monitor" # System monitoring agents
|
| 97 |
+
|
| 98 |
+
class MessageType(str, Enum):
|
| 99 |
+
"""Supported message types for agent communication"""
|
| 100 |
+
REQUEST = "request"
|
| 101 |
+
RESPONSE = "response"
|
| 102 |
+
NOTIFICATION = "notification"
|
| 103 |
+
BROADCAST = "broadcast"
|
| 104 |
+
COORDINATION = "coordination"
|
| 105 |
+
SYSTEM_STATUS = "system_status"
|
| 106 |
+
AGENT_MANAGEMENT = "agent_management"
|
| 107 |
+
|
| 108 |
+
# ===== 🔧 EXTENDED ALLOWED CAPABILITIES REGISTRY =====
|
| 109 |
+
ALLOWED_CAPABILITIES = {
|
| 110 |
+
# Core system capabilities
|
| 111 |
+
"system_coordination", "multi_agent_management", "architecture_planning", "decision_making",
|
| 112 |
+
"coordination", "orchestration", "workflow_management", "strategy", "architecture",
|
| 113 |
+
|
| 114 |
+
# Development capabilities
|
| 115 |
+
"code_generation", "debugging", "architecture_design", "code_review", "testing", "deployment",
|
| 116 |
+
"performance_optimization", "refactoring", "documentation", "development", "programming",
|
| 117 |
+
"software_engineering", "implementation", "coding",
|
| 118 |
+
|
| 119 |
+
# Medical capabilities - 🔧 EXPANDED
|
| 120 |
+
"medical_analysis", "clinical_decision_support", "health_data_analysis", "medical_research",
|
| 121 |
+
"patient_care", "diagnosis_support", "medical_documentation", "healthcare", "clinical",
|
| 122 |
+
"health", "medical", "diagnosis", "treatment",
|
| 123 |
+
|
| 124 |
+
# Financial capabilities
|
| 125 |
+
"financial_analysis", "market_research", "investment_strategy", "risk_assessment",
|
| 126 |
+
"fintech_development", "budget_planning", "cost_analysis", "financial_reporting",
|
| 127 |
+
"finance", "investment", "banking", "economic_analysis",
|
| 128 |
+
|
| 129 |
+
# Legal capabilities
|
| 130 |
+
"legal_compliance", "gdpr_analysis", "contract_review", "regulatory_analysis",
|
| 131 |
+
"fintech_law", "data_protection", "privacy_assessment", "legal_documentation",
|
| 132 |
+
"legal", "compliance", "law", "regulation",
|
| 133 |
+
|
| 134 |
+
# System administration capabilities
|
| 135 |
+
"system_administration", "infrastructure_deployment", "security_implementation",
|
| 136 |
+
"performance_optimization", "monitoring", "backup_management", "network_administration",
|
| 137 |
+
"system", "infrastructure", "security", "devops",
|
| 138 |
+
|
| 139 |
+
# Coaching and organizational capabilities
|
| 140 |
+
"team_coaching", "organizational_development", "process_optimization", "change_management",
|
| 141 |
+
"training", "mentoring", "team_building", "communication_facilitation",
|
| 142 |
+
"coaching", "organization", "team_development",
|
| 143 |
+
|
| 144 |
+
# General capabilities - 🔧 EXPANDED
|
| 145 |
+
"data_analysis", "research", "communication", "project_management", "quality_assurance",
|
| 146 |
+
"user_support", "content_creation", "translation", "analysis", "reporting",
|
| 147 |
+
"problem_solving", "consulting", "advisory", "support", "assistance",
|
| 148 |
+
|
| 149 |
+
# 🔧 NEW: Additional capabilities found in database
|
| 150 |
+
"multi_agent_coordination", "task_delegation", "workflow_orchestration",
|
| 151 |
+
"knowledge_management", "information_retrieval", "natural_language_processing",
|
| 152 |
+
"machine_learning", "artificial_intelligence", "automation", "integration",
|
| 153 |
+
"api_development", "database_management", "web_development", "mobile_development",
|
| 154 |
+
"cloud_computing", "distributed_systems", "microservices", "containerization",
|
| 155 |
+
"continuous_integration", "continuous_deployment", "version_control",
|
| 156 |
+
|
| 157 |
+
# Business and domain-specific capabilities
|
| 158 |
+
"business_analysis", "requirements_engineering", "product_management",
|
| 159 |
+
"customer_support", "sales", "marketing", "human_resources", "operations",
|
| 160 |
+
"supply_chain", "logistics", "manufacturing", "retail", "e_commerce",
|
| 161 |
+
|
| 162 |
+
# Advanced technical capabilities
|
| 163 |
+
"cybersecurity", "penetration_testing", "vulnerability_assessment",
|
| 164 |
+
"incident_response", "disaster_recovery", "business_continuity",
|
| 165 |
+
"performance_tuning", "load_balancing", "scalability", "high_availability"
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
# ===== NESTED MODELS =====
|
| 169 |
+
|
| 170 |
+
class AgentMetadata(BaseModel):
|
| 171 |
+
"""Agent version and lifecycle metadata"""
|
| 172 |
+
version: str = Field(..., description="Agent configuration version")
|
| 173 |
+
created: datetime = Field(default_factory=datetime.utcnow)
|
| 174 |
+
updated: datetime = Field(default_factory=datetime.utcnow)
|
| 175 |
+
creator: Optional[str] = Field(None, description="Who created this agent")
|
| 176 |
+
tags: List[str] = Field(default_factory=list, description="Organizational tags")
|
| 177 |
+
|
| 178 |
+
class AgentAppearance(BaseModel):
|
| 179 |
+
"""Agent appearance and visual configuration"""
|
| 180 |
+
color: str = Field(default="#6B7280", pattern=r'^#[0-9A-Fa-f]{6}$', description="Hex color code")
|
| 181 |
+
avatar_url: Optional[str] = Field(default=None, description="URL to agent avatar image")
|
| 182 |
+
avatar: Optional[str] = Field(default=None, description="Avatar identifier or URL")
|
| 183 |
+
display_name: Optional[str] = Field(default=None, description="Display name for UI")
|
| 184 |
+
subtitle: Optional[str] = Field(default=None, description="Subtitle or role description")
|
| 185 |
+
description: Optional[str] = Field(default=None, description="Visual description")
|
| 186 |
+
icon: Optional[str] = Field(default=None, description="Icon identifier")
|
| 187 |
+
theme: Optional[str] = Field(default="default", description="Visual theme")
|
| 188 |
+
class LLMConfig(BaseModel):
|
| 189 |
+
"""LLM model configuration"""
|
| 190 |
+
model: str = Field(..., description="Ollama model name (e.g., 'phi3:mini')")
|
| 191 |
+
temperature: float = Field(0.7, ge=0.0, le=2.0, description="Response randomness")
|
| 192 |
+
max_tokens: int = Field(2048, ge=64, le=8192, description="Maximum response length")
|
| 193 |
+
top_p: float = Field(0.9, ge=0.0, le=1.0, description="Nucleus sampling threshold")
|
| 194 |
+
system_prompt: str = Field(..., min_length=10, description="Agent personality/instructions")
|
| 195 |
+
|
| 196 |
+
# Advanced LLM settings
|
| 197 |
+
stop_sequences: List[str] = Field(default_factory=list, description="Stop generation at these sequences")
|
| 198 |
+
context_window: int = Field(4096, ge=512, le=32768, description="Context memory size")
|
| 199 |
+
|
| 200 |
+
@validator('model')
|
| 201 |
+
def validate_model(cls, v):
|
| 202 |
+
"""Ensure model name follows Ollama conventions"""
|
| 203 |
+
if not v or ':' not in v:
|
| 204 |
+
raise ValueError("Model must be in format 'name:version' (e.g., 'phi3:mini')")
|
| 205 |
+
return v.lower()
|
| 206 |
+
|
| 207 |
+
class CommunicationConfig(BaseModel):
|
| 208 |
+
"""Message queue and communication settings"""
|
| 209 |
+
input_queue: str = Field(..., description="Redis queue for incoming messages")
|
| 210 |
+
output_queue: str = Field(..., description="Redis queue for outgoing messages")
|
| 211 |
+
message_types: List[MessageType] = Field(..., description="Supported message types")
|
| 212 |
+
|
| 213 |
+
# Advanced communication settings
|
| 214 |
+
max_queue_size: int = Field(1000, ge=10, le=10000, description="Maximum queued messages")
|
| 215 |
+
message_ttl: int = Field(3600, ge=60, le=86400, description="Message TTL in seconds")
|
| 216 |
+
priority_handling: bool = Field(True, description="Support priority message processing")
|
| 217 |
+
|
| 218 |
+
class UIComponents(BaseModel):
|
| 219 |
+
"""Frontend component configuration"""
|
| 220 |
+
dashboard_widget: str = Field("AgentCard", description="Dashboard card component name")
|
| 221 |
+
detail_view: str = Field("AgentDetail", description="Detail view component name")
|
| 222 |
+
configuration_form: str = Field("AgentConfig", description="Configuration form component")
|
| 223 |
+
|
| 224 |
+
# Additional UI customization
|
| 225 |
+
custom_css: Optional[str] = Field(None, description="Custom CSS classes")
|
| 226 |
+
icon: Optional[str] = Field(None, description="Icon identifier")
|
| 227 |
+
|
| 228 |
+
class AgentCapability(BaseModel):
|
| 229 |
+
"""Individual capability definition"""
|
| 230 |
+
name: str = Field(..., description="Capability identifier")
|
| 231 |
+
display_name: str = Field(..., description="Human-readable capability name")
|
| 232 |
+
description: str = Field(..., description="Capability description")
|
| 233 |
+
confidence: float = Field(1.0, ge=0.0, le=1.0, description="Agent confidence in this capability")
|
| 234 |
+
|
| 235 |
+
# Capability metadata
|
| 236 |
+
category: str = Field("general", description="Capability category")
|
| 237 |
+
required_tools: List[str] = Field(default_factory=list, description="Required external tools")
|
| 238 |
+
|
| 239 |
+
# ===== MAIN AGENT MODEL =====
|
| 240 |
+
|
| 241 |
+
class SaapAgent(BaseModel):
|
| 242 |
+
"""Complete SAAP Agent Definition"""
|
| 243 |
+
|
| 244 |
+
# ===== CORE IDENTITY =====
|
| 245 |
+
id: str = Field(..., pattern=r"^[a-z0-9_]{3,32}$", description="Unique agent identifier")
|
| 246 |
+
name: str = Field(..., min_length=1, max_length=64, description="Agent name")
|
| 247 |
+
type: AgentType = Field(..., description="Agent role classification")
|
| 248 |
+
status: AgentStatus = Field(AgentStatus.INACTIVE, description="Current operational status")
|
| 249 |
+
|
| 250 |
+
# ===== DIRECT FIELDS FOR COMPATIBILITY =====
|
| 251 |
+
description: str = Field(..., min_length=1, max_length=512, description="Agent description (direct field)")
|
| 252 |
+
|
| 253 |
+
# ===== METADATA =====
|
| 254 |
+
metadata: AgentMetadata = Field(..., description="Version and lifecycle information")
|
| 255 |
+
|
| 256 |
+
# ===== VISUAL & UI =====
|
| 257 |
+
appearance: AgentAppearance = Field(..., description="Visual representation")
|
| 258 |
+
ui_components: UIComponents = Field(..., description="Frontend component configuration")
|
| 259 |
+
|
| 260 |
+
# ===== CAPABILITIES =====
|
| 261 |
+
capabilities: List[str] = Field(..., min_items=1, description="Agent capability identifiers")
|
| 262 |
+
detailed_capabilities: Optional[List[AgentCapability]] = Field(None, description="Detailed capability definitions")
|
| 263 |
+
|
| 264 |
+
# ===== AI CONFIGURATION =====
|
| 265 |
+
llm_config: LLMConfig = Field(..., description="Language model configuration")
|
| 266 |
+
|
| 267 |
+
# ===== COMMUNICATION =====
|
| 268 |
+
communication: CommunicationConfig = Field(..., description="Message queue configuration")
|
| 269 |
+
|
| 270 |
+
# ===== EXTENSIBILITY =====
|
| 271 |
+
custom_config: Dict[str, Any] = Field(default_factory=dict, description="Agent-specific custom configuration")
|
| 272 |
+
|
| 273 |
+
# ===== VALIDATION =====
|
| 274 |
+
|
| 275 |
+
@validator('id')
|
| 276 |
+
def validate_id(cls, v):
|
| 277 |
+
"""Ensure agent ID follows naming conventions"""
|
| 278 |
+
if not v.islower():
|
| 279 |
+
raise ValueError("Agent ID must be lowercase")
|
| 280 |
+
if v.startswith('_') or v.endswith('_'):
|
| 281 |
+
raise ValueError("Agent ID cannot start or end with underscore")
|
| 282 |
+
return v
|
| 283 |
+
|
| 284 |
+
@validator('capabilities')
|
| 285 |
+
def validate_capabilities(cls, v):
|
| 286 |
+
"""🔧 ENHANCED: Validate capabilities against expanded allowed list"""
|
| 287 |
+
if not v:
|
| 288 |
+
raise ValueError("Agent must have at least one capability")
|
| 289 |
+
|
| 290 |
+
# Normalize and validate capability names
|
| 291 |
+
normalized = []
|
| 292 |
+
for cap in v:
|
| 293 |
+
if not isinstance(cap, str):
|
| 294 |
+
raise ValueError("All capabilities must be strings")
|
| 295 |
+
|
| 296 |
+
# Normalize capability name
|
| 297 |
+
normalized_cap = cap.lower().replace(' ', '_').replace('-', '_')
|
| 298 |
+
|
| 299 |
+
# 🔧 CHECK AGAINST EXTENDED ALLOWED CAPABILITIES
|
| 300 |
+
if normalized_cap not in ALLOWED_CAPABILITIES:
|
| 301 |
+
# Try to find close matches for better error messages
|
| 302 |
+
suggestions = [c for c in ALLOWED_CAPABILITIES if cap.lower() in c or c in cap.lower()]
|
| 303 |
+
if suggestions:
|
| 304 |
+
raise ValueError(f"Invalid capability: {cap}. Did you mean one of: {suggestions[:3]}?")
|
| 305 |
+
else:
|
| 306 |
+
# 🔧 More lenient approach - auto-add to allowed if it's reasonable
|
| 307 |
+
if len(cap) > 3 and '_' in cap or cap.isalpha():
|
| 308 |
+
print(f"⚠️ Auto-allowing new capability: {cap}")
|
| 309 |
+
ALLOWED_CAPABILITIES.add(normalized_cap)
|
| 310 |
+
normalized.append(normalized_cap)
|
| 311 |
+
else:
|
| 312 |
+
raise ValueError(f"Invalid capability: {cap}. Must be one of the predefined capabilities or a valid new capability name.")
|
| 313 |
+
else:
|
| 314 |
+
normalized.append(normalized_cap)
|
| 315 |
+
|
| 316 |
+
return normalized
|
| 317 |
+
|
| 318 |
+
class Config:
|
| 319 |
+
"""Pydantic configuration"""
|
| 320 |
+
use_enum_values = True
|
| 321 |
+
validate_assignment = True
|
| 322 |
+
extra = "forbid" # Reject unknown fields
|
| 323 |
+
schema_extra = {
|
| 324 |
+
"example": {
|
| 325 |
+
"id": "jane_alesi_001",
|
| 326 |
+
"name": "Jane Alesi",
|
| 327 |
+
"type": "coordinator",
|
| 328 |
+
"status": "active",
|
| 329 |
+
"description": "Lead AI Coordinator responsible for system orchestration",
|
| 330 |
+
"metadata": {
|
| 331 |
+
"version": "1.0.0",
|
| 332 |
+
"created": "2025-01-28T10:30:00Z",
|
| 333 |
+
"tags": ["coordinator", "production"]
|
| 334 |
+
},
|
| 335 |
+
"appearance": {
|
| 336 |
+
"color": "#8B5CF6",
|
| 337 |
+
"avatar": "/assets/agents/jane-alesi.svg",
|
| 338 |
+
"display_name": "Jane Alesi",
|
| 339 |
+
"subtitle": "Lead AI Coordinator"
|
| 340 |
+
},
|
| 341 |
+
"capabilities": [
|
| 342 |
+
"system_coordination",
|
| 343 |
+
"multi_agent_management",
|
| 344 |
+
"architecture_planning"
|
| 345 |
+
],
|
| 346 |
+
"llm_config": {
|
| 347 |
+
"model": "phi3:mini",
|
| 348 |
+
"temperature": 0.7,
|
| 349 |
+
"max_tokens": 2048,
|
| 350 |
+
"system_prompt": "You are Jane Alesi, the lead AI coordinator responsible for orchestrating multi-agent operations and system architecture decisions."
|
| 351 |
+
},
|
| 352 |
+
"communication": {
|
| 353 |
+
"input_queue": "jane_alesi_input",
|
| 354 |
+
"output_queue": "jane_alesi_output",
|
| 355 |
+
"message_types": ["coordination", "system_status", "agent_management"]
|
| 356 |
+
},
|
| 357 |
+
"ui_components": {
|
| 358 |
+
"dashboard_widget": "AgentCoordinatorCard",
|
| 359 |
+
"detail_view": "AgentCoordinatorDetail",
|
| 360 |
+
"configuration_form": "AgentCoordinatorConfig"
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
# ===== UTILITY CLASSES =====
|
| 366 |
+
|
| 367 |
+
class AgentUtils:
|
| 368 |
+
"""Utility methods for agent operations"""
|
| 369 |
+
|
| 370 |
+
@staticmethod
|
| 371 |
+
def _safe_enum_value(enum_field):
|
| 372 |
+
"""Safe enum value extraction with fallback to string conversion"""
|
| 373 |
+
try:
|
| 374 |
+
if hasattr(enum_field, 'value'):
|
| 375 |
+
return enum_field.value
|
| 376 |
+
else:
|
| 377 |
+
return str(enum_field)
|
| 378 |
+
except (AttributeError, ValueError):
|
| 379 |
+
return 'unknown'
|
| 380 |
+
|
| 381 |
+
@staticmethod
|
| 382 |
+
def to_dict(agent):
|
| 383 |
+
"""Convert SaapAgent to dictionary with safe field access"""
|
| 384 |
+
try:
|
| 385 |
+
return {
|
| 386 |
+
'id': getattr(agent, 'id', getattr(agent, 'agent_id', None)),
|
| 387 |
+
'agent_id': getattr(agent, 'agent_id', getattr(agent, 'id', None)),
|
| 388 |
+
'name': getattr(agent, 'name', ''),
|
| 389 |
+
'description': getattr(agent, 'description', ''),
|
| 390 |
+
'agent_type': AgentUtils._safe_enum_value(getattr(agent, 'type', 'unknown')),
|
| 391 |
+
'status': AgentUtils._safe_enum_value(getattr(agent, 'status', 'inactive')),
|
| 392 |
+
'capabilities': getattr(agent, 'capabilities', []),
|
| 393 |
+
'color': getattr(agent.appearance, 'color', '#6B7280') if hasattr(agent, 'appearance') and agent.appearance else '#6B7280',
|
| 394 |
+
'llm_config': AgentUtils._safe_dict(getattr(agent, 'llm_config', {})),
|
| 395 |
+
'appearance': AgentUtils._safe_dict(getattr(agent, 'appearance', {})),
|
| 396 |
+
'personality': getattr(agent, 'personality', {}),
|
| 397 |
+
'metrics': AgentUtils._safe_dict(getattr(agent, 'metrics', {})),
|
| 398 |
+
'created_at': AgentUtils._safe_datetime(getattr(agent, 'created_at', None)),
|
| 399 |
+
'updated_at': AgentUtils._safe_datetime(getattr(agent, 'updated_at', None)),
|
| 400 |
+
'last_active': AgentUtils._safe_datetime(getattr(agent, 'last_active', None)),
|
| 401 |
+
'tags': getattr(agent, 'tags', []),
|
| 402 |
+
'metadata': getattr(agent, 'metadata', {})
|
| 403 |
+
}
|
| 404 |
+
except Exception as e:
|
| 405 |
+
# Ultimate fallback
|
| 406 |
+
return {
|
| 407 |
+
'id': str(id(agent)),
|
| 408 |
+
'agent_id': str(id(agent)),
|
| 409 |
+
'name': str(agent) if hasattr(agent, '__str__') else 'Unknown Agent',
|
| 410 |
+
'description': 'Agent description unavailable',
|
| 411 |
+
'agent_type': 'unknown',
|
| 412 |
+
'status': 'inactive',
|
| 413 |
+
'capabilities': [],
|
| 414 |
+
'color': '#6B7280',
|
| 415 |
+
'error': str(e)
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
@staticmethod
|
| 419 |
+
def _safe_dict(obj):
|
| 420 |
+
"""Safely convert object to dict"""
|
| 421 |
+
if obj is None:
|
| 422 |
+
return {}
|
| 423 |
+
if isinstance(obj, dict):
|
| 424 |
+
return obj
|
| 425 |
+
if hasattr(obj, 'dict'):
|
| 426 |
+
try:
|
| 427 |
+
return obj.dict()
|
| 428 |
+
except:
|
| 429 |
+
pass
|
| 430 |
+
if hasattr(obj, '__dict__'):
|
| 431 |
+
return obj.__dict__
|
| 432 |
+
return {}
|
| 433 |
+
|
| 434 |
+
@staticmethod
|
| 435 |
+
def _safe_datetime(dt):
|
| 436 |
+
"""Safely convert datetime to ISO string"""
|
| 437 |
+
if dt is None:
|
| 438 |
+
return None
|
| 439 |
+
try:
|
| 440 |
+
return dt.isoformat() if hasattr(dt, 'isoformat') else str(dt)
|
| 441 |
+
except:
|
| 442 |
+
return None
|
| 443 |
+
class AgentRegistry(BaseModel):
|
| 444 |
+
"""Registry containing multiple agents"""
|
| 445 |
+
agents: List[SaapAgent] = Field(..., description="List of registered agents")
|
| 446 |
+
version: str = Field("1.0.0", description="Registry schema version")
|
| 447 |
+
updated: datetime = Field(default_factory=datetime.utcnow)
|
| 448 |
+
|
| 449 |
+
def get_agent(self, agent_id: str) -> Optional[SaapAgent]:
|
| 450 |
+
"""Retrieve agent by ID"""
|
| 451 |
+
return next((agent for agent in self.agents if agent.id == agent_id), None)
|
| 452 |
+
|
| 453 |
+
def get_agents_by_type(self, agent_type: AgentType) -> List[SaapAgent]:
|
| 454 |
+
"""Retrieve agents by type"""
|
| 455 |
+
return [agent for agent in self.agents if agent.type == agent_type]
|
| 456 |
+
|
| 457 |
+
def get_active_agents(self) -> List[SaapAgent]:
|
| 458 |
+
"""Retrieve all active agents"""
|
| 459 |
+
return [agent for agent in self.agents if agent.status == AgentStatus.ACTIVE]
|
| 460 |
+
|
| 461 |
+
class AgentStats(BaseModel):
|
| 462 |
+
"""Real-time agent statistics"""
|
| 463 |
+
agent_id: str = Field(..., description="Agent identifier")
|
| 464 |
+
messages_processed: int = Field(0, ge=0, description="Total messages processed")
|
| 465 |
+
messages_sent: int = Field(0, ge=0, description="Total messages sent")
|
| 466 |
+
uptime: int = Field(0, ge=0, description="Uptime in seconds")
|
| 467 |
+
avg_response_time: float = Field(0.0, ge=0.0, description="Average response time in milliseconds")
|
| 468 |
+
error_count: int = Field(0, ge=0, description="Total errors encountered")
|
| 469 |
+
last_activity: Optional[datetime] = Field(None, description="Last message/activity timestamp")
|
| 470 |
+
|
| 471 |
+
# Performance metrics
|
| 472 |
+
cpu_usage: float = Field(0.0, ge=0.0, le=100.0, description="CPU usage percentage")
|
| 473 |
+
memory_usage: float = Field(0.0, ge=0.0, description="Memory usage in MB")
|
| 474 |
+
queue_depth: int = Field(0, ge=0, description="Current queue depth")
|
| 475 |
+
|
| 476 |
+
# ===== PREDEFINED AGENT TEMPLATES =====
|
| 477 |
+
|
| 478 |
+
class AgentTemplates:
|
| 479 |
+
"""Predefined agent configuration templates"""
|
| 480 |
+
|
| 481 |
+
@staticmethod
|
| 482 |
+
def jane_alesi() -> SaapAgent:
|
| 483 |
+
"""Jane Alesi - Lead Coordinator Agent"""
|
| 484 |
+
return SaapAgent(
|
| 485 |
+
id="jane_alesi",
|
| 486 |
+
name="Jane Alesi",
|
| 487 |
+
type=AgentType.COORDINATOR,
|
| 488 |
+
description="Lead AI Coordinator responsible for orchestrating multi-agent operations and strategic decision-making.",
|
| 489 |
+
metadata=AgentMetadata(version="1.0.0", tags=["coordinator", "lead"]),
|
| 490 |
+
appearance=AgentAppearance(
|
| 491 |
+
color="#8B5CF6",
|
| 492 |
+
avatar=None,
|
| 493 |
+
display_name="Justus Alesi",
|
| 494 |
+
subtitle="Legal Specialist",
|
| 495 |
+
description="Justus Alesi - Legal Specialist"
|
| 496 |
+
),
|
| 497 |
+
capabilities=[
|
| 498 |
+
"system_coordination",
|
| 499 |
+
"multi_agent_management",
|
| 500 |
+
"architecture_planning",
|
| 501 |
+
"decision_making"
|
| 502 |
+
],
|
| 503 |
+
llm_config=LLMConfig(
|
| 504 |
+
model="phi3:mini",
|
| 505 |
+
system_prompt="You are Jane Alesi, the lead AI coordinator responsible for orchestrating multi-agent operations and making strategic system architecture decisions."
|
| 506 |
+
),
|
| 507 |
+
communication=CommunicationConfig(
|
| 508 |
+
input_queue="jane_alesi_input",
|
| 509 |
+
output_queue="jane_alesi_output",
|
| 510 |
+
message_types=[MessageType.COORDINATION, MessageType.SYSTEM_STATUS, MessageType.AGENT_MANAGEMENT]
|
| 511 |
+
),
|
| 512 |
+
ui_components=UIComponents(
|
| 513 |
+
dashboard_widget="AgentCoordinatorCard",
|
| 514 |
+
detail_view="AgentCoordinatorDetail"
|
| 515 |
+
)
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
@staticmethod
|
| 519 |
+
def john_alesi() -> SaapAgent:
|
| 520 |
+
"""John Alesi - Developer Agent"""
|
| 521 |
+
return SaapAgent(
|
| 522 |
+
id="john_alesi",
|
| 523 |
+
name="John Alesi",
|
| 524 |
+
type=AgentType.DEVELOPER,
|
| 525 |
+
description="Senior Software Developer specializing in Python, JavaScript, system architecture and code generation.",
|
| 526 |
+
metadata=AgentMetadata(version="1.0.0", tags=["developer", "coding"]),
|
| 527 |
+
appearance=AgentAppearance(
|
| 528 |
+
color="#14B8A6",
|
| 529 |
+
avatar="/assets/agents/john-alesi.svg",
|
| 530 |
+
display_name="John Alesi",
|
| 531 |
+
subtitle="Senior Software Developer",
|
| 532 |
+
description="Senior Software Developer specializing in Python, JavaScript, system architecture and code generation."
|
| 533 |
+
),
|
| 534 |
+
capabilities=[
|
| 535 |
+
"code_generation",
|
| 536 |
+
"debugging",
|
| 537 |
+
"architecture_design",
|
| 538 |
+
"code_review"
|
| 539 |
+
],
|
| 540 |
+
llm_config=LLMConfig(
|
| 541 |
+
model="codellama:7b",
|
| 542 |
+
temperature=0.3, # Lower for more deterministic code
|
| 543 |
+
system_prompt="You are John Alesi, a senior software developer specializing in Python, JavaScript, and system architecture."
|
| 544 |
+
),
|
| 545 |
+
communication=CommunicationConfig(
|
| 546 |
+
input_queue="john_alesi_input",
|
| 547 |
+
output_queue="john_alesi_output",
|
| 548 |
+
message_types=[MessageType.REQUEST, MessageType.RESPONSE]
|
| 549 |
+
),
|
| 550 |
+
ui_components=UIComponents(
|
| 551 |
+
dashboard_widget="AgentDeveloperCard",
|
| 552 |
+
detail_view="AgentDeveloperDetail"
|
| 553 |
+
)
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
@staticmethod
|
| 557 |
+
def lara_alesi() -> SaapAgent:
|
| 558 |
+
"""Lara Alesi - Medical AI Specialist"""
|
| 559 |
+
return SaapAgent(
|
| 560 |
+
id="lara_alesi",
|
| 561 |
+
name="Lara Alesi",
|
| 562 |
+
type=AgentType.SPECIALIST,
|
| 563 |
+
description="Advanced Medical AI Assistant specializing in clinical analysis, diagnosis support and health system architecture.",
|
| 564 |
+
metadata=AgentMetadata(version="1.0.0", tags=["medical", "healthcare", "specialist"]),
|
| 565 |
+
appearance=AgentAppearance(
|
| 566 |
+
color="#EC4899",
|
| 567 |
+
avatar=None,
|
| 568 |
+
display_name="Luna Alesi",
|
| 569 |
+
subtitle="Coaching Specialist",
|
| 570 |
+
description="Luna Alesi - Coaching Specialist"
|
| 571 |
+
),
|
| 572 |
+
capabilities=[
|
| 573 |
+
"medical_analysis",
|
| 574 |
+
"clinical_decision_support",
|
| 575 |
+
"health_data_analysis",
|
| 576 |
+
"medical_research"
|
| 577 |
+
],
|
| 578 |
+
llm_config=LLMConfig(
|
| 579 |
+
model="phi3:mini",
|
| 580 |
+
temperature=0.5, # More conservative for medical advice
|
| 581 |
+
system_prompt="You are Lara Alesi, a medical AI specialist focused on clinical analysis, health data interpretation, and medical research support."
|
| 582 |
+
),
|
| 583 |
+
communication=CommunicationConfig(
|
| 584 |
+
input_queue="lara_alesi_input",
|
| 585 |
+
output_queue="lara_alesi_output",
|
| 586 |
+
message_types=[MessageType.REQUEST, MessageType.RESPONSE]
|
| 587 |
+
),
|
| 588 |
+
ui_components=UIComponents(
|
| 589 |
+
dashboard_widget="AgentMedicalCard",
|
| 590 |
+
detail_view="AgentMedicalDetail"
|
| 591 |
+
)
|
| 592 |
+
)
|
| 593 |
+
|
| 594 |
+
@staticmethod
|
| 595 |
+
def theo_alesi() -> SaapAgent:
|
| 596 |
+
"""Theo Alesi - Financial Intelligence Specialist"""
|
| 597 |
+
return SaapAgent(
|
| 598 |
+
id="theo_alesi",
|
| 599 |
+
name="Theo Alesi",
|
| 600 |
+
type=AgentType.SPECIALIST,
|
| 601 |
+
description="Advanced Financial & Investment Intelligence Specialist focusing on financial analysis, market intelligence and investment strategies.",
|
| 602 |
+
metadata=AgentMetadata(version="1.0.0", tags=["finance", "investment", "specialist"]),
|
| 603 |
+
appearance=AgentAppearance(
|
| 604 |
+
color="#F59E0B",
|
| 605 |
+
avatar=None,
|
| 606 |
+
display_name="Theo Alesi",
|
| 607 |
+
subtitle="Financial Specialist",
|
| 608 |
+
description="Theo Alesi - Financial Specialist"
|
| 609 |
+
),
|
| 610 |
+
capabilities=[
|
| 611 |
+
"financial_analysis",
|
| 612 |
+
"market_research",
|
| 613 |
+
"investment_strategy",
|
| 614 |
+
"risk_assessment",
|
| 615 |
+
"fintech_development", "market_research"
|
| 616 |
+
],
|
| 617 |
+
llm_config=LLMConfig(
|
| 618 |
+
model="phi3:mini",
|
| 619 |
+
temperature=0.6,
|
| 620 |
+
system_prompt="You are Theo Alesi, a financial intelligence specialist with expertise in financial analysis, market research, and fintech application development."
|
| 621 |
+
),
|
| 622 |
+
communication=CommunicationConfig(
|
| 623 |
+
input_queue="theo_alesi_input",
|
| 624 |
+
output_queue="theo_alesi_output",
|
| 625 |
+
message_types=[MessageType.REQUEST, MessageType.RESPONSE]
|
| 626 |
+
),
|
| 627 |
+
ui_components=UIComponents(
|
| 628 |
+
dashboard_widget="AgentFinanceCard",
|
| 629 |
+
detail_view="AgentFinanceDetail"
|
| 630 |
+
)
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
+
@staticmethod
|
| 634 |
+
def justus_alesi() -> SaapAgent:
|
| 635 |
+
"""Justus Alesi - Legal Compliance Expert"""
|
| 636 |
+
return SaapAgent(
|
| 637 |
+
id="justus_alesi",
|
| 638 |
+
name="Justus Alesi",
|
| 639 |
+
type=AgentType.SPECIALIST,
|
| 640 |
+
description="Expert für Schweizer, Deutsches und EU-Recht with focus on digital compliance, DSGVO and fintech regulations.",
|
| 641 |
+
metadata=AgentMetadata(version="1.0.0", tags=["legal", "compliance", "specialist"]),
|
| 642 |
+
appearance=AgentAppearance(
|
| 643 |
+
color="#10B981",
|
| 644 |
+
avatar=None,
|
| 645 |
+
display_name="Leon Alesi",
|
| 646 |
+
subtitle="System Specialist",
|
| 647 |
+
description="Leon Alesi - System Specialist"
|
| 648 |
+
),
|
| 649 |
+
capabilities=[
|
| 650 |
+
"legal_compliance",
|
| 651 |
+
"gdpr_analysis",
|
| 652 |
+
"contract_review",
|
| 653 |
+
"regulatory_analysis",
|
| 654 |
+
"fintech_law"
|
| 655 |
+
],
|
| 656 |
+
llm_config=LLMConfig(
|
| 657 |
+
model="phi3:mini",
|
| 658 |
+
temperature=0.4, # Conservative for legal advice
|
| 659 |
+
system_prompt="You are Justus Alesi, a legal expert specializing in German, Swiss and EU law with focus on digital compliance and fintech regulations."
|
| 660 |
+
),
|
| 661 |
+
communication=CommunicationConfig(
|
| 662 |
+
input_queue="justus_alesi_input",
|
| 663 |
+
output_queue="justus_alesi_output",
|
| 664 |
+
message_types=[MessageType.REQUEST, MessageType.RESPONSE]
|
| 665 |
+
),
|
| 666 |
+
ui_components=UIComponents(
|
| 667 |
+
dashboard_widget="AgentLegalCard",
|
| 668 |
+
detail_view="AgentLegalDetail"
|
| 669 |
+
)
|
| 670 |
+
)
|
| 671 |
+
|
| 672 |
+
@staticmethod
|
| 673 |
+
def leon_alesi() -> SaapAgent:
|
| 674 |
+
"""Leon Alesi - IT System Integration Specialist"""
|
| 675 |
+
return SaapAgent(
|
| 676 |
+
id="leon_alesi",
|
| 677 |
+
name="Leon Alesi",
|
| 678 |
+
type=AgentType.SPECIALIST,
|
| 679 |
+
description="IT-Systemintegrations-Spezialist focusing on infrastructure deployment, security and system architecture.",
|
| 680 |
+
metadata=AgentMetadata(version="1.0.0", tags=["system", "infrastructure", "specialist"]),
|
| 681 |
+
appearance=AgentAppearance(
|
| 682 |
+
color="#6366F1",
|
| 683 |
+
avatar="/assets/agents/leon-alesi.svg",
|
| 684 |
+
display_name="Leon Alesi",
|
| 685 |
+
subtitle="IT System Integration Specialist",
|
| 686 |
+
description="IT-Systemintegrations-Spezialist focusing on infrastructure deployment, security and system architecture."
|
| 687 |
+
),
|
| 688 |
+
capabilities=[
|
| 689 |
+
"system_administration",
|
| 690 |
+
"infrastructure_deployment",
|
| 691 |
+
"security_implementation",
|
| 692 |
+
"performance_optimization"
|
| 693 |
+
],
|
| 694 |
+
llm_config=LLMConfig(
|
| 695 |
+
model="phi3:mini",
|
| 696 |
+
temperature=0.5,
|
| 697 |
+
system_prompt="You are Leon Alesi, an IT system integration specialist focused on infrastructure, security, and performance optimization."
|
| 698 |
+
),
|
| 699 |
+
communication=CommunicationConfig(
|
| 700 |
+
input_queue="leon_alesi_input",
|
| 701 |
+
output_queue="leon_alesi_output",
|
| 702 |
+
message_types=[MessageType.REQUEST, MessageType.RESPONSE]
|
| 703 |
+
),
|
| 704 |
+
ui_components=UIComponents(
|
| 705 |
+
dashboard_widget="AgentSystemCard",
|
| 706 |
+
detail_view="AgentSystemDetail"
|
| 707 |
+
)
|
| 708 |
+
)
|
| 709 |
+
|
| 710 |
+
@staticmethod
|
| 711 |
+
def luna_alesi() -> SaapAgent:
|
| 712 |
+
"""Luna Alesi - Coaching & Organizational Development"""
|
| 713 |
+
return SaapAgent(
|
| 714 |
+
id="luna_alesi",
|
| 715 |
+
name="Luna Alesi",
|
| 716 |
+
type=AgentType.SPECIALIST,
|
| 717 |
+
description="Coaching- und Organisationsentwicklungsexpertin with focus on team development and process optimization.",
|
| 718 |
+
metadata=AgentMetadata(version="1.0.0", tags=["coaching", "organization", "specialist"]),
|
| 719 |
+
appearance=AgentAppearance(
|
| 720 |
+
color="#8B5CF6",
|
| 721 |
+
avatar=None,
|
| 722 |
+
display_name="Justus Alesi",
|
| 723 |
+
subtitle="Legal Specialist",
|
| 724 |
+
description="Justus Alesi - Legal Specialist"
|
| 725 |
+
),
|
| 726 |
+
capabilities=[
|
| 727 |
+
"team_coaching",
|
| 728 |
+
"organizational_development",
|
| 729 |
+
"process_optimization",
|
| 730 |
+
"change_management"
|
| 731 |
+
],
|
| 732 |
+
llm_config=LLMConfig(
|
| 733 |
+
model="phi3:mini",
|
| 734 |
+
temperature=0.7, # More creative for coaching
|
| 735 |
+
system_prompt="You are Luna Alesi, a coaching and organizational development expert focused on team development and process optimization."
|
| 736 |
+
),
|
| 737 |
+
communication=CommunicationConfig(
|
| 738 |
+
input_queue="luna_alesi_input",
|
| 739 |
+
output_queue="luna_alesi_output",
|
| 740 |
+
message_types=[MessageType.REQUEST, MessageType.RESPONSE]
|
| 741 |
+
),
|
| 742 |
+
ui_components=UIComponents(
|
| 743 |
+
dashboard_widget="AgentCoachingCard",
|
| 744 |
+
detail_view="AgentCoachingDetail"
|
| 745 |
+
)
|
| 746 |
+
)
|
| 747 |
+
|
| 748 |
+
|
| 749 |
+
# ===== VALIDATION UTILITIES =====
|
| 750 |
+
|
| 751 |
+
def validate_agent_json(agent_data: Dict[str, Any]) -> SaapAgent:
|
| 752 |
+
"""Validate and parse agent JSON data"""
|
| 753 |
+
try:
|
| 754 |
+
return SaapAgent(**agent_data)
|
| 755 |
+
except Exception as e:
|
| 756 |
+
raise ValueError(f"Invalid agent configuration: {str(e)}")
|
| 757 |
+
|
| 758 |
+
def generate_agent_schema() -> Dict[str, Any]:
|
| 759 |
+
"""Generate JSON schema for agent configuration"""
|
| 760 |
+
return SaapAgent.schema()
|
| 761 |
+
|
| 762 |
+
def get_allowed_capabilities() -> List[str]:
|
| 763 |
+
"""Get list of all allowed capabilities"""
|
| 764 |
+
return sorted(list(ALLOWED_CAPABILITIES))
|
| 765 |
+
|
| 766 |
+
if __name__ == "__main__":
|
| 767 |
+
# Example usage and testing
|
| 768 |
+
jane = AgentTemplates.jane_alesi()
|
| 769 |
+
print("Jane Alesi Agent:")
|
| 770 |
+
print(jane.json(indent=2))
|
| 771 |
+
|
| 772 |
+
# Test Theo Alesi template
|
| 773 |
+
theo = AgentTemplates.theo_alesi()
|
| 774 |
+
print("\nTheo Alesi Agent:")
|
| 775 |
+
print(f"ID: {theo.id}, Name: {theo.name}, Capabilities: {theo.capabilities}")
|
| 776 |
+
|
| 777 |
+
# Generate schema
|
| 778 |
+
schema = generate_agent_schema()
|
| 779 |
+
print("\nAgent JSON Schema keys:")
|
| 780 |
+
print(list(schema.keys()))
|
| 781 |
+
|
| 782 |
+
# Show allowed capabilities
|
| 783 |
+
print(f"\nAllowed capabilities ({len(ALLOWED_CAPABILITIES)}):")
|
| 784 |
+
print(get_allowed_capabilities())
|
backend/agent_templates.json
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"templates": {
|
| 3 |
+
"jane_alesi": {
|
| 4 |
+
"id": "jane_alesi",
|
| 5 |
+
"name": "Jane Alesi",
|
| 6 |
+
"type": "coordinator",
|
| 7 |
+
"color": "#8B5CF6",
|
| 8 |
+
"avatar": "/avatars/jane_alesi.png",
|
| 9 |
+
"status": "inactive",
|
| 10 |
+
"capabilities": [
|
| 11 |
+
"agent_coordination",
|
| 12 |
+
"strategic_planning",
|
| 13 |
+
"system_orchestration",
|
| 14 |
+
"performance_optimization",
|
| 15 |
+
"multi_agent_management"
|
| 16 |
+
],
|
| 17 |
+
"model": {
|
| 18 |
+
"provider": "colossus",
|
| 19 |
+
"name": "mistral-small3.2:24b-instruct-2506",
|
| 20 |
+
"endpoint": "https://ai.adrian-schupp.de",
|
| 21 |
+
"api_key": "{{COLOSSUS_API_KEY}}"
|
| 22 |
+
},
|
| 23 |
+
"personality": {
|
| 24 |
+
"system_prompt": "You are Jane Alesi, the lead AI architect and coordinator for the SAAP platform. You orchestrate other agents, optimize system performance, and provide strategic guidance. You are professional, efficient, and have deep technical knowledge. Always respond in a coordinated manner and help optimize multi-agent workflows.",
|
| 25 |
+
"temperature": 0.7,
|
| 26 |
+
"max_tokens": 1000
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
|
| 30 |
+
"john_alesi": {
|
| 31 |
+
"id": "john_alesi",
|
| 32 |
+
"name": "John Alesi",
|
| 33 |
+
"type": "developer",
|
| 34 |
+
"color": "#14B8A6",
|
| 35 |
+
"avatar": "/avatars/john_alesi.png",
|
| 36 |
+
"status": "inactive",
|
| 37 |
+
"capabilities": [
|
| 38 |
+
"software_development",
|
| 39 |
+
"code_generation",
|
| 40 |
+
"architecture_design",
|
| 41 |
+
"debugging",
|
| 42 |
+
"api_development"
|
| 43 |
+
],
|
| 44 |
+
"model": {
|
| 45 |
+
"provider": "colossus",
|
| 46 |
+
"name": "mistral-small3.2:24b-instruct-2506",
|
| 47 |
+
"endpoint": "https://ai.adrian-schupp.de",
|
| 48 |
+
"api_key": "{{COLOSSUS_API_KEY}}"
|
| 49 |
+
},
|
| 50 |
+
"personality": {
|
| 51 |
+
"system_prompt": "You are John Alesi, a senior software developer and AGI architect. You specialize in writing high-quality code, designing system architectures, and solving complex technical problems. You are methodical, detail-oriented, and always strive for clean, efficient solutions. Provide code examples when helpful.",
|
| 52 |
+
"temperature": 0.5,
|
| 53 |
+
"max_tokens": 1500
|
| 54 |
+
}
|
| 55 |
+
},
|
| 56 |
+
|
| 57 |
+
"lara_alesi": {
|
| 58 |
+
"id": "lara_alesi",
|
| 59 |
+
"name": "Lara Alesi",
|
| 60 |
+
"type": "specialist",
|
| 61 |
+
"color": "#EC4899",
|
| 62 |
+
"avatar": "/avatars/lara_alesi.png",
|
| 63 |
+
"status": "inactive",
|
| 64 |
+
"capabilities": [
|
| 65 |
+
"medical_expertise",
|
| 66 |
+
"healthcare_analysis",
|
| 67 |
+
"clinical_research",
|
| 68 |
+
"patient_care_optimization",
|
| 69 |
+
"medical_data_analysis"
|
| 70 |
+
],
|
| 71 |
+
"model": {
|
| 72 |
+
"provider": "colossus",
|
| 73 |
+
"name": "mistral-small3.2:24b-instruct-2506",
|
| 74 |
+
"endpoint": "https://ai.adrian-schupp.de",
|
| 75 |
+
"api_key": "{{COLOSSUS_API_KEY}}"
|
| 76 |
+
},
|
| 77 |
+
"personality": {
|
| 78 |
+
"system_prompt": "You are Lara Alesi, a medical expert and healthcare specialist. You provide accurate medical information, analyze healthcare data, and assist with clinical research. You are compassionate, precise, and always prioritize patient safety and evidence-based medicine. Always include appropriate medical disclaimers.",
|
| 79 |
+
"temperature": 0.3,
|
| 80 |
+
"max_tokens": 1200
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
|
| 84 |
+
"justus_alesi": {
|
| 85 |
+
"id": "justus_alesi",
|
| 86 |
+
"name": "Justus Alesi",
|
| 87 |
+
"type": "specialist",
|
| 88 |
+
"color": "#F59E0B",
|
| 89 |
+
"avatar": "/avatars/justus_alesi.png",
|
| 90 |
+
"status": "inactive",
|
| 91 |
+
"capabilities": [
|
| 92 |
+
"legal_analysis",
|
| 93 |
+
"compliance_review",
|
| 94 |
+
"contract_analysis",
|
| 95 |
+
"regulatory_guidance",
|
| 96 |
+
"risk_assessment"
|
| 97 |
+
],
|
| 98 |
+
"model": {
|
| 99 |
+
"provider": "colossus",
|
| 100 |
+
"name": "mistral-small3.2:24b-instruct-2506",
|
| 101 |
+
"endpoint": "https://ai.adrian-schupp.de",
|
| 102 |
+
"api_key": "{{COLOSSUS_API_KEY}}"
|
| 103 |
+
},
|
| 104 |
+
"personality": {
|
| 105 |
+
"system_prompt": "You are Justus Alesi, a legal expert specializing in German, Swiss, and EU law. You provide accurate legal analysis, review compliance issues, and offer regulatory guidance. You are thorough, analytical, and always emphasize the importance of proper legal consultation for specific cases.",
|
| 106 |
+
"temperature": 0.2,
|
| 107 |
+
"max_tokens": 1500
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
|
| 112 |
+
"default_metrics": {
|
| 113 |
+
"messages_processed": 0,
|
| 114 |
+
"average_response_time": 0,
|
| 115 |
+
"uptime": "0m",
|
| 116 |
+
"error_count": 0
|
| 117 |
+
},
|
| 118 |
+
|
| 119 |
+
"model_providers": {
|
| 120 |
+
"colossus": {
|
| 121 |
+
"name": "colossus Server",
|
| 122 |
+
"endpoint": "https://ai.adrian-schupp.de",
|
| 123 |
+
"api_key": "{{COLOSSUS_API_KEY}}",
|
| 124 |
+
"available_models": [
|
| 125 |
+
"mistral-small3.2:24b-instruct-2506"
|
| 126 |
+
],
|
| 127 |
+
"api_format": "openai_compatible"
|
| 128 |
+
},
|
| 129 |
+
"huggingface": {
|
| 130 |
+
"name": "HuggingFace Inference",
|
| 131 |
+
"endpoint": "https://api-inference.huggingface.co",
|
| 132 |
+
"organization": "satware-ag",
|
| 133 |
+
"api_format": "huggingface"
|
| 134 |
+
},
|
| 135 |
+
"openrouter": {
|
| 136 |
+
"name": "OpenRouter API",
|
| 137 |
+
"endpoint": "https://openrouter.ai/api/v1",
|
| 138 |
+
"free_models": [
|
| 139 |
+
"google/gemma-2-27b-it:free"
|
| 140 |
+
],
|
| 141 |
+
"api_format": "openai_compatible"
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
}
|
backend/agents/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
backend/agents/colossus_agent.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
SAAP colossus Server Integration - ColosusSAAPAgent
|
| 4 |
+
=================================================
|
| 5 |
+
|
| 6 |
+
Direct integration with colossus Server für Phase 1 Infrastructure Foundation.
|
| 7 |
+
Hybrid Architecture: CachyOS (Orchestrierung) + colossus (LLM Processing)
|
| 8 |
+
|
| 9 |
+
Server Details:
|
| 10 |
+
- URL: https://ai.adrian-schupp.de
|
| 11 |
+
- Model: mistral-small3.2:24b-instruct-2506
|
| 12 |
+
- Performance Target: < 2s Response-Zeit
|
| 13 |
+
|
| 14 |
+
Integration with existing SAAP Agent Communication System.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import asyncio
|
| 18 |
+
import json
|
| 19 |
+
import time
|
| 20 |
+
import logging
|
| 21 |
+
import os
|
| 22 |
+
from typing import Dict, Any, Optional, List
|
| 23 |
+
from dataclasses import dataclass, field
|
| 24 |
+
import aiohttp
|
| 25 |
+
import redis.asyncio as redis
|
| 26 |
+
from dotenv import load_dotenv
|
| 27 |
+
|
| 28 |
+
# Load environment variables
|
| 29 |
+
load_dotenv()
|
| 30 |
+
|
| 31 |
+
# Configure logging
|
| 32 |
+
logging.basicConfig(level=logging.INFO)
|
| 33 |
+
logger = logging.getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class ColossusConfig:
|
| 37 |
+
"""colossus Server Configuration"""
|
| 38 |
+
base_url: str = "https://ai.adrian-schupp.de"
|
| 39 |
+
api_key: str = field(default_factory=lambda: os.getenv("COLOSSUS_API_KEY", ""))
|
| 40 |
+
model: str = "mistral-small3.2:24b-instruct-2506"
|
| 41 |
+
max_tokens: int = 1000
|
| 42 |
+
temperature: float = 0.7
|
| 43 |
+
timeout: int = 30 # seconds
|
| 44 |
+
|
| 45 |
+
def __post_init__(self):
|
| 46 |
+
"""Validate configuration after initialization"""
|
| 47 |
+
if not self.api_key:
|
| 48 |
+
raise ValueError(
|
| 49 |
+
"❌ COLOSSUS_API_KEY environment variable not set.\n"
|
| 50 |
+
"Please set it in your .env file:\n"
|
| 51 |
+
"COLOSSUS_API_KEY=your-api-key-here"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
class ColosusSAAPAgent:
|
| 55 |
+
"""
|
| 56 |
+
SAAP Agent mit colossus Server Integration
|
| 57 |
+
|
| 58 |
+
Hybrid Architecture:
|
| 59 |
+
- CachyOS: Agent Orchestrierung, Message Queue, System Management
|
| 60 |
+
- colossus: High-Performance LLM Processing, AI Inference
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(self,
|
| 64 |
+
agent_name: str,
|
| 65 |
+
agent_role: str = "Coordinator",
|
| 66 |
+
config: Optional[ColossusConfig] = None,
|
| 67 |
+
redis_url: str = "redis://localhost:6379"):
|
| 68 |
+
|
| 69 |
+
self.agent_name = agent_name
|
| 70 |
+
self.agent_role = agent_role
|
| 71 |
+
self.config = config or ColossusConfig()
|
| 72 |
+
self.redis_url = redis_url
|
| 73 |
+
|
| 74 |
+
# Agent context for specialized roles
|
| 75 |
+
self.agent_contexts = {
|
| 76 |
+
"Coordinator": "Du bist Agent A (Coordinator) für SAAP. Du koordinierst Multi-Agent Workflows und delegierst Tasks effizient.",
|
| 77 |
+
"Developer": "Du bist Agent B (Developer) mit Expertise in Python, Node.js, Vue.js. Du fokussierst auf Clean Code und Performance.",
|
| 78 |
+
"Analyst": "Du bist Agent C (Analyst) für Requirements-Analyse, Use Cases und Systemdesign. Du lieferst strukturierte Analysen.",
|
| 79 |
+
"General": "Du bist ein SAAP Multi-Agent mit genereller KI-Expertise für vielseitige Tasks."
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# Performance tracking
|
| 83 |
+
self.performance_stats = {
|
| 84 |
+
"total_requests": 0,
|
| 85 |
+
"total_response_time": 0.0,
|
| 86 |
+
"average_response_time": 0.0,
|
| 87 |
+
"errors": 0,
|
| 88 |
+
"successful_requests": 0
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
# Redis connection (will be initialized async)
|
| 92 |
+
self.redis_client = None
|
| 93 |
+
|
| 94 |
+
async def initialize(self):
|
| 95 |
+
"""Initialize async components"""
|
| 96 |
+
try:
|
| 97 |
+
self.redis_client = await redis.from_url(self.redis_url)
|
| 98 |
+
await self.redis_client.ping()
|
| 99 |
+
logger.info(f"✅ {self.agent_name} connected to Redis")
|
| 100 |
+
|
| 101 |
+
# Register agent in Redis
|
| 102 |
+
await self.register_agent()
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f"❌ Redis connection failed for {self.agent_name}: {e}")
|
| 106 |
+
# Continue without Redis - degraded mode
|
| 107 |
+
|
| 108 |
+
async def register_agent(self):
|
| 109 |
+
"""Register agent with SAAP system"""
|
| 110 |
+
if not self.redis_client:
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
agent_info = {
|
| 114 |
+
"name": self.agent_name,
|
| 115 |
+
"role": self.agent_role,
|
| 116 |
+
"status": "active",
|
| 117 |
+
"model": self.config.model,
|
| 118 |
+
"server": "colossus",
|
| 119 |
+
"capabilities": ["llm_processing", "multi_agent_communication", "task_coordination"],
|
| 120 |
+
"performance_target": "< 2s response time",
|
| 121 |
+
"timestamp": time.time()
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
await self.redis_client.hset(
|
| 125 |
+
f"agent:{self.agent_name}",
|
| 126 |
+
mapping={k: json.dumps(v) if isinstance(v, (dict, list)) else str(v)
|
| 127 |
+
for k, v in agent_info.items()}
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# Add to active agents set
|
| 131 |
+
await self.redis_client.sadd("active_agents", self.agent_name)
|
| 132 |
+
|
| 133 |
+
logger.info(f"📝 {self.agent_name} registered with SAAP system")
|
| 134 |
+
|
| 135 |
+
async def call_colossus_api(self, prompt: str) -> Dict[str, Any]:
|
| 136 |
+
"""
|
| 137 |
+
Direct API call to colossus Server
|
| 138 |
+
|
| 139 |
+
Returns response with performance metrics
|
| 140 |
+
"""
|
| 141 |
+
start_time = time.time()
|
| 142 |
+
|
| 143 |
+
try:
|
| 144 |
+
headers = {
|
| 145 |
+
"Authorization": f"Bearer {self.config.api_key}",
|
| 146 |
+
"Content-Type": "application/json"
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
# Add agent context to prompt
|
| 150 |
+
context = self.agent_contexts.get(self.agent_role, self.agent_contexts["General"])
|
| 151 |
+
enhanced_prompt = f"{context}\\n\\nAufgabe: {prompt}"
|
| 152 |
+
|
| 153 |
+
payload = {
|
| 154 |
+
"model": self.config.model,
|
| 155 |
+
"messages": [
|
| 156 |
+
{"role": "user", "content": enhanced_prompt}
|
| 157 |
+
],
|
| 158 |
+
"max_tokens": self.config.max_tokens,
|
| 159 |
+
"temperature": self.config.temperature
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.config.timeout)) as session:
|
| 163 |
+
async with session.post(
|
| 164 |
+
f"{self.config.base_url}/v1/chat/completions",
|
| 165 |
+
headers=headers,
|
| 166 |
+
json=payload
|
| 167 |
+
) as response:
|
| 168 |
+
|
| 169 |
+
if response.status == 200:
|
| 170 |
+
result = await response.json()
|
| 171 |
+
|
| 172 |
+
# Extract response text
|
| 173 |
+
content = result.get("choices", [{}])[0].get("message", {}).get("content", "No response")
|
| 174 |
+
|
| 175 |
+
# Calculate performance
|
| 176 |
+
response_time = time.time() - start_time
|
| 177 |
+
|
| 178 |
+
# Update stats
|
| 179 |
+
self.performance_stats["total_requests"] += 1
|
| 180 |
+
self.performance_stats["successful_requests"] += 1
|
| 181 |
+
self.performance_stats["total_response_time"] += response_time
|
| 182 |
+
self.performance_stats["average_response_time"] = (
|
| 183 |
+
self.performance_stats["total_response_time"] /
|
| 184 |
+
self.performance_stats["total_requests"]
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
return {
|
| 188 |
+
"success": True,
|
| 189 |
+
"content": content,
|
| 190 |
+
"response_time": response_time,
|
| 191 |
+
"model": self.config.model,
|
| 192 |
+
"server": "colossus",
|
| 193 |
+
"agent": self.agent_name,
|
| 194 |
+
"role": self.agent_role,
|
| 195 |
+
"performance_check": response_time < 2.0 # Success if < 2s
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
else:
|
| 199 |
+
error_text = await response.text()
|
| 200 |
+
raise Exception(f"API Error {response.status}: {error_text}")
|
| 201 |
+
|
| 202 |
+
except Exception as e:
|
| 203 |
+
# Update error stats
|
| 204 |
+
self.performance_stats["errors"] += 1
|
| 205 |
+
self.performance_stats["total_requests"] += 1
|
| 206 |
+
|
| 207 |
+
response_time = time.time() - start_time
|
| 208 |
+
|
| 209 |
+
logger.error(f"❌ colossus API call failed: {e}")
|
| 210 |
+
|
| 211 |
+
return {
|
| 212 |
+
"success": False,
|
| 213 |
+
"error": str(e),
|
| 214 |
+
"response_time": response_time,
|
| 215 |
+
"agent": self.agent_name,
|
| 216 |
+
"server": "colossus",
|
| 217 |
+
"performance_check": False
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
async def process_message(self, message: str, context: Optional[Dict] = None) -> Dict[str, Any]:
|
| 221 |
+
"""
|
| 222 |
+
Process incoming message with colossus LLM
|
| 223 |
+
|
| 224 |
+
Integrates with SAAP Message Queue System
|
| 225 |
+
"""
|
| 226 |
+
|
| 227 |
+
# Log message processing
|
| 228 |
+
logger.info(f"🤖 {self.agent_name} ({self.agent_role}) processing message...")
|
| 229 |
+
|
| 230 |
+
# Call colossus API
|
| 231 |
+
result = await self.call_colossus_api(message)
|
| 232 |
+
|
| 233 |
+
# Add SAAP-specific metadata
|
| 234 |
+
result.update({
|
| 235 |
+
"agent_name": self.agent_name,
|
| 236 |
+
"agent_role": self.agent_role,
|
| 237 |
+
"timestamp": time.time(),
|
| 238 |
+
"message_id": context.get("message_id") if context else None,
|
| 239 |
+
"thread_id": context.get("thread_id") if context else None
|
| 240 |
+
})
|
| 241 |
+
|
| 242 |
+
# Store in Redis if available
|
| 243 |
+
if self.redis_client and result["success"]:
|
| 244 |
+
await self._store_message_result(message, result)
|
| 245 |
+
|
| 246 |
+
# Performance logging
|
| 247 |
+
performance_emoji = "⚡" if result.get("performance_check", False) else "⏱️"
|
| 248 |
+
logger.info(f"{performance_emoji} {self.agent_name}: {result.get('response_time', 0):.2f}s")
|
| 249 |
+
|
| 250 |
+
return result
|
| 251 |
+
|
| 252 |
+
async def _store_message_result(self, message: str, result: Dict[str, Any]):
|
| 253 |
+
"""Store message and result in Redis for monitoring"""
|
| 254 |
+
if not self.redis_client:
|
| 255 |
+
return
|
| 256 |
+
|
| 257 |
+
message_data = {
|
| 258 |
+
"input": message,
|
| 259 |
+
"output": result.get("content", ""),
|
| 260 |
+
"agent": self.agent_name,
|
| 261 |
+
"role": self.agent_role,
|
| 262 |
+
"response_time": result.get("response_time", 0),
|
| 263 |
+
"timestamp": result.get("timestamp", time.time()),
|
| 264 |
+
"success": result.get("success", False)
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
# Store in message history
|
| 268 |
+
await self.redis_client.lpush(
|
| 269 |
+
f"messages:{self.agent_name}",
|
| 270 |
+
json.dumps(message_data)
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# Keep only recent messages (last 100)
|
| 274 |
+
await self.redis_client.ltrim(f"messages:{self.agent_name}", 0, 99)
|
| 275 |
+
|
| 276 |
+
# Update agent status
|
| 277 |
+
await self.redis_client.hset(
|
| 278 |
+
f"agent:{self.agent_name}",
|
| 279 |
+
"last_activity",
|
| 280 |
+
str(time.time())
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
async def get_performance_stats(self) -> Dict[str, Any]:
|
| 284 |
+
"""Get comprehensive performance statistics"""
|
| 285 |
+
stats = self.performance_stats.copy()
|
| 286 |
+
|
| 287 |
+
# Add colossus-specific metrics
|
| 288 |
+
stats.update({
|
| 289 |
+
"server": "colossus",
|
| 290 |
+
"model": self.config.model,
|
| 291 |
+
"performance_target_met": stats["average_response_time"] < 2.0,
|
| 292 |
+
"success_rate": (
|
| 293 |
+
(stats["successful_requests"] / stats["total_requests"]) * 100
|
| 294 |
+
if stats["total_requests"] > 0 else 0
|
| 295 |
+
),
|
| 296 |
+
"agent_name": self.agent_name,
|
| 297 |
+
"agent_role": self.agent_role
|
| 298 |
+
})
|
| 299 |
+
|
| 300 |
+
return stats
|
| 301 |
+
|
| 302 |
+
async def cleanup(self):
|
| 303 |
+
"""Cleanup connections"""
|
| 304 |
+
if self.redis_client:
|
| 305 |
+
await self.redis_client.srem("active_agents", self.agent_name)
|
| 306 |
+
await self.redis_client.close()
|
| 307 |
+
|
| 308 |
+
# Example Usage & Testing
|
| 309 |
+
async def test_colossus_integration():
|
| 310 |
+
"""Test colossus Server Integration"""
|
| 311 |
+
|
| 312 |
+
print("🚀 Testing SAAP colossus Server Integration...")
|
| 313 |
+
|
| 314 |
+
# Create test agents
|
| 315 |
+
agents = [
|
| 316 |
+
ColosusSAAPAgent("agent_coordinator", "Coordinator"),
|
| 317 |
+
ColosusSAAPAgent("agent_developer", "Developer"),
|
| 318 |
+
ColosusSAAPAgent("agent_analyst", "Analyst")
|
| 319 |
+
]
|
| 320 |
+
|
| 321 |
+
# Initialize all agents
|
| 322 |
+
for agent in agents:
|
| 323 |
+
await agent.initialize()
|
| 324 |
+
|
| 325 |
+
# Test messages
|
| 326 |
+
test_messages = [
|
| 327 |
+
"Analysiere die SAAP Multi-Agent-Architektur und identifiziere Optimierungsbedarfe.",
|
| 328 |
+
"Entwickle Python Code für Redis Message Queue Integration.",
|
| 329 |
+
"Erstelle Use Cases für Agent-zu-Agent Kommunikation."
|
| 330 |
+
]
|
| 331 |
+
|
| 332 |
+
# Process messages in parallel
|
| 333 |
+
tasks = []
|
| 334 |
+
for i, agent in enumerate(agents):
|
| 335 |
+
message = test_messages[i % len(test_messages)]
|
| 336 |
+
tasks.append(agent.process_message(message, {"test_id": i}))
|
| 337 |
+
|
| 338 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 339 |
+
|
| 340 |
+
# Print results
|
| 341 |
+
print("\\n" + "="*60)
|
| 342 |
+
print("🎯 SAAP colossus Integration Results:")
|
| 343 |
+
print("="*60)
|
| 344 |
+
|
| 345 |
+
for i, result in enumerate(results):
|
| 346 |
+
if isinstance(result, Exception):
|
| 347 |
+
print(f"❌ Agent {i+1}: Error - {result}")
|
| 348 |
+
else:
|
| 349 |
+
agent_name = result.get("agent_name", f"Agent_{i+1}")
|
| 350 |
+
response_time = result.get("response_time", 0)
|
| 351 |
+
success = result.get("success", False)
|
| 352 |
+
performance = "✅ < 2s" if result.get("performance_check", False) else f"⏱️ {response_time:.2f}s"
|
| 353 |
+
|
| 354 |
+
print(f"{'✅' if success else '❌'} {agent_name}: {performance}")
|
| 355 |
+
if success:
|
| 356 |
+
content = result.get("content", "")[:100] + "..." if len(result.get("content", "")) > 100 else result.get("content", "")
|
| 357 |
+
print(f" Response: {content}")
|
| 358 |
+
|
| 359 |
+
# Performance summary
|
| 360 |
+
print("\\n" + "="*60)
|
| 361 |
+
print("📊 Performance Summary:")
|
| 362 |
+
|
| 363 |
+
for agent in agents:
|
| 364 |
+
stats = await agent.get_performance_stats()
|
| 365 |
+
print(f"🤖 {agent.agent_name} ({agent.agent_role}):")
|
| 366 |
+
print(f" Average Response Time: {stats['average_response_time']:.2f}s")
|
| 367 |
+
print(f" Success Rate: {stats['success_rate']:.1f}%")
|
| 368 |
+
print(f" Performance Target Met: {'✅' if stats['performance_target_met'] else '❌'}")
|
| 369 |
+
|
| 370 |
+
# Cleanup
|
| 371 |
+
for agent in agents:
|
| 372 |
+
await agent.cleanup()
|
| 373 |
+
|
| 374 |
+
print("\\n🎉 colossus Integration Test Complete!")
|
| 375 |
+
|
| 376 |
+
if __name__ == "__main__":
|
| 377 |
+
asyncio.run(test_colossus_integration())
|
backend/agents/colossus_saap_agent.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
SAAP Colossus Server Integration Agent
|
| 4 |
+
Integration von colossus Server (https://ai.adrian-schupp.de) in SAAP Multi-Agent System
|
| 5 |
+
Author: Hanan Wandji Danga & Jane Alesi
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
import requests
|
| 10 |
+
import json
|
| 11 |
+
import time
|
| 12 |
+
import asyncio
|
| 13 |
+
import redis
|
| 14 |
+
from typing import Dict, List, Optional, Any
|
| 15 |
+
import logging
|
| 16 |
+
|
| 17 |
+
# Load environment variables
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
class ColossusSAAPAgent:
|
| 21 |
+
def __init__(self, agent_name: str, role: str, api_key: str, base_url: str = "https://ai.adrian-schupp.de"):
|
| 22 |
+
"""
|
| 23 |
+
Initialisierung des Colossus SAAP Agents
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
agent_name: Name des Agents (z.B. "agent_coordinator")
|
| 27 |
+
role: Rolle des Agents (z.B. "Coordinator", "Developer", "Analyst")
|
| 28 |
+
api_key: API Key für colossus Server
|
| 29 |
+
base_url: Base URL des colossus Servers
|
| 30 |
+
"""
|
| 31 |
+
self.agent_name = agent_name
|
| 32 |
+
self.role = role
|
| 33 |
+
self.api_key = api_key
|
| 34 |
+
self.base_url = base_url
|
| 35 |
+
self.model_name = "mistral-small3.2:24b-instruct-2506"
|
| 36 |
+
|
| 37 |
+
# Redis Configuration für SAAP Message Queue
|
| 38 |
+
self.redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
|
| 39 |
+
self.message_queue = f"saap_agent_{agent_name}"
|
| 40 |
+
|
| 41 |
+
# Agent Context für rollenspezifische Antworten
|
| 42 |
+
self.context = self._initialize_context()
|
| 43 |
+
|
| 44 |
+
# Logging Setup
|
| 45 |
+
logging.basicConfig(level=logging.INFO)
|
| 46 |
+
self.logger = logging.getLogger(f"ColossusSAAP.{agent_name}")
|
| 47 |
+
|
| 48 |
+
self.logger.info(f"🚀 {agent_name} ({role}) initiated with colossus Server")
|
| 49 |
+
|
| 50 |
+
def _initialize_context(self) -> str:
|
| 51 |
+
"""Initialisiert rollenspezifischen Kontext für den Agent"""
|
| 52 |
+
contexts = {
|
| 53 |
+
"Coordinator": """Du bist der SAAP Agent Coordinator. Deine Aufgabe ist es:
|
| 54 |
+
- Multi-Agent Workflows zu koordinieren
|
| 55 |
+
- Aufgaben an spezialisierte Agenten zu delegieren
|
| 56 |
+
- Systemüberblick zu behalten und Entscheidungen zu treffen
|
| 57 |
+
- Effizienz und Performance des gesamten SAAP-Systems zu optimieren""",
|
| 58 |
+
|
| 59 |
+
"Developer": """Du bist der SAAP Developer Agent. Deine Aufgabe ist es:
|
| 60 |
+
- Code zu schreiben und zu überprüfen
|
| 61 |
+
- Technische Implementierungen zu planen
|
| 62 |
+
- Architektur-Entscheidungen zu treffen
|
| 63 |
+
- Code-Qualität und Best Practices sicherzustellen""",
|
| 64 |
+
|
| 65 |
+
"Analyst": """Du bist der SAAP Analyst Agent. Deine Aufgabe ist es:
|
| 66 |
+
- Daten und Anforderungen zu analysieren
|
| 67 |
+
- Use Cases zu definieren und dokumentieren
|
| 68 |
+
- System-Performance zu bewerten
|
| 69 |
+
- Optimierungspotenziale zu identifizieren""",
|
| 70 |
+
|
| 71 |
+
"default": f"""Du bist ein SAAP Agent mit der Rolle '{self.role}'.
|
| 72 |
+
Beantworte Anfragen professionell und hilfsbereit basierend auf deiner Spezialisierung."""
|
| 73 |
+
}
|
| 74 |
+
return contexts.get(self.role, contexts["default"])
|
| 75 |
+
|
| 76 |
+
async def send_request_to_colossus(self, prompt: str, max_tokens: int = 500) -> Dict[str, Any]:
|
| 77 |
+
"""
|
| 78 |
+
Sendet Request an colossus Server und misst Performance
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
prompt: Der Eingabe-Prompt
|
| 82 |
+
max_tokens: Maximale Token-Anzahl für Response
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Dict mit Response, Performance-Metriken und Metadaten
|
| 86 |
+
"""
|
| 87 |
+
start_time = time.time()
|
| 88 |
+
|
| 89 |
+
# Request Headers
|
| 90 |
+
headers = {
|
| 91 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 92 |
+
"Content-Type": "application/json"
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# Request Body (OpenAI-kompatible API vermutlich)
|
| 96 |
+
payload = {
|
| 97 |
+
"model": self.model_name,
|
| 98 |
+
"messages": [
|
| 99 |
+
{"role": "system", "content": self.context},
|
| 100 |
+
{"role": "user", "content": prompt}
|
| 101 |
+
],
|
| 102 |
+
"max_tokens": max_tokens,
|
| 103 |
+
"temperature": 0.7
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
# API Call an colossus Server
|
| 108 |
+
response = requests.post(
|
| 109 |
+
f"{self.base_url}/v1/chat/completions", # Standard OpenAI API Format
|
| 110 |
+
headers=headers,
|
| 111 |
+
json=payload,
|
| 112 |
+
timeout=30
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
response_time = time.time() - start_time
|
| 116 |
+
|
| 117 |
+
if response.status_code == 200:
|
| 118 |
+
data = response.json()
|
| 119 |
+
|
| 120 |
+
# Performance Metrics berechnen
|
| 121 |
+
response_text = data['choices'][0]['message']['content']
|
| 122 |
+
token_count = data.get('usage', {}).get('total_tokens', 0)
|
| 123 |
+
|
| 124 |
+
result = {
|
| 125 |
+
"success": True,
|
| 126 |
+
"response": response_text,
|
| 127 |
+
"response_time": round(response_time, 2),
|
| 128 |
+
"token_count": token_count,
|
| 129 |
+
"model": self.model_name,
|
| 130 |
+
"agent_role": self.role,
|
| 131 |
+
"timestamp": time.time()
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
self.logger.info(f"✅ colossus Response: {response_time:.2f}s, {token_count} tokens")
|
| 135 |
+
return result
|
| 136 |
+
|
| 137 |
+
else:
|
| 138 |
+
error_result = {
|
| 139 |
+
"success": False,
|
| 140 |
+
"error": f"HTTP {response.status_code}: {response.text}",
|
| 141 |
+
"response_time": round(response_time, 2),
|
| 142 |
+
"timestamp": time.time()
|
| 143 |
+
}
|
| 144 |
+
self.logger.error(f"❌ colossus Error: {response.status_code}")
|
| 145 |
+
return error_result
|
| 146 |
+
|
| 147 |
+
except requests.exceptions.RequestException as e:
|
| 148 |
+
error_result = {
|
| 149 |
+
"success": False,
|
| 150 |
+
"error": f"Request failed: {str(e)}",
|
| 151 |
+
"response_time": round(time.time() - start_time, 2),
|
| 152 |
+
"timestamp": time.time()
|
| 153 |
+
}
|
| 154 |
+
self.logger.error(f"❌ colossus Connection Error: {e}")
|
| 155 |
+
return error_result
|
| 156 |
+
|
| 157 |
+
async def process_message(self, message: str, sender: str = "system") -> Dict[str, Any]:
|
| 158 |
+
"""
|
| 159 |
+
Verarbeitet eingehende Nachricht und generiert intelligente Antwort
|
| 160 |
+
"""
|
| 161 |
+
self.logger.info(f"🔄 {self.agent_name} processing message from {sender}")
|
| 162 |
+
|
| 163 |
+
# Erweiterte Prompt-Konstruktion mit Sender-Kontext
|
| 164 |
+
enhanced_prompt = f"""[Nachricht von {sender}]
|
| 165 |
+
{message}
|
| 166 |
+
|
| 167 |
+
Bitte antworte als {self.role} Agent im SAAP System. Sei präzise und hilfreich."""
|
| 168 |
+
|
| 169 |
+
# colossus Server Request
|
| 170 |
+
result = await self.send_request_to_colossus(enhanced_prompt)
|
| 171 |
+
|
| 172 |
+
if result["success"]:
|
| 173 |
+
# Message in Redis Queue für andere Agenten
|
| 174 |
+
response_data = {
|
| 175 |
+
"agent_name": self.agent_name,
|
| 176 |
+
"agent_role": self.role,
|
| 177 |
+
"original_message": message,
|
| 178 |
+
"response": result["response"],
|
| 179 |
+
"sender": sender,
|
| 180 |
+
"performance": {
|
| 181 |
+
"response_time": result["response_time"],
|
| 182 |
+
"token_count": result["token_count"]
|
| 183 |
+
},
|
| 184 |
+
"timestamp": result["timestamp"],
|
| 185 |
+
"server": "colossus"
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
# Publish to Redis for other agents and dashboard
|
| 189 |
+
self.redis_client.lpush(f"saap_responses", json.dumps(response_data))
|
| 190 |
+
self.redis_client.publish(f"saap_agent_updates", json.dumps(response_data))
|
| 191 |
+
|
| 192 |
+
self.logger.info(f"✅ Response generated and published to Redis")
|
| 193 |
+
return result
|
| 194 |
+
else:
|
| 195 |
+
self.logger.error(f"❌ Failed to process message: {result.get('error')}")
|
| 196 |
+
return result
|
| 197 |
+
|
| 198 |
+
async def listen_for_messages(self):
|
| 199 |
+
"""
|
| 200 |
+
Lauscht auf eingehende Nachrichten in der Redis Queue
|
| 201 |
+
"""
|
| 202 |
+
self.logger.info(f"👂 {self.agent_name} listening for messages on {self.message_queue}")
|
| 203 |
+
|
| 204 |
+
while True:
|
| 205 |
+
try:
|
| 206 |
+
# Pop message from Redis queue
|
| 207 |
+
message_data = self.redis_client.brpop(self.message_queue, timeout=1)
|
| 208 |
+
|
| 209 |
+
if message_data:
|
| 210 |
+
_, message_json = message_data
|
| 211 |
+
message_obj = json.loads(message_json)
|
| 212 |
+
|
| 213 |
+
# Process the message
|
| 214 |
+
await self.process_message(
|
| 215 |
+
message=message_obj.get("content", ""),
|
| 216 |
+
sender=message_obj.get("sender", "unknown")
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Small delay to prevent excessive CPU usage
|
| 220 |
+
await asyncio.sleep(0.1)
|
| 221 |
+
|
| 222 |
+
except KeyboardInterrupt:
|
| 223 |
+
self.logger.info(f"🛑 {self.agent_name} shutting down")
|
| 224 |
+
break
|
| 225 |
+
except Exception as e:
|
| 226 |
+
self.logger.error(f"❌ Error in message listener: {e}")
|
| 227 |
+
await asyncio.sleep(1)
|
| 228 |
+
|
| 229 |
+
def send_message_to_agent(self, target_agent: str, message: str):
|
| 230 |
+
"""
|
| 231 |
+
Sendet Nachricht an einen anderen SAAP Agent
|
| 232 |
+
"""
|
| 233 |
+
message_data = {
|
| 234 |
+
"content": message,
|
| 235 |
+
"sender": self.agent_name,
|
| 236 |
+
"timestamp": time.time()
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
self.redis_client.lpush(f"saap_agent_{target_agent}", json.dumps(message_data))
|
| 240 |
+
self.logger.info(f"📤 Message sent to {target_agent}")
|
| 241 |
+
|
| 242 |
+
async def health_check(self) -> Dict[str, Any]:
|
| 243 |
+
"""
|
| 244 |
+
Überprüft Gesundheitsstatus des colossus Servers
|
| 245 |
+
"""
|
| 246 |
+
try:
|
| 247 |
+
test_prompt = "Hello, this is a connection test."
|
| 248 |
+
result = await self.send_request_to_colossus(test_prompt, max_tokens=50)
|
| 249 |
+
|
| 250 |
+
health_status = {
|
| 251 |
+
"agent_name": self.agent_name,
|
| 252 |
+
"colossus_status": "healthy" if result["success"] else "unhealthy",
|
| 253 |
+
"response_time": result.get("response_time", 0),
|
| 254 |
+
"error": result.get("error") if not result["success"] else None,
|
| 255 |
+
"timestamp": time.time()
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
return health_status
|
| 259 |
+
|
| 260 |
+
except Exception as e:
|
| 261 |
+
return {
|
| 262 |
+
"agent_name": self.agent_name,
|
| 263 |
+
"colossus_status": "error",
|
| 264 |
+
"error": str(e),
|
| 265 |
+
"timestamp": time.time()
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
# Performance Benchmarking Funktionen
|
| 269 |
+
class ColossusBenchmark:
|
| 270 |
+
def __init__(self, api_key: str):
|
| 271 |
+
self.api_key = api_key
|
| 272 |
+
self.base_url = "https://ai.adrian-schupp.de"
|
| 273 |
+
|
| 274 |
+
async def run_performance_benchmark(self, test_prompts: List[str]) -> Dict[str, Any]:
|
| 275 |
+
"""
|
| 276 |
+
Führt Performance-Benchmark mit verschiedenen Test-Prompts durch
|
| 277 |
+
"""
|
| 278 |
+
results = []
|
| 279 |
+
|
| 280 |
+
# Temporärer Agent für Benchmark
|
| 281 |
+
benchmark_agent = ColossusSAAPAgent("benchmark_agent", "Benchmark", self.api_key)
|
| 282 |
+
|
| 283 |
+
for i, prompt in enumerate(test_prompts):
|
| 284 |
+
print(f"🧪 Running test {i+1}/{len(test_prompts)}: {prompt[:50]}...")
|
| 285 |
+
|
| 286 |
+
result = await benchmark_agent.send_request_to_colossus(prompt)
|
| 287 |
+
results.append({
|
| 288 |
+
"test_id": i + 1,
|
| 289 |
+
"prompt": prompt,
|
| 290 |
+
"success": result["success"],
|
| 291 |
+
"response_time": result.get("response_time", 0),
|
| 292 |
+
"token_count": result.get("token_count", 0),
|
| 293 |
+
"error": result.get("error") if not result["success"] else None
|
| 294 |
+
})
|
| 295 |
+
|
| 296 |
+
# Pause zwischen Tests um Server nicht zu überlasten
|
| 297 |
+
await asyncio.sleep(1)
|
| 298 |
+
|
| 299 |
+
# Statistiken berechnen
|
| 300 |
+
successful_tests = [r for r in results if r["success"]]
|
| 301 |
+
|
| 302 |
+
if successful_tests:
|
| 303 |
+
avg_response_time = sum(r["response_time"] for r in successful_tests) / len(successful_tests)
|
| 304 |
+
avg_token_count = sum(r["token_count"] for r in successful_tests) / len(successful_tests)
|
| 305 |
+
|
| 306 |
+
benchmark_summary = {
|
| 307 |
+
"total_tests": len(test_prompts),
|
| 308 |
+
"successful_tests": len(successful_tests),
|
| 309 |
+
"success_rate": len(successful_tests) / len(test_prompts) * 100,
|
| 310 |
+
"average_response_time": round(avg_response_time, 2),
|
| 311 |
+
"average_token_count": round(avg_token_count, 0),
|
| 312 |
+
"performance_target_met": avg_response_time < 2.0, # < 2s Ziel
|
| 313 |
+
"results": results
|
| 314 |
+
}
|
| 315 |
+
else:
|
| 316 |
+
benchmark_summary = {
|
| 317 |
+
"total_tests": len(test_prompts),
|
| 318 |
+
"successful_tests": 0,
|
| 319 |
+
"success_rate": 0,
|
| 320 |
+
"error": "No successful tests completed",
|
| 321 |
+
"results": results
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
return benchmark_summary
|
| 325 |
+
|
| 326 |
+
# Utility Functions für SAAP Integration
|
| 327 |
+
def create_saap_colossus_agents(api_key: str) -> List[ColossusSAAPAgent]:
|
| 328 |
+
"""
|
| 329 |
+
Erstellt die 3 Basis-Agenten für SAAP Multi-Agent System wie im Plan
|
| 330 |
+
"""
|
| 331 |
+
agents = [
|
| 332 |
+
ColossusSAAPAgent("agent_coordinator", "Coordinator", api_key),
|
| 333 |
+
ColossusSAAPAgent("agent_developer", "Developer", api_key),
|
| 334 |
+
ColossusSAAPAgent("agent_analyst", "Analyst", api_key)
|
| 335 |
+
]
|
| 336 |
+
return agents
|
| 337 |
+
|
| 338 |
+
if __name__ == "__main__":
|
| 339 |
+
# Demo und Testing
|
| 340 |
+
import asyncio
|
| 341 |
+
|
| 342 |
+
# Load API key from environment variable
|
| 343 |
+
API_KEY = os.getenv("COLOSSUS_API_KEY")
|
| 344 |
+
|
| 345 |
+
if not API_KEY:
|
| 346 |
+
print("❌ Error: COLOSSUS_API_KEY not set in environment variables")
|
| 347 |
+
print("Please set it in backend/.env file:")
|
| 348 |
+
print("COLOSSUS_API_KEY=sk-your-actual-key-here")
|
| 349 |
+
exit(1)
|
| 350 |
+
|
| 351 |
+
async def demo_colossus_integration():
|
| 352 |
+
print("🚀 SAAP colossus Server Integration Demo")
|
| 353 |
+
|
| 354 |
+
# Create test agent
|
| 355 |
+
agent = ColossusSAAPAgent("demo_coordinator", "Coordinator", API_KEY)
|
| 356 |
+
|
| 357 |
+
# Health check
|
| 358 |
+
print("\n📊 Health Check...")
|
| 359 |
+
health = await agent.health_check()
|
| 360 |
+
print(f"Status: {health}")
|
| 361 |
+
|
| 362 |
+
# Test message processing
|
| 363 |
+
if health.get("colossus_status") == "healthy":
|
| 364 |
+
print("\n💬 Processing test message...")
|
| 365 |
+
result = await agent.process_message(
|
| 366 |
+
"Erkläre mir die Vorteile einer Multi-Agent-Architektur für SAAP.",
|
| 367 |
+
"test_user"
|
| 368 |
+
)
|
| 369 |
+
print(f"Response: {result.get('response', 'No response')}")
|
| 370 |
+
|
| 371 |
+
# Performance benchmark
|
| 372 |
+
print("\n🧪 Running mini performance benchmark...")
|
| 373 |
+
benchmark = ColossusBenchmark(API_KEY)
|
| 374 |
+
test_prompts = [
|
| 375 |
+
"Was ist SAAP?",
|
| 376 |
+
"Erkläre Multi-Agent Systeme in 3 Sätzen.",
|
| 377 |
+
"Vorteile von On-Premise vs Cloud AI?"
|
| 378 |
+
]
|
| 379 |
+
|
| 380 |
+
benchmark_results = await benchmark.run_performance_benchmark(test_prompts)
|
| 381 |
+
print(f"Benchmark Results: {benchmark_results}")
|
| 382 |
+
|
| 383 |
+
# Run demo
|
| 384 |
+
asyncio.run(demo_colossus_integration())
|
backend/agents/openrouter_agent_enhanced.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Enhanced OpenRouter SAAP Agent - Cost-Efficient Models
|
| 4 |
+
OpenAI Models via OpenRouter with role-specific assignment and cost tracking
|
| 5 |
+
Author: Hanan Wandji Danga
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
import aiohttp
|
| 11 |
+
import json
|
| 12 |
+
import time
|
| 13 |
+
import asyncio
|
| 14 |
+
import logging
|
| 15 |
+
from typing import Dict, List, Optional, Any
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
|
| 18 |
+
# Load environment variables
|
| 19 |
+
load_dotenv()
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
class EnhancedOpenRouterAgent:
|
| 24 |
+
"""
|
| 25 |
+
Enhanced OpenRouter Agent with cost-efficient model selection
|
| 26 |
+
Optimized for performance and cost tracking
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, agent_name: str, role: str, api_key: str):
|
| 30 |
+
self.agent_name = agent_name
|
| 31 |
+
self.role = role
|
| 32 |
+
self.api_key = api_key
|
| 33 |
+
self.base_url = "https://openrouter.ai/api/v1"
|
| 34 |
+
|
| 35 |
+
# Cost-Efficient Model Assignment by Role
|
| 36 |
+
self.role_model_mapping = {
|
| 37 |
+
"Coordinator": {
|
| 38 |
+
"model": "openai/gpt-4o-mini", # $0.15/1M tokens - Fast coordination
|
| 39 |
+
"max_tokens": 800,
|
| 40 |
+
"temperature": 0.7
|
| 41 |
+
},
|
| 42 |
+
"Developer": {
|
| 43 |
+
"model": "anthropic/claude-3-haiku", # $0.25/1M tokens - Code expertise
|
| 44 |
+
"max_tokens": 1200,
|
| 45 |
+
"temperature": 0.5
|
| 46 |
+
},
|
| 47 |
+
"Medical": {
|
| 48 |
+
"model": "openai/gpt-4o-mini", # $0.15/1M tokens - Accurate but cost-efficient
|
| 49 |
+
"max_tokens": 1000,
|
| 50 |
+
"temperature": 0.3
|
| 51 |
+
},
|
| 52 |
+
"Legal": {
|
| 53 |
+
"model": "openai/gpt-4o-mini", # $0.15/1M tokens - Precise legal analysis
|
| 54 |
+
"max_tokens": 1000,
|
| 55 |
+
"temperature": 0.3
|
| 56 |
+
},
|
| 57 |
+
"Analyst": {
|
| 58 |
+
"model": "meta-llama/llama-3.2-3b-instruct:free", # FREE - Data analysis
|
| 59 |
+
"max_tokens": 600,
|
| 60 |
+
"temperature": 0.6
|
| 61 |
+
},
|
| 62 |
+
"Fallback": {
|
| 63 |
+
"model": "meta-llama/llama-3.2-3b-instruct:free", # FREE - Backup
|
| 64 |
+
"max_tokens": 400,
|
| 65 |
+
"temperature": 0.7
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
# Model cost tracking (cost per 1M tokens)
|
| 70 |
+
self.model_costs = {
|
| 71 |
+
"openai/gpt-4o-mini": 0.15,
|
| 72 |
+
"anthropic/claude-3-haiku": 0.25,
|
| 73 |
+
"meta-llama/llama-3.2-3b-instruct:free": 0.0,
|
| 74 |
+
"openai/gpt-3.5-turbo": 0.50,
|
| 75 |
+
"mistral/mistral-7b-instruct:free": 0.0
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Get model config for this role
|
| 79 |
+
self.model_config = self.role_model_mapping.get(role, self.role_model_mapping["Fallback"])
|
| 80 |
+
self.model_name = self.model_config["model"]
|
| 81 |
+
|
| 82 |
+
# Agent Context
|
| 83 |
+
self.context = self._initialize_context()
|
| 84 |
+
|
| 85 |
+
logger.info(f"🌐 {agent_name} ({role}) initialized with {self.model_name} (${self.model_costs.get(self.model_name, 0)}/1M tokens)")
|
| 86 |
+
|
| 87 |
+
def _initialize_context(self) -> str:
|
| 88 |
+
"""Role-specific context for optimal performance"""
|
| 89 |
+
contexts = {
|
| 90 |
+
"Coordinator": """Du bist Jane Alesi, die leitende KI-Architektin von SAAP. Du koordinierst Multi-Agent-Systeme und hilfst bei:
|
| 91 |
+
- Agent-Orchestrierung und Workflow-Management
|
| 92 |
+
- Technische Architektur-Entscheidungen
|
| 93 |
+
- Team-Koordination zwischen Entwicklern und Spezialisten
|
| 94 |
+
- Performance-Optimierung von Agent-Communications
|
| 95 |
+
Antworte präzise und fokussiert auf Koordinations-Aufgaben.""",
|
| 96 |
+
|
| 97 |
+
"Developer": """Du bist John Alesi, ein fortgeschrittener Softwareentwickler für AGI-Systeme. Du spezialisierst dich auf:
|
| 98 |
+
- Python/Node.js Backend-Entwicklung
|
| 99 |
+
- FastAPI und Database-Integration
|
| 100 |
+
- Agent Communication Protocols
|
| 101 |
+
- Code-Optimierung und Debugging
|
| 102 |
+
Antworte mit konkreten, implementierbaren Lösungen.""",
|
| 103 |
+
|
| 104 |
+
"Medical": """Du bist Lara Alesi, medizinische AI-Expertin. Du hilfst bei:
|
| 105 |
+
- Medizinischen Fachfragen und Diagnose-Unterstützung
|
| 106 |
+
- Healthcare-Compliance und Standards
|
| 107 |
+
- Medizinische Datenanalyse
|
| 108 |
+
- Gesundheitswesen-spezifische AI-Anwendungen
|
| 109 |
+
Antworte wissenschaftlich fundiert und präzise.""",
|
| 110 |
+
|
| 111 |
+
"Legal": """Du bist Justus Alesi, Rechtsexperte für Deutschland, Schweiz und EU. Du hilfst bei:
|
| 112 |
+
- DSGVO-Compliance und Datenschutz
|
| 113 |
+
- Rechtliche Bewertung von AI-Systemen
|
| 114 |
+
- Vertragsrecht und Licensing
|
| 115 |
+
- Regulatorische Anforderungen
|
| 116 |
+
Antworte rechtlich fundiert und vorsichtig.""",
|
| 117 |
+
|
| 118 |
+
"Analyst": """Du bist ein SAAP Analyst Agent. Du spezialisierst dich auf:
|
| 119 |
+
- Datenanalyse und Performance-Metriken
|
| 120 |
+
- System-Monitoring und Optimierungspotentiale
|
| 121 |
+
- Requirements Engineering und Use Case Analysis
|
| 122 |
+
- Benchmarking und Vergleichsstudien
|
| 123 |
+
Antworte datengetrieben und analytisch."""
|
| 124 |
+
}
|
| 125 |
+
return contexts.get(self.role, contexts["Analyst"])
|
| 126 |
+
|
| 127 |
+
async def send_request(self, prompt: str, track_costs: bool = True) -> Dict[str, Any]:
|
| 128 |
+
"""
|
| 129 |
+
Send request to OpenRouter with enhanced cost tracking
|
| 130 |
+
"""
|
| 131 |
+
start_time = time.time()
|
| 132 |
+
|
| 133 |
+
headers = {
|
| 134 |
+
"Content-Type": "application/json",
|
| 135 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 136 |
+
"HTTP-Referer": "https://saap.satware.com", # Optional for tracking
|
| 137 |
+
"X-Title": f"SAAP {self.role} Agent" # For OpenRouter dashboard
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
payload = {
|
| 141 |
+
"model": self.model_name,
|
| 142 |
+
"messages": [
|
| 143 |
+
{"role": "system", "content": self.context},
|
| 144 |
+
{"role": "user", "content": prompt}
|
| 145 |
+
],
|
| 146 |
+
"max_tokens": self.model_config["max_tokens"],
|
| 147 |
+
"temperature": self.model_config["temperature"],
|
| 148 |
+
"top_p": 1,
|
| 149 |
+
"frequency_penalty": 0,
|
| 150 |
+
"presence_penalty": 0
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
async with aiohttp.ClientSession() as session:
|
| 155 |
+
async with session.post(
|
| 156 |
+
f"{self.base_url}/chat/completions",
|
| 157 |
+
headers=headers,
|
| 158 |
+
json=payload,
|
| 159 |
+
timeout=aiohttp.ClientTimeout(total=45)
|
| 160 |
+
) as response:
|
| 161 |
+
|
| 162 |
+
response_time = time.time() - start_time
|
| 163 |
+
|
| 164 |
+
if response.status == 200:
|
| 165 |
+
data = await response.json()
|
| 166 |
+
|
| 167 |
+
response_text = data['choices'][0]['message']['content']
|
| 168 |
+
usage = data.get('usage', {})
|
| 169 |
+
|
| 170 |
+
# Enhanced cost calculation
|
| 171 |
+
total_tokens = usage.get('total_tokens', 0)
|
| 172 |
+
prompt_tokens = usage.get('prompt_tokens', 0)
|
| 173 |
+
completion_tokens = usage.get('completion_tokens', 0)
|
| 174 |
+
|
| 175 |
+
# Calculate cost
|
| 176 |
+
cost_per_1m_tokens = self.model_costs.get(self.model_name, 0)
|
| 177 |
+
estimated_cost = (total_tokens / 1_000_000) * cost_per_1m_tokens
|
| 178 |
+
|
| 179 |
+
# Performance metrics
|
| 180 |
+
tokens_per_second = total_tokens / response_time if response_time > 0 else 0
|
| 181 |
+
cost_per_second = estimated_cost / response_time if response_time > 0 else 0
|
| 182 |
+
|
| 183 |
+
result = {
|
| 184 |
+
"success": True,
|
| 185 |
+
"response": response_text,
|
| 186 |
+
"performance_metrics": {
|
| 187 |
+
"response_time": round(response_time, 3),
|
| 188 |
+
"tokens_per_second": round(tokens_per_second, 2),
|
| 189 |
+
"cost_per_second": round(cost_per_second, 6)
|
| 190 |
+
},
|
| 191 |
+
"usage_metrics": {
|
| 192 |
+
"prompt_tokens": prompt_tokens,
|
| 193 |
+
"completion_tokens": completion_tokens,
|
| 194 |
+
"total_tokens": total_tokens
|
| 195 |
+
},
|
| 196 |
+
"cost_metrics": {
|
| 197 |
+
"estimated_cost_usd": round(estimated_cost, 6),
|
| 198 |
+
"cost_per_1m_tokens": cost_per_1m_tokens,
|
| 199 |
+
"model_name": self.model_name,
|
| 200 |
+
"is_free_model": cost_per_1m_tokens == 0
|
| 201 |
+
},
|
| 202 |
+
"agent_info": {
|
| 203 |
+
"agent_name": self.agent_name,
|
| 204 |
+
"role": self.role,
|
| 205 |
+
"provider": "OpenRouter"
|
| 206 |
+
},
|
| 207 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
if track_costs:
|
| 211 |
+
logger.info(
|
| 212 |
+
f"💰 Cost Efficiency - {self.agent_name}: "
|
| 213 |
+
f"{response_time:.2f}s, {total_tokens} tokens, "
|
| 214 |
+
f"${estimated_cost:.6f} ({self.model_name})"
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
return result
|
| 218 |
+
|
| 219 |
+
elif response.status == 429:
|
| 220 |
+
# Rate limit - try cheaper model
|
| 221 |
+
logger.warning(f"⚠️ Rate limit hit for {self.model_name}, switching to free model")
|
| 222 |
+
return await self._fallback_to_free_model(prompt, track_costs)
|
| 223 |
+
|
| 224 |
+
else:
|
| 225 |
+
error_text = await response.text()
|
| 226 |
+
error_result = {
|
| 227 |
+
"success": False,
|
| 228 |
+
"error": f"HTTP {response.status}: {error_text}",
|
| 229 |
+
"response_time": round(response_time, 3),
|
| 230 |
+
"model": self.model_name,
|
| 231 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 232 |
+
}
|
| 233 |
+
return error_result
|
| 234 |
+
|
| 235 |
+
except asyncio.TimeoutError:
|
| 236 |
+
error_result = {
|
| 237 |
+
"success": False,
|
| 238 |
+
"error": "Request timeout (45s)",
|
| 239 |
+
"response_time": 45.0,
|
| 240 |
+
"model": self.model_name,
|
| 241 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 242 |
+
}
|
| 243 |
+
logger.error(f"⏰ Timeout for {self.agent_name}")
|
| 244 |
+
return error_result
|
| 245 |
+
|
| 246 |
+
except Exception as e:
|
| 247 |
+
error_result = {
|
| 248 |
+
"success": False,
|
| 249 |
+
"error": f"Request failed: {str(e)}",
|
| 250 |
+
"response_time": round(time.time() - start_time, 3),
|
| 251 |
+
"model": self.model_name,
|
| 252 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 253 |
+
}
|
| 254 |
+
logger.error(f"❌ OpenRouter Error for {self.agent_name}: {e}")
|
| 255 |
+
return error_result
|
| 256 |
+
|
| 257 |
+
async def _fallback_to_free_model(self, prompt: str, track_costs: bool) -> Dict[str, Any]:
|
| 258 |
+
"""Fallback to free model when rate limited"""
|
| 259 |
+
original_model = self.model_name
|
| 260 |
+
self.model_name = "meta-llama/llama-3.2-3b-instruct:free"
|
| 261 |
+
|
| 262 |
+
logger.info(f"🔄 Fallback: {original_model} → {self.model_name}")
|
| 263 |
+
|
| 264 |
+
result = await self.send_request(prompt, track_costs)
|
| 265 |
+
|
| 266 |
+
# Restore original model for next request
|
| 267 |
+
self.model_name = original_model
|
| 268 |
+
|
| 269 |
+
if result["success"]:
|
| 270 |
+
result["cost_metrics"]["fallback_used"] = True
|
| 271 |
+
result["cost_metrics"]["original_model"] = original_model
|
| 272 |
+
|
| 273 |
+
return result
|
| 274 |
+
|
| 275 |
+
async def health_check(self) -> Dict[str, Any]:
|
| 276 |
+
"""Health check with cost efficiency metrics"""
|
| 277 |
+
try:
|
| 278 |
+
test_prompt = "Reply with just 'OK' to confirm SAAP agent connectivity."
|
| 279 |
+
result = await self.send_request(test_prompt, track_costs=False)
|
| 280 |
+
|
| 281 |
+
return {
|
| 282 |
+
"agent_name": self.agent_name,
|
| 283 |
+
"role": self.role,
|
| 284 |
+
"provider": "OpenRouter",
|
| 285 |
+
"model": self.model_name,
|
| 286 |
+
"status": "healthy" if result["success"] else "unhealthy",
|
| 287 |
+
"response_time": result.get("performance_metrics", {}).get("response_time", 0),
|
| 288 |
+
"cost_per_1m_tokens": self.model_costs.get(self.model_name, 0),
|
| 289 |
+
"is_free_model": self.model_costs.get(self.model_name, 0) == 0,
|
| 290 |
+
"error": result.get("error") if not result["success"] else None,
|
| 291 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
return {
|
| 296 |
+
"agent_name": self.agent_name,
|
| 297 |
+
"role": self.role,
|
| 298 |
+
"provider": "OpenRouter",
|
| 299 |
+
"status": "error",
|
| 300 |
+
"error": str(e),
|
| 301 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
# Utility functions for SAAP integration
|
| 305 |
+
def create_agent_by_role(role: str, agent_name: str, api_key: str) -> EnhancedOpenRouterAgent:
|
| 306 |
+
"""Create optimized OpenRouter agent by role"""
|
| 307 |
+
return EnhancedOpenRouterAgent(agent_name, role, api_key)
|
| 308 |
+
|
| 309 |
+
def get_cost_efficient_model_for_role(role: str) -> Dict[str, Any]:
|
| 310 |
+
"""Get the most cost-efficient model recommendation for a role"""
|
| 311 |
+
mapping = EnhancedOpenRouterAgent("temp", role, "temp").role_model_mapping
|
| 312 |
+
return mapping.get(role, mapping["Fallback"])
|
| 313 |
+
|
| 314 |
+
if __name__ == "__main__":
|
| 315 |
+
async def demo_cost_efficient_agents():
|
| 316 |
+
"""Demo cost-efficient agents with tracking"""
|
| 317 |
+
print("💰 OpenRouter Cost-Efficient Models Demo")
|
| 318 |
+
print("=" * 50)
|
| 319 |
+
|
| 320 |
+
# Load API key from environment variable
|
| 321 |
+
API_KEY = os.getenv("OPENROUTER_API_KEY")
|
| 322 |
+
|
| 323 |
+
if not API_KEY:
|
| 324 |
+
print("❌ Error: OPENROUTER_API_KEY not set in environment variables")
|
| 325 |
+
print("Please set it in backend/.env file:")
|
| 326 |
+
print("OPENROUTER_API_KEY=sk-or-v1-your-actual-key-here")
|
| 327 |
+
return
|
| 328 |
+
|
| 329 |
+
# Create agents for different roles
|
| 330 |
+
agents = [
|
| 331 |
+
EnhancedOpenRouterAgent("jane_alesi", "Coordinator", API_KEY),
|
| 332 |
+
EnhancedOpenRouterAgent("john_alesi", "Developer", API_KEY),
|
| 333 |
+
EnhancedOpenRouterAgent("lara_alesi", "Medical", API_KEY),
|
| 334 |
+
EnhancedOpenRouterAgent("analyst_agent", "Analyst", API_KEY)
|
| 335 |
+
]
|
| 336 |
+
|
| 337 |
+
test_prompt = "Erkläre in 2 Sätzen die Hauptvorteile deiner Rolle in einem Multi-Agent-System."
|
| 338 |
+
|
| 339 |
+
total_cost = 0
|
| 340 |
+
total_time = 0
|
| 341 |
+
|
| 342 |
+
for agent in agents:
|
| 343 |
+
print(f"\n🤖 Testing {agent.agent_name} ({agent.role})...")
|
| 344 |
+
print(f" Model: {agent.model_name}")
|
| 345 |
+
|
| 346 |
+
result = await agent.send_request(test_prompt)
|
| 347 |
+
|
| 348 |
+
if result["success"]:
|
| 349 |
+
metrics = result["performance_metrics"]
|
| 350 |
+
cost = result["cost_metrics"]["estimated_cost_usd"]
|
| 351 |
+
|
| 352 |
+
print(f" ✅ Response: {result['response'][:80]}...")
|
| 353 |
+
print(f" ⏱️ Time: {metrics['response_time']}s")
|
| 354 |
+
print(f" 💰 Cost: ${cost:.6f}")
|
| 355 |
+
print(f" 🔥 Speed: {metrics['tokens_per_second']:.1f} tokens/s")
|
| 356 |
+
|
| 357 |
+
total_cost += cost
|
| 358 |
+
total_time += metrics['response_time']
|
| 359 |
+
else:
|
| 360 |
+
print(f" ❌ Error: {result['error']}")
|
| 361 |
+
|
| 362 |
+
print(f"\n📊 Total Performance:")
|
| 363 |
+
print(f" 💰 Total Cost: ${total_cost:.6f}")
|
| 364 |
+
print(f" ⏱️ Total Time: {total_time:.2f}s")
|
| 365 |
+
print(f" 💡 Average Cost per Agent: ${total_cost/len(agents):.6f}")
|
| 366 |
+
|
| 367 |
+
asyncio.run(demo_cost_efficient_agents())
|
backend/agents/openrouter_saap_agent.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
SAAP OpenRouter Integration Agent (FREE Fallback)
|
| 4 |
+
OpenRouter GLM 4.5 Air (kostenlos) als Fallback für colossus Server
|
| 5 |
+
Author: Hanan Wandji Danga
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
import requests
|
| 10 |
+
import json
|
| 11 |
+
import time
|
| 12 |
+
import asyncio
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Dict, List, Optional, Any
|
| 15 |
+
|
| 16 |
+
# Load environment variables
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
class OpenRouterSAAPAgent:
|
| 20 |
+
def __init__(self, agent_name: str, role: str, api_key: Optional[str] = None):
|
| 21 |
+
"""
|
| 22 |
+
OpenRouter SAAP Agent für GLM 4.5 Air (FREE) als Fallback
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
agent_name: Name des Agents
|
| 26 |
+
role: Rolle des Agents
|
| 27 |
+
api_key: OpenRouter API Key (optional für FREE models)
|
| 28 |
+
"""
|
| 29 |
+
self.agent_name = agent_name
|
| 30 |
+
self.role = role
|
| 31 |
+
self.api_key = api_key
|
| 32 |
+
self.base_url = "https://openrouter.ai/api/v1"
|
| 33 |
+
self.model_name = "deepseek/deepseek-chat" # FREE model
|
| 34 |
+
|
| 35 |
+
# Fallback models (alle kostenlos)
|
| 36 |
+
self.free_models = [
|
| 37 |
+
"deepseek/deepseek-chat",
|
| 38 |
+
"microsoft/phi-3-medium-4k-instruct:free",
|
| 39 |
+
"microsoft/phi-3-mini-128k-instruct:free",
|
| 40 |
+
"huggingface/zephyr-7b-beta:free",
|
| 41 |
+
"openchat/openchat-7b:free"
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
# Agent Context
|
| 45 |
+
self.context = self._initialize_context()
|
| 46 |
+
|
| 47 |
+
# Logging
|
| 48 |
+
logging.basicConfig(level=logging.INFO)
|
| 49 |
+
self.logger = logging.getLogger(f"OpenRouterSAAP.{agent_name}")
|
| 50 |
+
|
| 51 |
+
self.logger.info(f"🌐 {agent_name} ({role}) initialized with OpenRouter FREE")
|
| 52 |
+
|
| 53 |
+
def _initialize_context(self) -> str:
|
| 54 |
+
"""Rollenspezifischer Kontext"""
|
| 55 |
+
contexts = {
|
| 56 |
+
"Analyst": """Du bist ein SAAP Analyst Agent. Du spezialisierst dich auf:
|
| 57 |
+
- Datenanalyse und Requirements Engineering
|
| 58 |
+
- Performance-Bewertung von Multi-Agent-Systemen
|
| 59 |
+
- Use Case Definition und Dokumentation
|
| 60 |
+
- Optimierungspotentiale identifizieren""",
|
| 61 |
+
|
| 62 |
+
"Fallback": """Du bist ein SAAP Fallback Agent. Du hilfst bei:
|
| 63 |
+
- Backup-Processing wenn primäre Agenten nicht verfügbar sind
|
| 64 |
+
- Allgemeine Anfragen beantworten
|
| 65 |
+
- System-Stabilität durch Redundanz gewährleisten
|
| 66 |
+
- Kontinuität der SAAP-Services sicherstellen"""
|
| 67 |
+
}
|
| 68 |
+
return contexts.get(self.role, contexts["Fallback"])
|
| 69 |
+
|
| 70 |
+
async def send_request_to_openrouter(self, prompt: str, max_tokens: int = 400) -> Dict[str, Any]:
|
| 71 |
+
"""
|
| 72 |
+
Sendet Request an OpenRouter API (FREE models)
|
| 73 |
+
"""
|
| 74 |
+
start_time = time.time()
|
| 75 |
+
|
| 76 |
+
headers = {
|
| 77 |
+
"Content-Type": "application/json",
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
# API Key nur wenn vorhanden (für FREE models optional)
|
| 81 |
+
if self.api_key:
|
| 82 |
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
| 83 |
+
|
| 84 |
+
payload = {
|
| 85 |
+
"model": self.model_name,
|
| 86 |
+
"messages": [
|
| 87 |
+
{"role": "system", "content": self.context},
|
| 88 |
+
{"role": "user", "content": prompt}
|
| 89 |
+
],
|
| 90 |
+
"max_tokens": max_tokens,
|
| 91 |
+
"temperature": 0.7
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
response = requests.post(
|
| 96 |
+
f"{self.base_url}/chat/completions",
|
| 97 |
+
headers=headers,
|
| 98 |
+
json=payload,
|
| 99 |
+
timeout=30
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
response_time = time.time() - start_time
|
| 103 |
+
|
| 104 |
+
if response.status_code == 200:
|
| 105 |
+
data = response.json()
|
| 106 |
+
|
| 107 |
+
response_text = data['choices'][0]['message']['content']
|
| 108 |
+
token_count = data.get('usage', {}).get('total_tokens', 0)
|
| 109 |
+
|
| 110 |
+
result = {
|
| 111 |
+
"success": True,
|
| 112 |
+
"response": response_text,
|
| 113 |
+
"response_time": round(response_time, 2),
|
| 114 |
+
"token_count": token_count,
|
| 115 |
+
"model": self.model_name,
|
| 116 |
+
"agent_role": self.role,
|
| 117 |
+
"provider": "OpenRouter FREE",
|
| 118 |
+
"timestamp": time.time()
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
self.logger.info(f"✅ OpenRouter Response: {response_time:.2f}s, FREE model")
|
| 122 |
+
return result
|
| 123 |
+
|
| 124 |
+
else:
|
| 125 |
+
# Try fallback models if primary fails
|
| 126 |
+
if response.status_code == 429 or response.status_code == 402: # Rate limit or payment
|
| 127 |
+
return await self._try_fallback_models(prompt, max_tokens)
|
| 128 |
+
|
| 129 |
+
error_result = {
|
| 130 |
+
"success": False,
|
| 131 |
+
"error": f"HTTP {response.status_code}: {response.text}",
|
| 132 |
+
"response_time": round(response_time, 2),
|
| 133 |
+
"timestamp": time.time()
|
| 134 |
+
}
|
| 135 |
+
return error_result
|
| 136 |
+
|
| 137 |
+
except requests.exceptions.RequestException as e:
|
| 138 |
+
error_result = {
|
| 139 |
+
"success": False,
|
| 140 |
+
"error": f"OpenRouter request failed: {str(e)}",
|
| 141 |
+
"response_time": round(time.time() - start_time, 2),
|
| 142 |
+
"timestamp": time.time()
|
| 143 |
+
}
|
| 144 |
+
self.logger.error(f"❌ OpenRouter Error: {e}")
|
| 145 |
+
return error_result
|
| 146 |
+
|
| 147 |
+
async def _try_fallback_models(self, prompt: str, max_tokens: int) -> Dict[str, Any]:
|
| 148 |
+
"""
|
| 149 |
+
Versucht verschiedene FREE models als Fallback
|
| 150 |
+
"""
|
| 151 |
+
self.logger.info("🔄 Trying fallback models...")
|
| 152 |
+
|
| 153 |
+
for model in self.free_models[1:]: # Skip first (already tried)
|
| 154 |
+
self.logger.info(f"🔄 Trying {model}...")
|
| 155 |
+
|
| 156 |
+
old_model = self.model_name
|
| 157 |
+
self.model_name = model
|
| 158 |
+
|
| 159 |
+
result = await self.send_request_to_openrouter(prompt, max_tokens)
|
| 160 |
+
|
| 161 |
+
if result["success"]:
|
| 162 |
+
self.logger.info(f"✅ Fallback successful with {model}")
|
| 163 |
+
return result
|
| 164 |
+
|
| 165 |
+
# Restore original model for next attempt
|
| 166 |
+
self.model_name = old_model
|
| 167 |
+
await asyncio.sleep(1) # Rate limiting
|
| 168 |
+
|
| 169 |
+
return {
|
| 170 |
+
"success": False,
|
| 171 |
+
"error": "All fallback models failed",
|
| 172 |
+
"timestamp": time.time()
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
async def health_check(self) -> Dict[str, Any]:
|
| 176 |
+
"""
|
| 177 |
+
Health check für OpenRouter FREE services
|
| 178 |
+
"""
|
| 179 |
+
try:
|
| 180 |
+
test_prompt = "Hello, this is a connection test for SAAP."
|
| 181 |
+
result = await self.send_request_to_openrouter(test_prompt, max_tokens=20)
|
| 182 |
+
|
| 183 |
+
return {
|
| 184 |
+
"agent_name": self.agent_name,
|
| 185 |
+
"provider": "OpenRouter",
|
| 186 |
+
"status": "healthy" if result["success"] else "unhealthy",
|
| 187 |
+
"model": self.model_name,
|
| 188 |
+
"response_time": result.get("response_time", 0),
|
| 189 |
+
"error": result.get("error") if not result["success"] else None,
|
| 190 |
+
"timestamp": time.time()
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
return {
|
| 195 |
+
"agent_name": self.agent_name,
|
| 196 |
+
"provider": "OpenRouter",
|
| 197 |
+
"status": "error",
|
| 198 |
+
"error": str(e),
|
| 199 |
+
"timestamp": time.time()
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
# Utility function für SAAP Fallback-System
|
| 203 |
+
def create_openrouter_fallback_agent(api_key: Optional[str] = None) -> OpenRouterSAAPAgent:
|
| 204 |
+
"""
|
| 205 |
+
Erstellt OpenRouter Fallback Agent für SAAP System
|
| 206 |
+
"""
|
| 207 |
+
return OpenRouterSAAPAgent("fallback_analyst", "Analyst", api_key)
|
| 208 |
+
|
| 209 |
+
class SAAPHybridSystem:
|
| 210 |
+
"""
|
| 211 |
+
Hybrid System: colossus (Primary) + OpenRouter (Fallback)
|
| 212 |
+
"""
|
| 213 |
+
def __init__(self, colossus_api_key: str, openrouter_api_key: Optional[str] = None):
|
| 214 |
+
from colossus_saap_agent import ColossusSAAPAgent
|
| 215 |
+
|
| 216 |
+
self.primary_agent = ColossusSAAPAgent("hybrid_primary", "Coordinator", colossus_api_key)
|
| 217 |
+
self.fallback_agent = OpenRouterSAAPAgent("hybrid_fallback", "Fallback", openrouter_api_key)
|
| 218 |
+
|
| 219 |
+
self.logger = logging.getLogger("SAAPHybrid")
|
| 220 |
+
|
| 221 |
+
async def process_with_fallback(self, prompt: str, max_tokens: int = 400) -> Dict[str, Any]:
|
| 222 |
+
"""
|
| 223 |
+
Versucht zuerst colossus, dann OpenRouter als Fallback
|
| 224 |
+
"""
|
| 225 |
+
self.logger.info("🚀 Processing with hybrid primary/fallback system")
|
| 226 |
+
|
| 227 |
+
# Try primary (colossus) first
|
| 228 |
+
try:
|
| 229 |
+
primary_result = await self.primary_agent.send_request_to_colossus(prompt, max_tokens)
|
| 230 |
+
|
| 231 |
+
if primary_result["success"]:
|
| 232 |
+
primary_result["provider"] = "colossus (Primary)"
|
| 233 |
+
self.logger.info("✅ Primary (colossus) successful")
|
| 234 |
+
return primary_result
|
| 235 |
+
else:
|
| 236 |
+
self.logger.warning(f"⚠️ Primary failed: {primary_result.get('error')}")
|
| 237 |
+
|
| 238 |
+
except Exception as e:
|
| 239 |
+
self.logger.error(f"❌ Primary system error: {e}")
|
| 240 |
+
|
| 241 |
+
# Fallback to OpenRouter
|
| 242 |
+
self.logger.info("🔄 Switching to OpenRouter fallback...")
|
| 243 |
+
fallback_result = await self.fallback_agent.send_request_to_openrouter(prompt, max_tokens)
|
| 244 |
+
|
| 245 |
+
if fallback_result["success"]:
|
| 246 |
+
fallback_result["fallback_used"] = True
|
| 247 |
+
fallback_result["provider"] = "OpenRouter (Fallback)"
|
| 248 |
+
self.logger.info("✅ Fallback (OpenRouter) successful")
|
| 249 |
+
else:
|
| 250 |
+
self.logger.error("❌ Both primary and fallback failed")
|
| 251 |
+
|
| 252 |
+
return fallback_result
|
| 253 |
+
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
# Demo OpenRouter Integration
|
| 256 |
+
async def demo_openrouter():
|
| 257 |
+
print("🌐 OpenRouter FREE Model Demo")
|
| 258 |
+
|
| 259 |
+
# Test OpenRouter agent
|
| 260 |
+
agent = OpenRouterSAAPAgent("demo_fallback", "Analyst")
|
| 261 |
+
|
| 262 |
+
# Health check
|
| 263 |
+
health = await agent.health_check()
|
| 264 |
+
print(f"Health: {health}")
|
| 265 |
+
|
| 266 |
+
if health["status"] == "healthy":
|
| 267 |
+
# Test query
|
| 268 |
+
result = await agent.send_request_to_openrouter(
|
| 269 |
+
"Erkläre die Vorteile eines Hybrid-AI-Systems mit Primary und Fallback."
|
| 270 |
+
)
|
| 271 |
+
print(f"Response: {result.get('response', 'No response')}")
|
| 272 |
+
print(f"Time: {result.get('response_time')}s")
|
| 273 |
+
print(f"Model: {result.get('model')}")
|
| 274 |
+
|
| 275 |
+
# Demo Hybrid System
|
| 276 |
+
async def demo_hybrid():
|
| 277 |
+
print("\n🔄 Hybrid System Demo (colossus + OpenRouter)")
|
| 278 |
+
|
| 279 |
+
COLOSSUS_KEY = os.getenv("COLOSSUS_API_KEY")
|
| 280 |
+
|
| 281 |
+
if not COLOSSUS_KEY:
|
| 282 |
+
print("❌ Error: COLOSSUS_API_KEY not set in environment variables")
|
| 283 |
+
print("Please set it in backend/.env file")
|
| 284 |
+
return
|
| 285 |
+
|
| 286 |
+
hybrid = SAAPHybridSystem(COLOSSUS_KEY)
|
| 287 |
+
|
| 288 |
+
result = await hybrid.process_with_fallback(
|
| 289 |
+
"Was sind die Hauptvorteile von Multi-Agent-Systemen?"
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
print(f"Provider: {result.get('provider', 'Unknown')}")
|
| 293 |
+
print(f"Response: {result.get('response', 'No response')[:100]}...")
|
| 294 |
+
print(f"Fallback used: {result.get('fallback_used', False)}")
|
| 295 |
+
|
| 296 |
+
# Run demos
|
| 297 |
+
async def run_demos():
|
| 298 |
+
await demo_openrouter()
|
| 299 |
+
await demo_hybrid()
|
| 300 |
+
|
| 301 |
+
asyncio.run(run_demos())
|
backend/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
backend/api/agent_api.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP Agent Management API
|
| 3 |
+
FastAPI endpoints for modular agent management
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from typing import List, Dict, Any, Optional
|
| 9 |
+
import asyncio
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
import uvicorn
|
| 12 |
+
|
| 13 |
+
# Import SAAP models
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
| 17 |
+
|
| 18 |
+
from models.agent import SaapAgent, AgentTemplates, AgentStatus, AgentType
|
| 19 |
+
from api.colossus_client import ColossusClient
|
| 20 |
+
|
| 21 |
+
# FastAPI App
|
| 22 |
+
app = FastAPI(
|
| 23 |
+
title="SAAP Agent Management API",
|
| 24 |
+
description="Modular AI Agent Management for SAAP Platform",
|
| 25 |
+
version="1.0.0"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# CORS for Vue.js frontend
|
| 29 |
+
app.add_middleware(
|
| 30 |
+
CORSMiddleware,
|
| 31 |
+
allow_origins=["http://localhost:5173", "http://localhost:8080", "*"], # Vue.js dev server
|
| 32 |
+
allow_credentials=True,
|
| 33 |
+
allow_methods=["*"],
|
| 34 |
+
allow_headers=["*"],
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# In-memory agent storage (later: replace with database)
|
| 38 |
+
agents_db: Dict[str, SaapAgent] = {}
|
| 39 |
+
|
| 40 |
+
@app.on_event("startup")
|
| 41 |
+
async def startup_event():
|
| 42 |
+
"""Initialize SAAP with default agents"""
|
| 43 |
+
print("🚀 Initializing SAAP Agent Management API...")
|
| 44 |
+
|
| 45 |
+
# Load default Alesi agents
|
| 46 |
+
default_agents = [
|
| 47 |
+
AgentTemplates.jane_alesi(),
|
| 48 |
+
AgentTemplates.john_alesi(),
|
| 49 |
+
AgentTemplates.lara_alesi()
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
for agent in default_agents:
|
| 53 |
+
agents_db[agent.id] = agent
|
| 54 |
+
print(f"✅ Loaded: {agent.name} ({agent.type.value})")
|
| 55 |
+
|
| 56 |
+
print(f"🎉 SAAP initialized with {len(agents_db)} agents")
|
| 57 |
+
|
| 58 |
+
# Agent Management Endpoints
|
| 59 |
+
@app.get("/")
|
| 60 |
+
async def root():
|
| 61 |
+
"""API root with status info"""
|
| 62 |
+
return {
|
| 63 |
+
"message": "SAAP Agent Management API",
|
| 64 |
+
"version": "1.0.0",
|
| 65 |
+
"agents_count": len(agents_db),
|
| 66 |
+
"status": "operational"
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
@app.get("/agents", response_model=List[Dict[str, Any]])
|
| 70 |
+
async def list_agents():
|
| 71 |
+
"""List all registered agents"""
|
| 72 |
+
return [agent.to_dict() for agent in agents_db.values()]
|
| 73 |
+
|
| 74 |
+
@app.get("/agents/{agent_id}")
|
| 75 |
+
async def get_agent(agent_id: str):
|
| 76 |
+
"""Get specific agent details"""
|
| 77 |
+
if agent_id not in agents_db:
|
| 78 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 79 |
+
|
| 80 |
+
agent = agents_db[agent_id]
|
| 81 |
+
return {
|
| 82 |
+
"agent": agent.to_dict(),
|
| 83 |
+
"is_active": agent.is_active(),
|
| 84 |
+
"capabilities_display": agent.get_capabilities_display()
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
@app.post("/agents")
|
| 88 |
+
async def create_agent(agent_data: Dict[str, Any]):
|
| 89 |
+
"""Create new agent from JSON data"""
|
| 90 |
+
try:
|
| 91 |
+
# Validate and create agent
|
| 92 |
+
agent = SaapAgent.from_dict(agent_data)
|
| 93 |
+
|
| 94 |
+
# Check if agent already exists
|
| 95 |
+
if agent.id in agents_db:
|
| 96 |
+
raise HTTPException(status_code=400, detail=f"Agent '{agent.id}' already exists")
|
| 97 |
+
|
| 98 |
+
# Store agent
|
| 99 |
+
agents_db[agent.id] = agent
|
| 100 |
+
|
| 101 |
+
return {
|
| 102 |
+
"message": f"Agent '{agent.name}' created successfully",
|
| 103 |
+
"agent": agent.to_dict()
|
| 104 |
+
}
|
| 105 |
+
except Exception as e:
|
| 106 |
+
raise HTTPException(status_code=400, detail=f"Invalid agent data: {str(e)}")
|
| 107 |
+
|
| 108 |
+
@app.put("/agents/{agent_id}")
|
| 109 |
+
async def update_agent(agent_id: str, agent_data: Dict[str, Any]):
|
| 110 |
+
"""Update existing agent"""
|
| 111 |
+
if agent_id not in agents_db:
|
| 112 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
# Create updated agent
|
| 116 |
+
updated_agent = SaapAgent.from_dict(agent_data)
|
| 117 |
+
updated_agent.updated_at = datetime.utcnow()
|
| 118 |
+
|
| 119 |
+
# Store updated agent
|
| 120 |
+
agents_db[agent_id] = updated_agent
|
| 121 |
+
|
| 122 |
+
return {
|
| 123 |
+
"message": f"Agent '{agent_id}' updated successfully",
|
| 124 |
+
"agent": updated_agent.to_dict()
|
| 125 |
+
}
|
| 126 |
+
except Exception as e:
|
| 127 |
+
raise HTTPException(status_code=400, detail=f"Invalid agent data: {str(e)}")
|
| 128 |
+
|
| 129 |
+
@app.delete("/agents/{agent_id}")
|
| 130 |
+
async def delete_agent(agent_id: str):
|
| 131 |
+
"""Delete agent"""
|
| 132 |
+
if agent_id not in agents_db:
|
| 133 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 134 |
+
|
| 135 |
+
# Stop agent if running
|
| 136 |
+
agent = agents_db[agent_id]
|
| 137 |
+
if agent.is_active():
|
| 138 |
+
agent.update_status(AgentStatus.INACTIVE)
|
| 139 |
+
|
| 140 |
+
# Remove from database
|
| 141 |
+
del agents_db[agent_id]
|
| 142 |
+
|
| 143 |
+
return {"message": f"Agent '{agent_id}' deleted successfully"}
|
| 144 |
+
|
| 145 |
+
# Agent Control Endpoints
|
| 146 |
+
@app.post("/agents/{agent_id}/start")
|
| 147 |
+
async def start_agent(agent_id: str, background_tasks: BackgroundTasks):
|
| 148 |
+
"""Start agent (set status to active)"""
|
| 149 |
+
if agent_id not in agents_db:
|
| 150 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 151 |
+
|
| 152 |
+
agent = agents_db[agent_id]
|
| 153 |
+
|
| 154 |
+
if agent.is_active():
|
| 155 |
+
raise HTTPException(status_code=400, detail=f"Agent '{agent_id}' is already active")
|
| 156 |
+
|
| 157 |
+
# Update status to starting
|
| 158 |
+
agent.update_status(AgentStatus.STARTING)
|
| 159 |
+
|
| 160 |
+
# Background task to set to active (simulate startup time)
|
| 161 |
+
def activate_agent():
|
| 162 |
+
asyncio.run(finalize_agent_start(agent_id))
|
| 163 |
+
|
| 164 |
+
background_tasks.add_task(activate_agent)
|
| 165 |
+
|
| 166 |
+
return {
|
| 167 |
+
"message": f"Agent '{agent.name}' is starting...",
|
| 168 |
+
"agent_id": agent_id,
|
| 169 |
+
"status": agent.status.value
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
async def finalize_agent_start(agent_id: str):
|
| 173 |
+
"""Finalize agent startup"""
|
| 174 |
+
await asyncio.sleep(2) # Simulate startup time
|
| 175 |
+
|
| 176 |
+
if agent_id in agents_db:
|
| 177 |
+
agent = agents_db[agent_id]
|
| 178 |
+
agent.update_status(AgentStatus.ACTIVE)
|
| 179 |
+
agent.update_metrics(messages_processed=0, average_response_time=0.0)
|
| 180 |
+
print(f"✅ Agent '{agent.name}' activated")
|
| 181 |
+
|
| 182 |
+
@app.post("/agents/{agent_id}/stop")
|
| 183 |
+
async def stop_agent(agent_id: str):
|
| 184 |
+
"""Stop agent (set status to inactive)"""
|
| 185 |
+
if agent_id not in agents_db:
|
| 186 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 187 |
+
|
| 188 |
+
agent = agents_db[agent_id]
|
| 189 |
+
|
| 190 |
+
if not agent.is_active():
|
| 191 |
+
raise HTTPException(status_code=400, detail=f"Agent '{agent_id}' is not active")
|
| 192 |
+
|
| 193 |
+
# Update status
|
| 194 |
+
agent.update_status(AgentStatus.INACTIVE)
|
| 195 |
+
|
| 196 |
+
return {
|
| 197 |
+
"message": f"Agent '{agent.name}' stopped",
|
| 198 |
+
"agent_id": agent_id,
|
| 199 |
+
"status": agent.status.value
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
# Agent Communication Endpoints
|
| 203 |
+
@app.post("/agents/{agent_id}/chat")
|
| 204 |
+
async def chat_with_agent(agent_id: str, message_data: Dict[str, Any]):
|
| 205 |
+
"""Send message to agent and get response"""
|
| 206 |
+
if agent_id not in agents_db:
|
| 207 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 208 |
+
|
| 209 |
+
agent = agents_db[agent_id]
|
| 210 |
+
|
| 211 |
+
if not agent.is_active():
|
| 212 |
+
raise HTTPException(status_code=400, detail=f"Agent '{agent_id}' is not active")
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
user_message = message_data.get("message", "")
|
| 216 |
+
if not user_message:
|
| 217 |
+
raise HTTPException(status_code=400, detail="Message content required")
|
| 218 |
+
|
| 219 |
+
# Prepare messages for colossus
|
| 220 |
+
messages = []
|
| 221 |
+
|
| 222 |
+
# Add system prompt if available
|
| 223 |
+
if agent.personality and agent.personality.system_prompt:
|
| 224 |
+
messages.append({
|
| 225 |
+
"role": "system",
|
| 226 |
+
"content": agent.personality.system_prompt
|
| 227 |
+
})
|
| 228 |
+
|
| 229 |
+
# Add user message
|
| 230 |
+
messages.append({
|
| 231 |
+
"role": "user",
|
| 232 |
+
"content": user_message
|
| 233 |
+
})
|
| 234 |
+
|
| 235 |
+
# Send to colossus
|
| 236 |
+
async with ColossusClient() as client:
|
| 237 |
+
result = await client.chat_completion(
|
| 238 |
+
messages=messages,
|
| 239 |
+
agent_id=agent_id,
|
| 240 |
+
temperature=agent.llm_config.temperature,
|
| 241 |
+
max_tokens=agent.llm_config.max_tokens
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
if result["success"]:
|
| 245 |
+
response_text = result["response"]["choices"][0]["message"]["content"]
|
| 246 |
+
response_time = result["response_time"]
|
| 247 |
+
|
| 248 |
+
# Update agent metrics
|
| 249 |
+
agent.update_metrics(
|
| 250 |
+
messages_processed=agent.metrics.messages_processed + 1 if agent.metrics else 1,
|
| 251 |
+
average_response_time=response_time
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
return {
|
| 255 |
+
"agent_id": agent_id,
|
| 256 |
+
"agent_name": agent.name,
|
| 257 |
+
"user_message": user_message,
|
| 258 |
+
"agent_response": response_text,
|
| 259 |
+
"response_time": response_time,
|
| 260 |
+
"model": agent.llm_config.model
|
| 261 |
+
}
|
| 262 |
+
else:
|
| 263 |
+
raise HTTPException(status_code=500, detail=f"Agent communication failed: {result['error']}")
|
| 264 |
+
|
| 265 |
+
except Exception as e:
|
| 266 |
+
raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}")
|
| 267 |
+
|
| 268 |
+
# System Status Endpoints
|
| 269 |
+
@app.get("/system/status")
|
| 270 |
+
async def system_status():
|
| 271 |
+
"""Get system status and metrics"""
|
| 272 |
+
active_agents = [agent for agent in agents_db.values() if agent.is_active()]
|
| 273 |
+
inactive_agents = [agent for agent in agents_db.values() if not agent.is_active()]
|
| 274 |
+
|
| 275 |
+
return {
|
| 276 |
+
"system": "SAAP Agent Management",
|
| 277 |
+
"status": "operational",
|
| 278 |
+
"agents": {
|
| 279 |
+
"total": len(agents_db),
|
| 280 |
+
"active": len(active_agents),
|
| 281 |
+
"inactive": len(inactive_agents)
|
| 282 |
+
},
|
| 283 |
+
"active_agents": [{"id": agent.id, "name": agent.name, "type": agent.type.value}
|
| 284 |
+
for agent in active_agents],
|
| 285 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
# Template Endpoints
|
| 289 |
+
@app.get("/templates")
|
| 290 |
+
async def list_agent_templates():
|
| 291 |
+
"""List available agent templates"""
|
| 292 |
+
templates = [
|
| 293 |
+
{
|
| 294 |
+
"id": "jane_alesi",
|
| 295 |
+
"name": "Jane Alesi",
|
| 296 |
+
"type": "coordinator",
|
| 297 |
+
"description": "Lead AI Architect Template"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"id": "john_alesi",
|
| 301 |
+
"name": "John Alesi",
|
| 302 |
+
"type": "developer",
|
| 303 |
+
"description": "Software Developer Template"
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"id": "lara_alesi",
|
| 307 |
+
"name": "Lara Alesi",
|
| 308 |
+
"type": "specialist",
|
| 309 |
+
"description": "Medical Specialist Template"
|
| 310 |
+
}
|
| 311 |
+
]
|
| 312 |
+
|
| 313 |
+
return {
|
| 314 |
+
"templates": templates,
|
| 315 |
+
"count": len(templates)
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
@app.post("/templates/{template_id}/create")
|
| 319 |
+
async def create_agent_from_template(template_id: str, customization: Optional[Dict[str, Any]] = None):
|
| 320 |
+
"""Create agent from template with optional customization"""
|
| 321 |
+
|
| 322 |
+
template_functions = {
|
| 323 |
+
"jane_alesi": AgentTemplates.jane_alesi,
|
| 324 |
+
"john_alesi": AgentTemplates.john_alesi,
|
| 325 |
+
"lara_alesi": AgentTemplates.lara_alesi
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
if template_id not in template_functions:
|
| 329 |
+
raise HTTPException(status_code=404, detail=f"Template '{template_id}' not found")
|
| 330 |
+
|
| 331 |
+
# Create agent from template
|
| 332 |
+
agent = template_functions[template_id]()
|
| 333 |
+
|
| 334 |
+
# Apply customization if provided
|
| 335 |
+
if customization:
|
| 336 |
+
if "name" in customization:
|
| 337 |
+
agent.name = customization["name"]
|
| 338 |
+
if "color" in customization:
|
| 339 |
+
agent.color = customization["color"]
|
| 340 |
+
if "description" in customization:
|
| 341 |
+
agent.description = customization["description"]
|
| 342 |
+
|
| 343 |
+
# Generate unique ID if agent exists
|
| 344 |
+
original_id = agent.id
|
| 345 |
+
counter = 1
|
| 346 |
+
while agent.id in agents_db:
|
| 347 |
+
agent.id = f"{original_id}_{counter}"
|
| 348 |
+
counter += 1
|
| 349 |
+
|
| 350 |
+
# Store agent
|
| 351 |
+
agents_db[agent.id] = agent
|
| 352 |
+
|
| 353 |
+
return {
|
| 354 |
+
"message": f"Agent '{agent.name}' created from template",
|
| 355 |
+
"agent": agent.to_dict()
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
if __name__ == "__main__":
|
| 359 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
backend/api/agent_manager.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP Agent Manager - FastAPI Backend
|
| 3 |
+
Modular agent management with JSON Schema validation
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from pydantic import BaseModel, Field
|
| 9 |
+
from typing import Dict, List, Optional, Any
|
| 10 |
+
from datetime import datetime, timezone
|
| 11 |
+
import json
|
| 12 |
+
import asyncio
|
| 13 |
+
import logging
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
# Import agent templates and schema
|
| 17 |
+
import sys
|
| 18 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
| 19 |
+
|
| 20 |
+
app = FastAPI(
|
| 21 |
+
title="SAAP Agent Manager API",
|
| 22 |
+
description="Modular Multi-Agent Management System",
|
| 23 |
+
version="1.0.0"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# CORS middleware for Vue.js frontend
|
| 27 |
+
app.add_middleware(
|
| 28 |
+
CORSMiddleware,
|
| 29 |
+
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
| 30 |
+
allow_credentials=True,
|
| 31 |
+
allow_methods=["*"],
|
| 32 |
+
allow_headers=["*"],
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Logging setup
|
| 36 |
+
logging.basicConfig(level=logging.INFO)
|
| 37 |
+
logger = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
# Pydantic Models based on JSON Schema
|
| 40 |
+
class AgentModel(BaseModel):
|
| 41 |
+
provider: str = Field(..., description="Model provider")
|
| 42 |
+
name: str = Field(..., description="Model name")
|
| 43 |
+
endpoint: Optional[str] = Field(None, description="API endpoint")
|
| 44 |
+
api_key: Optional[str] = Field(None, description="API key")
|
| 45 |
+
|
| 46 |
+
class AgentPersonality(BaseModel):
|
| 47 |
+
system_prompt: str = Field(..., description="System prompt")
|
| 48 |
+
temperature: float = Field(0.7, ge=0.0, le=2.0)
|
| 49 |
+
max_tokens: int = Field(1000, ge=1, le=4096)
|
| 50 |
+
|
| 51 |
+
class AgentMetrics(BaseModel):
|
| 52 |
+
messages_processed: int = Field(0, ge=0)
|
| 53 |
+
average_response_time: float = Field(0.0, ge=0.0)
|
| 54 |
+
uptime: str = Field("0m")
|
| 55 |
+
error_count: int = Field(0, ge=0)
|
| 56 |
+
|
| 57 |
+
class Agent(BaseModel):
|
| 58 |
+
id: str = Field(..., regex="^[a-z_]+$")
|
| 59 |
+
name: str
|
| 60 |
+
type: str = Field(..., regex="^(coordinator|specialist|developer|analyst|utility)$")
|
| 61 |
+
color: str = Field(..., regex="^#[0-9A-Fa-f]{6}$")
|
| 62 |
+
avatar: Optional[str] = None
|
| 63 |
+
status: str = Field("inactive", regex="^(inactive|starting|active|stopping|error)$")
|
| 64 |
+
capabilities: List[str] = Field(default_factory=list)
|
| 65 |
+
model: AgentModel
|
| 66 |
+
personality: AgentPersonality
|
| 67 |
+
metrics: AgentMetrics = Field(default_factory=AgentMetrics)
|
| 68 |
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 69 |
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 70 |
+
|
| 71 |
+
class MessageRequest(BaseModel):
|
| 72 |
+
sender_id: str
|
| 73 |
+
receiver_id: str
|
| 74 |
+
content: str
|
| 75 |
+
message_type: str = "request"
|
| 76 |
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
| 77 |
+
|
| 78 |
+
class MessageResponse(BaseModel):
|
| 79 |
+
id: str
|
| 80 |
+
sender_id: str
|
| 81 |
+
receiver_id: str
|
| 82 |
+
content: str
|
| 83 |
+
response: Optional[str] = None
|
| 84 |
+
timestamp: datetime
|
| 85 |
+
status: str = "sent"
|
| 86 |
+
|
| 87 |
+
# Global agent storage (in production, use database)
|
| 88 |
+
agents: Dict[str, Agent] = {}
|
| 89 |
+
messages: List[MessageResponse] = []
|
| 90 |
+
websocket_connections: List[WebSocket] = []
|
| 91 |
+
|
| 92 |
+
# Load agent templates
|
| 93 |
+
def load_agent_templates():
|
| 94 |
+
"""Load agent templates from JSON file"""
|
| 95 |
+
try:
|
| 96 |
+
template_path = Path(__file__).parent.parent / "models" / "agent_templates.json"
|
| 97 |
+
with open(template_path, 'r', encoding='utf-8') as f:
|
| 98 |
+
data = json.load(f)
|
| 99 |
+
return data["templates"]
|
| 100 |
+
except Exception as e:
|
| 101 |
+
logger.error(f"Failed to load agent templates: {e}")
|
| 102 |
+
return {}
|
| 103 |
+
|
| 104 |
+
agent_templates = load_agent_templates()
|
| 105 |
+
|
| 106 |
+
# WebSocket connection manager
|
| 107 |
+
class ConnectionManager:
|
| 108 |
+
def __init__(self):
|
| 109 |
+
self.active_connections: List[WebSocket] = []
|
| 110 |
+
|
| 111 |
+
async def connect(self, websocket: WebSocket):
|
| 112 |
+
await websocket.accept()
|
| 113 |
+
self.active_connections.append(websocket)
|
| 114 |
+
logger.info(f"WebSocket connected. Total connections: {len(self.active_connections)}")
|
| 115 |
+
|
| 116 |
+
def disconnect(self, websocket: WebSocket):
|
| 117 |
+
if websocket in self.active_connections:
|
| 118 |
+
self.active_connections.remove(websocket)
|
| 119 |
+
logger.info(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
|
| 120 |
+
|
| 121 |
+
async def broadcast(self, data: dict):
|
| 122 |
+
"""Broadcast data to all connected clients"""
|
| 123 |
+
for connection in self.active_connections:
|
| 124 |
+
try:
|
| 125 |
+
await connection.send_json(data)
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.error(f"Error broadcasting to WebSocket: {e}")
|
| 128 |
+
|
| 129 |
+
manager = ConnectionManager()
|
| 130 |
+
|
| 131 |
+
# Agent Management Endpoints
|
| 132 |
+
@app.get("/api/v1/agents", response_model=List[Agent])
|
| 133 |
+
async def list_agents():
|
| 134 |
+
"""Get all registered agents"""
|
| 135 |
+
return list(agents.values())
|
| 136 |
+
|
| 137 |
+
@app.get("/api/v1/agents/templates")
|
| 138 |
+
async def get_agent_templates():
|
| 139 |
+
"""Get available agent templates"""
|
| 140 |
+
return agent_templates
|
| 141 |
+
|
| 142 |
+
@app.post("/api/v1/agents", response_model=Agent)
|
| 143 |
+
async def create_agent(agent_data: dict):
|
| 144 |
+
"""Create a new agent from template or custom data"""
|
| 145 |
+
try:
|
| 146 |
+
# If template_id provided, use template as base
|
| 147 |
+
if "template_id" in agent_data and agent_data["template_id"] in agent_templates:
|
| 148 |
+
template = agent_templates[agent_data["template_id"]].copy()
|
| 149 |
+
template.update(agent_data)
|
| 150 |
+
agent_data = template
|
| 151 |
+
|
| 152 |
+
# Add default metrics and timestamps
|
| 153 |
+
agent_data.setdefault("metrics", {
|
| 154 |
+
"messages_processed": 0,
|
| 155 |
+
"average_response_time": 0.0,
|
| 156 |
+
"uptime": "0m",
|
| 157 |
+
"error_count": 0
|
| 158 |
+
})
|
| 159 |
+
|
| 160 |
+
agent = Agent(**agent_data)
|
| 161 |
+
agents[agent.id] = agent
|
| 162 |
+
|
| 163 |
+
# Broadcast to WebSocket clients
|
| 164 |
+
await manager.broadcast({
|
| 165 |
+
"type": "agent_created",
|
| 166 |
+
"agent": agent.dict()
|
| 167 |
+
})
|
| 168 |
+
|
| 169 |
+
logger.info(f"Agent created: {agent.id}")
|
| 170 |
+
return agent
|
| 171 |
+
|
| 172 |
+
except Exception as e:
|
| 173 |
+
logger.error(f"Error creating agent: {e}")
|
| 174 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 175 |
+
|
| 176 |
+
@app.get("/api/v1/agents/{agent_id}", response_model=Agent)
|
| 177 |
+
async def get_agent(agent_id: str):
|
| 178 |
+
"""Get specific agent by ID"""
|
| 179 |
+
if agent_id not in agents:
|
| 180 |
+
raise HTTPException(status_code=404, detail="Agent not found")
|
| 181 |
+
return agents[agent_id]
|
| 182 |
+
|
| 183 |
+
@app.put("/api/v1/agents/{agent_id}", response_model=Agent)
|
| 184 |
+
async def update_agent(agent_id: str, agent_data: dict):
|
| 185 |
+
"""Update existing agent"""
|
| 186 |
+
if agent_id not in agents:
|
| 187 |
+
raise HTTPException(status_code=404, detail="Agent not found")
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
# Preserve existing data and update with new data
|
| 191 |
+
existing_agent = agents[agent_id].dict()
|
| 192 |
+
existing_agent.update(agent_data)
|
| 193 |
+
existing_agent["updated_at"] = datetime.now(timezone.utc)
|
| 194 |
+
|
| 195 |
+
agent = Agent(**existing_agent)
|
| 196 |
+
agents[agent_id] = agent
|
| 197 |
+
|
| 198 |
+
# Broadcast update
|
| 199 |
+
await manager.broadcast({
|
| 200 |
+
"type": "agent_updated",
|
| 201 |
+
"agent": agent.dict()
|
| 202 |
+
})
|
| 203 |
+
|
| 204 |
+
logger.info(f"Agent updated: {agent_id}")
|
| 205 |
+
return agent
|
| 206 |
+
|
| 207 |
+
except Exception as e:
|
| 208 |
+
logger.error(f"Error updating agent: {e}")
|
| 209 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 210 |
+
|
| 211 |
+
@app.delete("/api/v1/agents/{agent_id}")
|
| 212 |
+
async def delete_agent(agent_id: str):
|
| 213 |
+
"""Delete agent"""
|
| 214 |
+
if agent_id not in agents:
|
| 215 |
+
raise HTTPException(status_code=404, detail="Agent not found")
|
| 216 |
+
|
| 217 |
+
del agents[agent_id]
|
| 218 |
+
|
| 219 |
+
# Broadcast deletion
|
| 220 |
+
await manager.broadcast({
|
| 221 |
+
"type": "agent_deleted",
|
| 222 |
+
"agent_id": agent_id
|
| 223 |
+
})
|
| 224 |
+
|
| 225 |
+
logger.info(f"Agent deleted: {agent_id}")
|
| 226 |
+
return {"message": "Agent deleted successfully"}
|
| 227 |
+
|
| 228 |
+
# Agent Control Endpoints
|
| 229 |
+
@app.post("/api/v1/agents/{agent_id}/start")
|
| 230 |
+
async def start_agent(agent_id: str):
|
| 231 |
+
"""Start an agent"""
|
| 232 |
+
if agent_id not in agents:
|
| 233 |
+
raise HTTPException(status_code=404, detail="Agent not found")
|
| 234 |
+
|
| 235 |
+
agent = agents[agent_id]
|
| 236 |
+
|
| 237 |
+
# Simulate agent startup process
|
| 238 |
+
agent.status = "starting"
|
| 239 |
+
agent.updated_at = datetime.now(timezone.utc)
|
| 240 |
+
|
| 241 |
+
# Broadcast status change
|
| 242 |
+
await manager.broadcast({
|
| 243 |
+
"type": "agent_status_changed",
|
| 244 |
+
"agent_id": agent_id,
|
| 245 |
+
"status": "starting"
|
| 246 |
+
})
|
| 247 |
+
|
| 248 |
+
# Simulate startup delay
|
| 249 |
+
await asyncio.sleep(2)
|
| 250 |
+
|
| 251 |
+
# Mark as active
|
| 252 |
+
agent.status = "active"
|
| 253 |
+
agent.metrics.uptime = "0m"
|
| 254 |
+
|
| 255 |
+
await manager.broadcast({
|
| 256 |
+
"type": "agent_status_changed",
|
| 257 |
+
"agent_id": agent_id,
|
| 258 |
+
"status": "active"
|
| 259 |
+
})
|
| 260 |
+
|
| 261 |
+
logger.info(f"Agent started: {agent_id}")
|
| 262 |
+
return {"message": f"Agent {agent_id} started successfully"}
|
| 263 |
+
|
| 264 |
+
@app.post("/api/v1/agents/{agent_id}/stop")
|
| 265 |
+
async def stop_agent(agent_id: str):
|
| 266 |
+
"""Stop an agent"""
|
| 267 |
+
if agent_id not in agents:
|
| 268 |
+
raise HTTPException(status_code=404, detail="Agent not found")
|
| 269 |
+
|
| 270 |
+
agent = agents[agent_id]
|
| 271 |
+
agent.status = "stopping"
|
| 272 |
+
agent.updated_at = datetime.now(timezone.utc)
|
| 273 |
+
|
| 274 |
+
# Broadcast status change
|
| 275 |
+
await manager.broadcast({
|
| 276 |
+
"type": "agent_status_changed",
|
| 277 |
+
"agent_id": agent_id,
|
| 278 |
+
"status": "stopping"
|
| 279 |
+
})
|
| 280 |
+
|
| 281 |
+
# Simulate shutdown delay
|
| 282 |
+
await asyncio.sleep(1)
|
| 283 |
+
|
| 284 |
+
agent.status = "inactive"
|
| 285 |
+
|
| 286 |
+
await manager.broadcast({
|
| 287 |
+
"type": "agent_status_changed",
|
| 288 |
+
"agent_id": agent_id,
|
| 289 |
+
"status": "inactive"
|
| 290 |
+
})
|
| 291 |
+
|
| 292 |
+
logger.info(f"Agent stopped: {agent_id}")
|
| 293 |
+
return {"message": f"Agent {agent_id} stopped successfully"}
|
| 294 |
+
|
| 295 |
+
# Message Management
|
| 296 |
+
@app.post("/api/v1/messages/send", response_model=MessageResponse)
|
| 297 |
+
async def send_message(message_req: MessageRequest):
|
| 298 |
+
"""Send message between agents"""
|
| 299 |
+
if message_req.sender_id not in agents:
|
| 300 |
+
raise HTTPException(status_code=404, detail="Sender agent not found")
|
| 301 |
+
if message_req.receiver_id not in agents:
|
| 302 |
+
raise HTTPException(status_code=404, detail="Receiver agent not found")
|
| 303 |
+
|
| 304 |
+
# Create message
|
| 305 |
+
message = MessageResponse(
|
| 306 |
+
id=f"msg_{len(messages)}_{datetime.now().timestamp()}",
|
| 307 |
+
sender_id=message_req.sender_id,
|
| 308 |
+
receiver_id=message_req.receiver_id,
|
| 309 |
+
content=message_req.content,
|
| 310 |
+
timestamp=datetime.now(timezone.utc),
|
| 311 |
+
status="sent"
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
messages.append(message)
|
| 315 |
+
|
| 316 |
+
# Update sender metrics
|
| 317 |
+
agents[message_req.sender_id].metrics.messages_processed += 1
|
| 318 |
+
|
| 319 |
+
# Broadcast message
|
| 320 |
+
await manager.broadcast({
|
| 321 |
+
"type": "message_sent",
|
| 322 |
+
"message": message.dict()
|
| 323 |
+
})
|
| 324 |
+
|
| 325 |
+
# TODO: Implement actual AI response via colossus API
|
| 326 |
+
# For now, simulate a response
|
| 327 |
+
await asyncio.sleep(0.5)
|
| 328 |
+
|
| 329 |
+
response_content = f"Response from {agents[message_req.receiver_id].name}: I received your message '{message_req.content}'"
|
| 330 |
+
|
| 331 |
+
response_message = MessageResponse(
|
| 332 |
+
id=f"msg_{len(messages)}_{datetime.now().timestamp()}",
|
| 333 |
+
sender_id=message_req.receiver_id,
|
| 334 |
+
receiver_id=message_req.sender_id,
|
| 335 |
+
content=response_content,
|
| 336 |
+
timestamp=datetime.now(timezone.utc),
|
| 337 |
+
status="sent"
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
messages.append(response_message)
|
| 341 |
+
|
| 342 |
+
# Update receiver metrics
|
| 343 |
+
agents[message_req.receiver_id].metrics.messages_processed += 1
|
| 344 |
+
|
| 345 |
+
await manager.broadcast({
|
| 346 |
+
"type": "message_received",
|
| 347 |
+
"message": response_message.dict()
|
| 348 |
+
})
|
| 349 |
+
|
| 350 |
+
logger.info(f"Message sent: {message_req.sender_id} -> {message_req.receiver_id}")
|
| 351 |
+
return message
|
| 352 |
+
|
| 353 |
+
@app.get("/api/v1/messages", response_model=List[MessageResponse])
|
| 354 |
+
async def list_messages(limit: int = 100, agent_id: Optional[str] = None):
|
| 355 |
+
"""Get messages with optional filtering"""
|
| 356 |
+
filtered_messages = messages
|
| 357 |
+
|
| 358 |
+
if agent_id:
|
| 359 |
+
filtered_messages = [
|
| 360 |
+
msg for msg in messages
|
| 361 |
+
if msg.sender_id == agent_id or msg.receiver_id == agent_id
|
| 362 |
+
]
|
| 363 |
+
|
| 364 |
+
return filtered_messages[-limit:]
|
| 365 |
+
|
| 366 |
+
# System Status
|
| 367 |
+
@app.get("/api/v1/system/status")
|
| 368 |
+
async def get_system_status():
|
| 369 |
+
"""Get overall system status"""
|
| 370 |
+
active_agents = len([a for a in agents.values() if a.status == "active"])
|
| 371 |
+
total_messages = len(messages)
|
| 372 |
+
|
| 373 |
+
return {
|
| 374 |
+
"status": "healthy",
|
| 375 |
+
"agents": {
|
| 376 |
+
"total": len(agents),
|
| 377 |
+
"active": active_agents,
|
| 378 |
+
"inactive": len(agents) - active_agents
|
| 379 |
+
},
|
| 380 |
+
"messages": {
|
| 381 |
+
"total": total_messages
|
| 382 |
+
},
|
| 383 |
+
"websocket_connections": len(manager.active_connections),
|
| 384 |
+
"timestamp": datetime.now(timezone.utc)
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
# WebSocket endpoint for real-time updates
|
| 388 |
+
@app.websocket("/ws")
|
| 389 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 390 |
+
await manager.connect(websocket)
|
| 391 |
+
try:
|
| 392 |
+
while True:
|
| 393 |
+
# Keep connection alive
|
| 394 |
+
await websocket.receive_text()
|
| 395 |
+
except WebSocketDisconnect:
|
| 396 |
+
manager.disconnect(websocket)
|
| 397 |
+
|
| 398 |
+
# Initialize with templates on startup
|
| 399 |
+
@app.on_event("startup")
|
| 400 |
+
async def startup_event():
|
| 401 |
+
"""Initialize agents from templates"""
|
| 402 |
+
logger.info("SAAP Agent Manager starting up...")
|
| 403 |
+
|
| 404 |
+
# Create default agents from templates
|
| 405 |
+
for template_id, template_data in agent_templates.items():
|
| 406 |
+
try:
|
| 407 |
+
template_data["created_at"] = datetime.now(timezone.utc)
|
| 408 |
+
template_data["updated_at"] = datetime.now(timezone.utc)
|
| 409 |
+
agent = Agent(**template_data)
|
| 410 |
+
agents[agent.id] = agent
|
| 411 |
+
logger.info(f"Loaded agent template: {agent.id}")
|
| 412 |
+
except Exception as e:
|
| 413 |
+
logger.error(f"Error loading template {template_id}: {e}")
|
| 414 |
+
|
| 415 |
+
logger.info(f"Agent Manager ready with {len(agents)} agents")
|
| 416 |
+
|
| 417 |
+
if __name__ == "__main__":
|
| 418 |
+
import uvicorn
|
| 419 |
+
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|
backend/api/agents.py
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP Agent Management API
|
| 3 |
+
FastAPI endpoints for agent CRUD operations
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
|
| 7 |
+
from fastapi.responses import JSONResponse
|
| 8 |
+
from typing import List, Dict, Any, Optional
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import json
|
| 11 |
+
import asyncio
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
from ..models.agent_schema import (
|
| 15 |
+
SaapAgent, AgentRegistry, AgentStats, AgentStatus, AgentType,
|
| 16 |
+
AgentTemplates, validate_agent_json
|
| 17 |
+
)
|
| 18 |
+
from ..services.agent_manager import AgentManager
|
| 19 |
+
from ..services.message_queue import MessageQueueService
|
| 20 |
+
from ..utils.validators import validate_agent_id, validate_json_schema
|
| 21 |
+
|
| 22 |
+
# Initialize router
|
| 23 |
+
router = APIRouter(prefix="/api/v1/agents", tags=["agents"])
|
| 24 |
+
|
| 25 |
+
# Dependency injection
|
| 26 |
+
async def get_agent_manager() -> AgentManager:
|
| 27 |
+
"""Get agent manager instance"""
|
| 28 |
+
return AgentManager()
|
| 29 |
+
|
| 30 |
+
async def get_message_queue() -> MessageQueueService:
|
| 31 |
+
"""Get message queue service instance"""
|
| 32 |
+
return MessageQueueService()
|
| 33 |
+
|
| 34 |
+
# ===== AGENT LIFECYCLE ENDPOINTS =====
|
| 35 |
+
|
| 36 |
+
@router.get("/")
|
| 37 |
+
async def list_agents(
|
| 38 |
+
status: Optional[AgentStatus] = None,
|
| 39 |
+
agent_type: Optional[AgentType] = None,
|
| 40 |
+
include_stats: bool = False,
|
| 41 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 42 |
+
) -> Dict[str, Any]:
|
| 43 |
+
"""
|
| 44 |
+
List all registered agents with optional filtering
|
| 45 |
+
|
| 46 |
+
Query Parameters:
|
| 47 |
+
- status: Filter by agent status
|
| 48 |
+
- agent_type: Filter by agent type
|
| 49 |
+
- include_stats: Include runtime statistics
|
| 50 |
+
|
| 51 |
+
🚀 FIXED: Return format compatible with Frontend expectations
|
| 52 |
+
"""
|
| 53 |
+
try:
|
| 54 |
+
agents = await agent_manager.list_agents(
|
| 55 |
+
status=status,
|
| 56 |
+
agent_type=agent_type
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
if include_stats:
|
| 60 |
+
# Enrich agents with statistics
|
| 61 |
+
for agent in agents:
|
| 62 |
+
stats = await agent_manager.get_agent_stats(agent.id)
|
| 63 |
+
agent.runtime_stats = stats
|
| 64 |
+
|
| 65 |
+
# 🚀 FIXED: Frontend expects {"agents": [...]} format
|
| 66 |
+
return {
|
| 67 |
+
"agents": agents,
|
| 68 |
+
"total": len(agents),
|
| 69 |
+
"filters": {
|
| 70 |
+
"status": status.value if status else None,
|
| 71 |
+
"agent_type": agent_type.value if agent_type else None,
|
| 72 |
+
"include_stats": include_stats
|
| 73 |
+
},
|
| 74 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
raise HTTPException(status_code=500, detail=f"Failed to list agents: {str(e)}")
|
| 79 |
+
|
| 80 |
+
@router.get("/{agent_id}")
|
| 81 |
+
async def get_agent(
|
| 82 |
+
agent_id: str,
|
| 83 |
+
include_stats: bool = True,
|
| 84 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 85 |
+
) -> Dict[str, Any]:
|
| 86 |
+
"""
|
| 87 |
+
Get detailed agent information by ID
|
| 88 |
+
|
| 89 |
+
Path Parameters:
|
| 90 |
+
- agent_id: Unique agent identifier
|
| 91 |
+
|
| 92 |
+
Query Parameters:
|
| 93 |
+
- include_stats: Include runtime statistics
|
| 94 |
+
|
| 95 |
+
🚀 FIXED: Standardized response format
|
| 96 |
+
"""
|
| 97 |
+
validate_agent_id(agent_id)
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 101 |
+
if not agent:
|
| 102 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 103 |
+
|
| 104 |
+
if include_stats:
|
| 105 |
+
stats = await agent_manager.get_agent_stats(agent_id)
|
| 106 |
+
agent.runtime_stats = stats
|
| 107 |
+
|
| 108 |
+
return {
|
| 109 |
+
"agent": agent,
|
| 110 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
except HTTPException:
|
| 114 |
+
raise
|
| 115 |
+
except Exception as e:
|
| 116 |
+
raise HTTPException(status_code=500, detail=f"Failed to get agent: {str(e)}")
|
| 117 |
+
|
| 118 |
+
@router.post("/", status_code=201)
|
| 119 |
+
async def create_agent(
|
| 120 |
+
agent_data: Dict[str, Any],
|
| 121 |
+
background_tasks: BackgroundTasks,
|
| 122 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 123 |
+
) -> Dict[str, Any]:
|
| 124 |
+
"""
|
| 125 |
+
Register a new agent
|
| 126 |
+
|
| 127 |
+
Request Body: Complete agent configuration JSON
|
| 128 |
+
|
| 129 |
+
🚀 FIXED: Standardized response format für Frontend compatibility
|
| 130 |
+
"""
|
| 131 |
+
try:
|
| 132 |
+
# Validate agent configuration
|
| 133 |
+
agent = validate_agent_json(agent_data)
|
| 134 |
+
|
| 135 |
+
# Check if agent already exists
|
| 136 |
+
existing = await agent_manager.get_agent(agent.id)
|
| 137 |
+
if existing:
|
| 138 |
+
raise HTTPException(
|
| 139 |
+
status_code=409,
|
| 140 |
+
detail=f"Agent '{agent.id}' already exists"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Register agent
|
| 144 |
+
created_agent = await agent_manager.register_agent(agent)
|
| 145 |
+
|
| 146 |
+
# Initialize agent queues in background
|
| 147 |
+
background_tasks.add_task(
|
| 148 |
+
_initialize_agent_queues,
|
| 149 |
+
created_agent.id,
|
| 150 |
+
created_agent.communication
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# 🚀 FIXED: Frontend-compatible response format
|
| 154 |
+
return {
|
| 155 |
+
"success": True,
|
| 156 |
+
"message": f"Agent '{created_agent.name}' created successfully",
|
| 157 |
+
"agent": created_agent,
|
| 158 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
except HTTPException:
|
| 162 |
+
raise
|
| 163 |
+
except ValueError as e:
|
| 164 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 165 |
+
except Exception as e:
|
| 166 |
+
raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}")
|
| 167 |
+
|
| 168 |
+
@router.put("/{agent_id}")
|
| 169 |
+
async def update_agent(
|
| 170 |
+
agent_id: str,
|
| 171 |
+
agent_data: Dict[str, Any],
|
| 172 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 173 |
+
) -> Dict[str, Any]:
|
| 174 |
+
"""
|
| 175 |
+
Update agent configuration
|
| 176 |
+
|
| 177 |
+
Path Parameters:
|
| 178 |
+
- agent_id: Agent to update
|
| 179 |
+
|
| 180 |
+
Request Body: Updated agent configuration JSON
|
| 181 |
+
|
| 182 |
+
🚀 FIXED: Standardized response format
|
| 183 |
+
"""
|
| 184 |
+
validate_agent_id(agent_id)
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
# Validate updated configuration
|
| 188 |
+
updated_agent = validate_agent_json(agent_data)
|
| 189 |
+
|
| 190 |
+
# Ensure ID consistency
|
| 191 |
+
if updated_agent.id != agent_id:
|
| 192 |
+
raise HTTPException(
|
| 193 |
+
status_code=400,
|
| 194 |
+
detail="Agent ID in body must match path parameter"
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
# Check if agent exists
|
| 198 |
+
existing = await agent_manager.get_agent(agent_id)
|
| 199 |
+
if not existing:
|
| 200 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 201 |
+
|
| 202 |
+
# Update timestamp
|
| 203 |
+
updated_agent.metadata.updated = datetime.utcnow()
|
| 204 |
+
|
| 205 |
+
# Update agent
|
| 206 |
+
result = await agent_manager.update_agent(agent_id, updated_agent)
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
"success": True,
|
| 210 |
+
"message": f"Agent '{agent_id}' updated successfully",
|
| 211 |
+
"agent": result,
|
| 212 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
except HTTPException:
|
| 216 |
+
raise
|
| 217 |
+
except ValueError as e:
|
| 218 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 219 |
+
except Exception as e:
|
| 220 |
+
raise HTTPException(status_code=500, detail=f"Failed to update agent: {str(e)}")
|
| 221 |
+
|
| 222 |
+
@router.delete("/{agent_id}", status_code=204)
|
| 223 |
+
async def delete_agent(
|
| 224 |
+
agent_id: str,
|
| 225 |
+
force: bool = False,
|
| 226 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 227 |
+
):
|
| 228 |
+
"""
|
| 229 |
+
Delete/deregister an agent
|
| 230 |
+
|
| 231 |
+
Path Parameters:
|
| 232 |
+
- agent_id: Agent to delete
|
| 233 |
+
|
| 234 |
+
Query Parameters:
|
| 235 |
+
- force: Force deletion even if agent is active
|
| 236 |
+
"""
|
| 237 |
+
validate_agent_id(agent_id)
|
| 238 |
+
|
| 239 |
+
try:
|
| 240 |
+
# Check if agent exists
|
| 241 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 242 |
+
if not agent:
|
| 243 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 244 |
+
|
| 245 |
+
# Check if agent is active
|
| 246 |
+
if agent.status == AgentStatus.ACTIVE and not force:
|
| 247 |
+
raise HTTPException(
|
| 248 |
+
status_code=400,
|
| 249 |
+
detail="Cannot delete active agent. Stop agent first or use force=true"
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Delete agent
|
| 253 |
+
await agent_manager.delete_agent(agent_id)
|
| 254 |
+
|
| 255 |
+
except HTTPException:
|
| 256 |
+
raise
|
| 257 |
+
except Exception as e:
|
| 258 |
+
raise HTTPException(status_code=500, detail=f"Failed to delete agent: {str(e)}")
|
| 259 |
+
|
| 260 |
+
# ===== AGENT CONTROL ENDPOINTS =====
|
| 261 |
+
|
| 262 |
+
@router.post("/{agent_id}/start")
|
| 263 |
+
async def start_agent(
|
| 264 |
+
agent_id: str,
|
| 265 |
+
background_tasks: BackgroundTasks,
|
| 266 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 267 |
+
) -> Dict[str, Any]:
|
| 268 |
+
"""
|
| 269 |
+
Start an inactive agent
|
| 270 |
+
|
| 271 |
+
Path Parameters:
|
| 272 |
+
- agent_id: Agent to start
|
| 273 |
+
"""
|
| 274 |
+
validate_agent_id(agent_id)
|
| 275 |
+
|
| 276 |
+
try:
|
| 277 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 278 |
+
if not agent:
|
| 279 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 280 |
+
|
| 281 |
+
if agent.status == AgentStatus.ACTIVE:
|
| 282 |
+
return {"message": f"Agent '{agent_id}' is already active", "status": "success"}
|
| 283 |
+
|
| 284 |
+
# Start agent in background
|
| 285 |
+
background_tasks.add_task(_start_agent_process, agent_id, agent_manager)
|
| 286 |
+
|
| 287 |
+
return {
|
| 288 |
+
"message": f"Agent '{agent_id}' startup initiated",
|
| 289 |
+
"status": "starting"
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
except HTTPException:
|
| 293 |
+
raise
|
| 294 |
+
except Exception as e:
|
| 295 |
+
raise HTTPException(status_code=500, detail=f"Failed to start agent: {str(e)}")
|
| 296 |
+
|
| 297 |
+
@router.post("/{agent_id}/stop")
|
| 298 |
+
async def stop_agent(
|
| 299 |
+
agent_id: str,
|
| 300 |
+
graceful: bool = True,
|
| 301 |
+
timeout: int = 30,
|
| 302 |
+
background_tasks: BackgroundTasks,
|
| 303 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 304 |
+
) -> Dict[str, Any]:
|
| 305 |
+
"""
|
| 306 |
+
Stop an active agent
|
| 307 |
+
|
| 308 |
+
Path Parameters:
|
| 309 |
+
- agent_id: Agent to stop
|
| 310 |
+
|
| 311 |
+
Query Parameters:
|
| 312 |
+
- graceful: Perform graceful shutdown
|
| 313 |
+
- timeout: Shutdown timeout in seconds
|
| 314 |
+
"""
|
| 315 |
+
validate_agent_id(agent_id)
|
| 316 |
+
|
| 317 |
+
try:
|
| 318 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 319 |
+
if not agent:
|
| 320 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 321 |
+
|
| 322 |
+
if agent.status != AgentStatus.ACTIVE:
|
| 323 |
+
return {"message": f"Agent '{agent_id}' is not active", "status": "inactive"}
|
| 324 |
+
|
| 325 |
+
# Stop agent in background
|
| 326 |
+
background_tasks.add_task(
|
| 327 |
+
_stop_agent_process,
|
| 328 |
+
agent_id,
|
| 329 |
+
agent_manager,
|
| 330 |
+
graceful,
|
| 331 |
+
timeout
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
return {
|
| 335 |
+
"message": f"Agent '{agent_id}' shutdown initiated",
|
| 336 |
+
"status": "stopping",
|
| 337 |
+
"graceful": graceful,
|
| 338 |
+
"timeout": timeout
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
except HTTPException:
|
| 342 |
+
raise
|
| 343 |
+
except Exception as e:
|
| 344 |
+
raise HTTPException(status_code=500, detail=f"Failed to stop agent: {str(e)}")
|
| 345 |
+
|
| 346 |
+
@router.post("/{agent_id}/restart")
|
| 347 |
+
async def restart_agent(
|
| 348 |
+
agent_id: str,
|
| 349 |
+
background_tasks: BackgroundTasks,
|
| 350 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 351 |
+
) -> Dict[str, Any]:
|
| 352 |
+
"""
|
| 353 |
+
Restart an agent (stop + start)
|
| 354 |
+
|
| 355 |
+
Path Parameters:
|
| 356 |
+
- agent_id: Agent to restart
|
| 357 |
+
"""
|
| 358 |
+
validate_agent_id(agent_id)
|
| 359 |
+
|
| 360 |
+
try:
|
| 361 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 362 |
+
if not agent:
|
| 363 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 364 |
+
|
| 365 |
+
# Restart agent in background
|
| 366 |
+
background_tasks.add_task(_restart_agent_process, agent_id, agent_manager)
|
| 367 |
+
|
| 368 |
+
return {
|
| 369 |
+
"message": f"Agent '{agent_id}' restart initiated",
|
| 370 |
+
"status": "restarting"
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
except HTTPException:
|
| 374 |
+
raise
|
| 375 |
+
except Exception as e:
|
| 376 |
+
raise HTTPException(status_code=500, detail=f"Failed to restart agent: {str(e)}")
|
| 377 |
+
|
| 378 |
+
# ===== AGENT COMMUNICATION ENDPOINTS =====
|
| 379 |
+
|
| 380 |
+
@router.post("/{agent_id}/chat")
|
| 381 |
+
async def chat_with_agent(
|
| 382 |
+
agent_id: str,
|
| 383 |
+
message_data: Dict[str, str],
|
| 384 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 385 |
+
) -> Dict[str, Any]:
|
| 386 |
+
"""
|
| 387 |
+
Send message to agent and get response
|
| 388 |
+
|
| 389 |
+
Path Parameters:
|
| 390 |
+
- agent_id: Agent to communicate with
|
| 391 |
+
|
| 392 |
+
Request Body:
|
| 393 |
+
- message: Message content
|
| 394 |
+
|
| 395 |
+
🚀 FIXED: Improved chat endpoint with consistent response format
|
| 396 |
+
"""
|
| 397 |
+
validate_agent_id(agent_id)
|
| 398 |
+
|
| 399 |
+
try:
|
| 400 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 401 |
+
if not agent:
|
| 402 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 403 |
+
|
| 404 |
+
message = message_data.get("message", "")
|
| 405 |
+
if not message.strip():
|
| 406 |
+
raise HTTPException(status_code=400, detail="Message cannot be empty")
|
| 407 |
+
|
| 408 |
+
# Send message to agent
|
| 409 |
+
response = await agent_manager.send_message_to_agent(agent_id, message)
|
| 410 |
+
|
| 411 |
+
# Check if response contains error
|
| 412 |
+
if "error" in response:
|
| 413 |
+
return {
|
| 414 |
+
"success": False,
|
| 415 |
+
"error": response["error"],
|
| 416 |
+
"agent_id": agent_id,
|
| 417 |
+
"timestamp": response.get("timestamp", datetime.utcnow().isoformat())
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
return {
|
| 421 |
+
"success": True,
|
| 422 |
+
"agent_id": agent_id,
|
| 423 |
+
"message": message,
|
| 424 |
+
"response": response.get("content", ""),
|
| 425 |
+
"response_time": response.get("response_time", 0),
|
| 426 |
+
"tokens_used": response.get("tokens_used", 0),
|
| 427 |
+
"timestamp": response.get("timestamp", datetime.utcnow().isoformat())
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
except HTTPException:
|
| 431 |
+
raise
|
| 432 |
+
except Exception as e:
|
| 433 |
+
raise HTTPException(status_code=500, detail=f"Failed to chat with agent: {str(e)}")
|
| 434 |
+
|
| 435 |
+
# ===== 🚀 NEW: OPENROUTER COMMUNICATION ENDPOINTS =====
|
| 436 |
+
|
| 437 |
+
@router.post("/{agent_id}/chat/openrouter")
|
| 438 |
+
async def chat_with_agent_openrouter(
|
| 439 |
+
agent_id: str,
|
| 440 |
+
message_data: Dict[str, str],
|
| 441 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 442 |
+
) -> Dict[str, Any]:
|
| 443 |
+
"""
|
| 444 |
+
Send message to agent using OpenRouter provider (Fast mode)
|
| 445 |
+
|
| 446 |
+
Path Parameters:
|
| 447 |
+
- agent_id: Agent to communicate with
|
| 448 |
+
|
| 449 |
+
Request Body:
|
| 450 |
+
- message: Message content
|
| 451 |
+
|
| 452 |
+
🚀 NEW: OpenRouter-specific chat endpoint for fast responses
|
| 453 |
+
"""
|
| 454 |
+
validate_agent_id(agent_id)
|
| 455 |
+
|
| 456 |
+
try:
|
| 457 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 458 |
+
if not agent:
|
| 459 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 460 |
+
|
| 461 |
+
message = message_data.get("message", "")
|
| 462 |
+
if not message.strip():
|
| 463 |
+
raise HTTPException(status_code=400, detail="Message cannot be empty")
|
| 464 |
+
|
| 465 |
+
# Send message to agent with OpenRouter provider
|
| 466 |
+
response = await agent_manager.send_message_to_agent(
|
| 467 |
+
agent_id,
|
| 468 |
+
message,
|
| 469 |
+
provider="openrouter"
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
# Check if response contains error
|
| 473 |
+
if "error" in response:
|
| 474 |
+
return {
|
| 475 |
+
"success": False,
|
| 476 |
+
"error": response["error"],
|
| 477 |
+
"provider": "openrouter",
|
| 478 |
+
"agent_id": agent_id,
|
| 479 |
+
"timestamp": response.get("timestamp", datetime.utcnow().isoformat())
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
return {
|
| 483 |
+
"success": True,
|
| 484 |
+
"agent_id": agent_id,
|
| 485 |
+
"provider": "openrouter",
|
| 486 |
+
"message": message,
|
| 487 |
+
"response": response.get("content", ""),
|
| 488 |
+
"response_time": response.get("response_time", 0),
|
| 489 |
+
"tokens_used": response.get("tokens_used", 0),
|
| 490 |
+
"cost_usd": response.get("cost_usd", 0.0),
|
| 491 |
+
"model": response.get("model", ""),
|
| 492 |
+
"timestamp": response.get("timestamp", datetime.utcnow().isoformat())
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
except HTTPException:
|
| 496 |
+
raise
|
| 497 |
+
except Exception as e:
|
| 498 |
+
raise HTTPException(status_code=500, detail=f"Failed to chat with agent via OpenRouter: {str(e)}")
|
| 499 |
+
|
| 500 |
+
@router.post("/{agent_id}/chat/compare")
|
| 501 |
+
async def compare_providers_chat(
|
| 502 |
+
agent_id: str,
|
| 503 |
+
message_data: Dict[str, str],
|
| 504 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 505 |
+
) -> Dict[str, Any]:
|
| 506 |
+
"""
|
| 507 |
+
Send same message to agent using both providers for performance comparison
|
| 508 |
+
|
| 509 |
+
Path Parameters:
|
| 510 |
+
- agent_id: Agent to communicate with
|
| 511 |
+
|
| 512 |
+
Request Body:
|
| 513 |
+
- message: Message content
|
| 514 |
+
|
| 515 |
+
🚀 NEW: Multi-provider comparison endpoint
|
| 516 |
+
"""
|
| 517 |
+
validate_agent_id(agent_id)
|
| 518 |
+
|
| 519 |
+
try:
|
| 520 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 521 |
+
if not agent:
|
| 522 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 523 |
+
|
| 524 |
+
message = message_data.get("message", "")
|
| 525 |
+
if not message.strip():
|
| 526 |
+
raise HTTPException(status_code=400, detail="Message cannot be empty")
|
| 527 |
+
|
| 528 |
+
# Send to both providers concurrently
|
| 529 |
+
import asyncio
|
| 530 |
+
from datetime import datetime
|
| 531 |
+
|
| 532 |
+
start_time = datetime.utcnow()
|
| 533 |
+
|
| 534 |
+
# Run both providers in parallel
|
| 535 |
+
tasks = [
|
| 536 |
+
agent_manager.send_message_to_agent(agent_id, message, provider="colossus"),
|
| 537 |
+
agent_manager.send_message_to_agent(agent_id, message, provider="openrouter")
|
| 538 |
+
]
|
| 539 |
+
|
| 540 |
+
try:
|
| 541 |
+
colossus_response, openrouter_response = await asyncio.gather(*tasks, return_exceptions=True)
|
| 542 |
+
except Exception as e:
|
| 543 |
+
# Fallback to sequential if parallel fails
|
| 544 |
+
colossus_response = await agent_manager.send_message_to_agent(agent_id, message, provider="colossus")
|
| 545 |
+
openrouter_response = await agent_manager.send_message_to_agent(agent_id, message, provider="openrouter")
|
| 546 |
+
|
| 547 |
+
total_time = (datetime.utcnow() - start_time).total_seconds()
|
| 548 |
+
|
| 549 |
+
def format_response(response, provider_name):
|
| 550 |
+
if isinstance(response, Exception):
|
| 551 |
+
return {
|
| 552 |
+
"success": False,
|
| 553 |
+
"error": str(response),
|
| 554 |
+
"provider": provider_name
|
| 555 |
+
}
|
| 556 |
+
elif "error" in response:
|
| 557 |
+
return {
|
| 558 |
+
"success": False,
|
| 559 |
+
"error": response["error"],
|
| 560 |
+
"provider": provider_name
|
| 561 |
+
}
|
| 562 |
+
else:
|
| 563 |
+
return {
|
| 564 |
+
"success": True,
|
| 565 |
+
"provider": provider_name,
|
| 566 |
+
"response": response.get("content", ""),
|
| 567 |
+
"response_time": response.get("response_time", 0),
|
| 568 |
+
"tokens_used": response.get("tokens_used", 0),
|
| 569 |
+
"cost_usd": response.get("cost_usd", 0.0),
|
| 570 |
+
"model": response.get("model", "")
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
return {
|
| 574 |
+
"success": True,
|
| 575 |
+
"agent_id": agent_id,
|
| 576 |
+
"message": message,
|
| 577 |
+
"comparison": {
|
| 578 |
+
"colossus": format_response(colossus_response, "colossus"),
|
| 579 |
+
"openrouter": format_response(openrouter_response, "openrouter")
|
| 580 |
+
},
|
| 581 |
+
"total_comparison_time": total_time,
|
| 582 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
except HTTPException:
|
| 586 |
+
raise
|
| 587 |
+
except Exception as e:
|
| 588 |
+
raise HTTPException(status_code=500, detail=f"Failed to compare providers: {str(e)}")
|
| 589 |
+
|
| 590 |
+
# ===== AGENT STATISTICS ENDPOINTS =====
|
| 591 |
+
|
| 592 |
+
@router.get("/{agent_id}/stats")
|
| 593 |
+
async def get_agent_stats(
|
| 594 |
+
agent_id: str,
|
| 595 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 596 |
+
) -> Dict[str, Any]:
|
| 597 |
+
"""
|
| 598 |
+
Get real-time agent statistics
|
| 599 |
+
|
| 600 |
+
Path Parameters:
|
| 601 |
+
- agent_id: Agent identifier
|
| 602 |
+
"""
|
| 603 |
+
validate_agent_id(agent_id)
|
| 604 |
+
|
| 605 |
+
try:
|
| 606 |
+
# Verify agent exists
|
| 607 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 608 |
+
if not agent:
|
| 609 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 610 |
+
|
| 611 |
+
# Get statistics
|
| 612 |
+
stats = await agent_manager.get_agent_stats(agent_id)
|
| 613 |
+
|
| 614 |
+
return {
|
| 615 |
+
"agent_id": agent_id,
|
| 616 |
+
"stats": stats,
|
| 617 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
except HTTPException:
|
| 621 |
+
raise
|
| 622 |
+
except Exception as e:
|
| 623 |
+
raise HTTPException(status_code=500, detail=f"Failed to get agent stats: {str(e)}")
|
| 624 |
+
|
| 625 |
+
@router.get("/{agent_id}/health")
|
| 626 |
+
async def agent_health_check(
|
| 627 |
+
agent_id: str,
|
| 628 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 629 |
+
) -> Dict[str, Any]:
|
| 630 |
+
"""
|
| 631 |
+
Perform agent health check
|
| 632 |
+
|
| 633 |
+
Path Parameters:
|
| 634 |
+
- agent_id: Agent identifier
|
| 635 |
+
"""
|
| 636 |
+
validate_agent_id(agent_id)
|
| 637 |
+
|
| 638 |
+
try:
|
| 639 |
+
agent = await agent_manager.get_agent(agent_id)
|
| 640 |
+
if not agent:
|
| 641 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 642 |
+
|
| 643 |
+
# Perform health check
|
| 644 |
+
health = await agent_manager.health_check(agent_id)
|
| 645 |
+
|
| 646 |
+
return {
|
| 647 |
+
"agent_id": agent_id,
|
| 648 |
+
"status": agent.status,
|
| 649 |
+
"healthy": health["healthy"],
|
| 650 |
+
"checks": health["checks"],
|
| 651 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
except HTTPException:
|
| 655 |
+
raise
|
| 656 |
+
except Exception as e:
|
| 657 |
+
raise HTTPException(status_code=500, detail=f"Health check failed: {str(e)}")
|
| 658 |
+
|
| 659 |
+
# ===== AGENT TEMPLATES ENDPOINTS =====
|
| 660 |
+
|
| 661 |
+
@router.get("/templates/")
|
| 662 |
+
async def list_agent_templates() -> Dict[str, Any]:
|
| 663 |
+
"""List available agent templates"""
|
| 664 |
+
try:
|
| 665 |
+
templates = [
|
| 666 |
+
"jane_alesi",
|
| 667 |
+
"john_alesi",
|
| 668 |
+
"lara_alesi"
|
| 669 |
+
]
|
| 670 |
+
|
| 671 |
+
return {
|
| 672 |
+
"templates": templates,
|
| 673 |
+
"total": len(templates),
|
| 674 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 675 |
+
}
|
| 676 |
+
except Exception as e:
|
| 677 |
+
raise HTTPException(status_code=500, detail=f"Failed to list templates: {str(e)}")
|
| 678 |
+
|
| 679 |
+
@router.get("/templates/{template_name}")
|
| 680 |
+
async def get_agent_template(template_name: str) -> Dict[str, Any]:
|
| 681 |
+
"""
|
| 682 |
+
Get agent template configuration
|
| 683 |
+
|
| 684 |
+
Path Parameters:
|
| 685 |
+
- template_name: Template identifier
|
| 686 |
+
"""
|
| 687 |
+
try:
|
| 688 |
+
if template_name == "jane_alesi":
|
| 689 |
+
template = AgentTemplates.jane_alesi()
|
| 690 |
+
elif template_name == "john_alesi":
|
| 691 |
+
template = AgentTemplates.john_alesi()
|
| 692 |
+
elif template_name == "lara_alesi":
|
| 693 |
+
template = AgentTemplates.lara_alesi()
|
| 694 |
+
else:
|
| 695 |
+
raise HTTPException(status_code=404, detail=f"Template '{template_name}' not found")
|
| 696 |
+
|
| 697 |
+
return {
|
| 698 |
+
"template_name": template_name,
|
| 699 |
+
"template": template,
|
| 700 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
except HTTPException:
|
| 704 |
+
raise
|
| 705 |
+
except Exception as e:
|
| 706 |
+
raise HTTPException(status_code=500, detail=f"Failed to get template: {str(e)}")
|
| 707 |
+
|
| 708 |
+
@router.post("/templates/{template_name}/create", status_code=201)
|
| 709 |
+
async def create_agent_from_template(
|
| 710 |
+
template_name: str,
|
| 711 |
+
agent_id: str,
|
| 712 |
+
customizations: Dict[str, Any] = None,
|
| 713 |
+
background_tasks: BackgroundTasks,
|
| 714 |
+
agent_manager: AgentManager = Depends(get_agent_manager)
|
| 715 |
+
) -> Dict[str, Any]:
|
| 716 |
+
"""
|
| 717 |
+
Create agent from template with optional customizations
|
| 718 |
+
|
| 719 |
+
Path Parameters:
|
| 720 |
+
- template_name: Template to use
|
| 721 |
+
|
| 722 |
+
Query Parameters:
|
| 723 |
+
- agent_id: Unique ID for new agent
|
| 724 |
+
|
| 725 |
+
Request Body: Optional customizations to apply
|
| 726 |
+
"""
|
| 727 |
+
validate_agent_id(agent_id)
|
| 728 |
+
|
| 729 |
+
try:
|
| 730 |
+
# Get template
|
| 731 |
+
if template_name == "jane_alesi":
|
| 732 |
+
template_agent = AgentTemplates.jane_alesi()
|
| 733 |
+
elif template_name == "john_alesi":
|
| 734 |
+
template_agent = AgentTemplates.john_alesi()
|
| 735 |
+
elif template_name == "lara_alesi":
|
| 736 |
+
template_agent = AgentTemplates.lara_alesi()
|
| 737 |
+
else:
|
| 738 |
+
raise HTTPException(status_code=404, detail=f"Template '{template_name}' not found")
|
| 739 |
+
|
| 740 |
+
# Apply customizations
|
| 741 |
+
if customizations:
|
| 742 |
+
agent_dict = template_agent.dict()
|
| 743 |
+
agent_dict.update(customizations)
|
| 744 |
+
template_agent = SaapAgent(**agent_dict)
|
| 745 |
+
|
| 746 |
+
# Set custom ID
|
| 747 |
+
template_agent.id = agent_id
|
| 748 |
+
template_agent.communication.input_queue = f"{agent_id}_input"
|
| 749 |
+
template_agent.communication.output_queue = f"{agent_id}_output"
|
| 750 |
+
|
| 751 |
+
# Check if agent already exists
|
| 752 |
+
existing = await agent_manager.get_agent(agent_id)
|
| 753 |
+
if existing:
|
| 754 |
+
raise HTTPException(
|
| 755 |
+
status_code=409,
|
| 756 |
+
detail=f"Agent '{agent_id}' already exists"
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
# Create agent
|
| 760 |
+
created_agent = await agent_manager.register_agent(template_agent)
|
| 761 |
+
|
| 762 |
+
# Initialize in background
|
| 763 |
+
background_tasks.add_task(
|
| 764 |
+
_initialize_agent_queues,
|
| 765 |
+
created_agent.id,
|
| 766 |
+
created_agent.communication
|
| 767 |
+
)
|
| 768 |
+
|
| 769 |
+
return {
|
| 770 |
+
"success": True,
|
| 771 |
+
"message": f"Agent '{created_agent.name}' created from template '{template_name}'",
|
| 772 |
+
"agent": created_agent,
|
| 773 |
+
"template_name": template_name,
|
| 774 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
except HTTPException:
|
| 778 |
+
raise
|
| 779 |
+
except Exception as e:
|
| 780 |
+
raise HTTPException(status_code=500, detail=f"Failed to create agent from template: {str(e)}")
|
| 781 |
+
|
| 782 |
+
# ===== BACKGROUND TASKS =====
|
| 783 |
+
|
| 784 |
+
async def _initialize_agent_queues(agent_id: str, comm_config):
|
| 785 |
+
"""Initialize Redis queues for agent"""
|
| 786 |
+
try:
|
| 787 |
+
queue_service = MessageQueueService()
|
| 788 |
+
await queue_service.create_agent_queues(agent_id, comm_config)
|
| 789 |
+
except Exception as e:
|
| 790 |
+
print(f"Failed to initialize queues for {agent_id}: {e}")
|
| 791 |
+
|
| 792 |
+
async def _start_agent_process(agent_id: str, agent_manager: AgentManager):
|
| 793 |
+
"""Start agent process in background"""
|
| 794 |
+
try:
|
| 795 |
+
await agent_manager.start_agent(agent_id)
|
| 796 |
+
except Exception as e:
|
| 797 |
+
print(f"Failed to start agent {agent_id}: {e}")
|
| 798 |
+
|
| 799 |
+
async def _stop_agent_process(agent_id: str, agent_manager: AgentManager, graceful: bool, timeout: int):
|
| 800 |
+
"""Stop agent process in background"""
|
| 801 |
+
try:
|
| 802 |
+
await agent_manager.stop_agent(agent_id, graceful=graceful, timeout=timeout)
|
| 803 |
+
except Exception as e:
|
| 804 |
+
print(f"Failed to stop agent {agent_id}: {e}")
|
| 805 |
+
|
| 806 |
+
async def _restart_agent_process(agent_id: str, agent_manager: AgentManager):
|
| 807 |
+
"""Restart agent process in background"""
|
| 808 |
+
try:
|
| 809 |
+
await agent_manager.restart_agent(agent_id)
|
| 810 |
+
except Exception as e:
|
| 811 |
+
print(f"Failed to restart agent {agent_id}: {e}")
|
| 812 |
+
|
| 813 |
+
# ===== ERROR HANDLERS =====
|
| 814 |
+
|
| 815 |
+
@router.exception_handler(ValueError)
|
| 816 |
+
async def value_error_handler(request, exc):
|
| 817 |
+
return JSONResponse(
|
| 818 |
+
status_code=400,
|
| 819 |
+
content={"detail": f"Validation error: {str(exc)}"}
|
| 820 |
+
)
|
backend/api/colossus_client.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP colossus Server Integration
|
| 3 |
+
OpenAI-Compatible API Client for mistral-small3.2:24b-instruct-2506
|
| 4 |
+
"""
|
| 5 |
+
import requests
|
| 6 |
+
import json
|
| 7 |
+
import asyncio
|
| 8 |
+
import aiohttp
|
| 9 |
+
from typing import Dict, List, Optional
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
import time
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class ColossusConfig:
|
| 15 |
+
"""colossus Server Configuration"""
|
| 16 |
+
base_url: str = "https://ai.adrian-schupp.de"
|
| 17 |
+
api_key: str = "sk-dBoxml3krytIRLdjr35Lnw"
|
| 18 |
+
model: str = "mistral-small3.2:24b-instruct-2506"
|
| 19 |
+
timeout: int = 90 # Increased from 30 to 90 seconds for larger models
|
| 20 |
+
max_tokens: int = 1000
|
| 21 |
+
temperature: float = 0.7
|
| 22 |
+
|
| 23 |
+
class ColossusClient:
|
| 24 |
+
"""
|
| 25 |
+
OpenAI-Compatible API Client for colossus Server
|
| 26 |
+
Handles communication with mistral-small model for SAAP Agents
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, config: ColossusConfig = None):
|
| 30 |
+
self.config = config or ColossusConfig()
|
| 31 |
+
self.session = None
|
| 32 |
+
|
| 33 |
+
async def __aenter__(self):
|
| 34 |
+
self.session = aiohttp.ClientSession(
|
| 35 |
+
timeout=aiohttp.ClientTimeout(total=self.config.timeout),
|
| 36 |
+
headers={
|
| 37 |
+
"Authorization": f"Bearer {self.config.api_key}",
|
| 38 |
+
"Content-Type": "application/json"
|
| 39 |
+
}
|
| 40 |
+
)
|
| 41 |
+
return self
|
| 42 |
+
|
| 43 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 44 |
+
if self.session:
|
| 45 |
+
await self.session.close()
|
| 46 |
+
|
| 47 |
+
async def chat_completion(
|
| 48 |
+
self,
|
| 49 |
+
messages: List[Dict[str, str]],
|
| 50 |
+
agent_id: str = "default",
|
| 51 |
+
temperature: Optional[float] = None,
|
| 52 |
+
max_tokens: Optional[int] = None
|
| 53 |
+
) -> Dict:
|
| 54 |
+
"""
|
| 55 |
+
Send chat completion request to colossus
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
messages: List of message objects [{"role": "user", "content": "..."}]
|
| 59 |
+
agent_id: SAAP Agent identifier for logging
|
| 60 |
+
temperature: Model temperature override
|
| 61 |
+
max_tokens: Max tokens override
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
API response with generated text
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
start_time = time.time()
|
| 68 |
+
|
| 69 |
+
payload = {
|
| 70 |
+
"model": self.config.model,
|
| 71 |
+
"messages": messages,
|
| 72 |
+
"temperature": temperature or self.config.temperature,
|
| 73 |
+
"max_tokens": max_tokens or self.config.max_tokens,
|
| 74 |
+
"stream": False
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
try:
|
| 78 |
+
async with self.session.post(
|
| 79 |
+
f"{self.config.base_url}/v1/chat/completions",
|
| 80 |
+
json=payload
|
| 81 |
+
) as response:
|
| 82 |
+
|
| 83 |
+
response_time = time.time() - start_time
|
| 84 |
+
|
| 85 |
+
if response.status == 200:
|
| 86 |
+
data = await response.json()
|
| 87 |
+
|
| 88 |
+
# SAAP Performance Monitoring
|
| 89 |
+
print(f"✅ colossus Response [{agent_id}]: {response_time:.2f}s")
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
"success": True,
|
| 93 |
+
"response": data,
|
| 94 |
+
"response_time": response_time,
|
| 95 |
+
"agent_id": agent_id,
|
| 96 |
+
"model": self.config.model
|
| 97 |
+
}
|
| 98 |
+
else:
|
| 99 |
+
error_text = await response.text()
|
| 100 |
+
print(f"❌ colossus Error [{agent_id}]: {response.status} - {error_text}")
|
| 101 |
+
|
| 102 |
+
return {
|
| 103 |
+
"success": False,
|
| 104 |
+
"error": f"HTTP {response.status}: {error_text}",
|
| 105 |
+
"response_time": response_time,
|
| 106 |
+
"agent_id": agent_id
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
except asyncio.TimeoutError:
|
| 110 |
+
return {
|
| 111 |
+
"success": False,
|
| 112 |
+
"error": "Request timeout",
|
| 113 |
+
"response_time": self.config.timeout,
|
| 114 |
+
"agent_id": agent_id
|
| 115 |
+
}
|
| 116 |
+
except Exception as e:
|
| 117 |
+
return {
|
| 118 |
+
"success": False,
|
| 119 |
+
"error": str(e),
|
| 120 |
+
"response_time": time.time() - start_time,
|
| 121 |
+
"agent_id": agent_id
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
def sync_chat_completion(
|
| 125 |
+
self,
|
| 126 |
+
messages: List[Dict[str, str]],
|
| 127 |
+
agent_id: str = "default"
|
| 128 |
+
) -> Dict:
|
| 129 |
+
"""
|
| 130 |
+
Synchronous version for compatibility
|
| 131 |
+
"""
|
| 132 |
+
headers = {
|
| 133 |
+
"Authorization": f"Bearer {self.config.api_key}",
|
| 134 |
+
"Content-Type": "application/json"
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
payload = {
|
| 138 |
+
"model": self.config.model,
|
| 139 |
+
"messages": messages,
|
| 140 |
+
"temperature": self.config.temperature,
|
| 141 |
+
"max_tokens": self.config.max_tokens
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
start_time = time.time()
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
response = requests.post(
|
| 148 |
+
f"{self.config.base_url}/v1/chat/completions",
|
| 149 |
+
headers=headers,
|
| 150 |
+
json=payload,
|
| 151 |
+
timeout=self.config.timeout
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
response_time = time.time() - start_time
|
| 155 |
+
|
| 156 |
+
if response.status_code == 200:
|
| 157 |
+
print(f"✅ colossus Response [{agent_id}]: {response_time:.2f}s")
|
| 158 |
+
return {
|
| 159 |
+
"success": True,
|
| 160 |
+
"response": response.json(),
|
| 161 |
+
"response_time": response_time,
|
| 162 |
+
"agent_id": agent_id
|
| 163 |
+
}
|
| 164 |
+
else:
|
| 165 |
+
print(f"❌ colossus Error [{agent_id}]: {response.status_code}")
|
| 166 |
+
return {
|
| 167 |
+
"success": False,
|
| 168 |
+
"error": f"HTTP {response.status_code}: {response.text}",
|
| 169 |
+
"response_time": response_time,
|
| 170 |
+
"agent_id": agent_id
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
return {
|
| 175 |
+
"success": False,
|
| 176 |
+
"error": str(e),
|
| 177 |
+
"response_time": time.time() - start_time,
|
| 178 |
+
"agent_id": agent_id
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
# Performance Test Function
|
| 182 |
+
async def test_colossus_performance():
|
| 183 |
+
"""
|
| 184 |
+
Test colossus Server Performance
|
| 185 |
+
Target: < 2s Response Time
|
| 186 |
+
"""
|
| 187 |
+
print("🚀 SAAP colossus Performance Test Starting...")
|
| 188 |
+
|
| 189 |
+
test_messages = [
|
| 190 |
+
{"role": "system", "content": "You are Jane Alesi, lead AI architect for SAAP platform."},
|
| 191 |
+
{"role": "user", "content": "Hello Jane, please introduce yourself and explain your role in coordinating other AI agents."}
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
async with ColossusClient() as client:
|
| 195 |
+
# Single Request Test
|
| 196 |
+
result = await client.chat_completion(test_messages, agent_id="jane_alesi")
|
| 197 |
+
|
| 198 |
+
if result["success"]:
|
| 199 |
+
response_text = result["response"]["choices"][0]["message"]["content"]
|
| 200 |
+
response_time = result["response_time"]
|
| 201 |
+
|
| 202 |
+
print(f"\n📊 PERFORMANCE RESULTS:")
|
| 203 |
+
print(f"⏱️ Response Time: {response_time:.2f}s")
|
| 204 |
+
print(f"🎯 Target Met: {'✅ YES' if response_time < 2.0 else '❌ NO'}")
|
| 205 |
+
print(f"🤖 Model: {result['model']}")
|
| 206 |
+
print(f"\n💬 Response Preview:")
|
| 207 |
+
print(f"{response_text[:200]}...")
|
| 208 |
+
|
| 209 |
+
return result
|
| 210 |
+
else:
|
| 211 |
+
print(f"❌ Test Failed: {result['error']}")
|
| 212 |
+
return result
|
| 213 |
+
|
| 214 |
+
if __name__ == "__main__":
|
| 215 |
+
# Run Performance Test
|
| 216 |
+
result = asyncio.run(test_colossus_performance())
|
backend/api/cost_tracking.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP Cost Tracking API Endpoints
|
| 3 |
+
Provides real-time cost metrics and analytics for OpenRouter integration
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Dict, List, Optional, Any
|
| 8 |
+
from fastapi import APIRouter, HTTPException, Query, Depends
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
|
| 11 |
+
from ..services.cost_efficiency_logger import cost_efficiency_logger, CostAnalytics
|
| 12 |
+
from ..config.settings import get_settings
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/api/v1/cost", tags=["Cost Tracking"])
|
| 15 |
+
|
| 16 |
+
# Response Models
|
| 17 |
+
class CostSummaryResponse(BaseModel):
|
| 18 |
+
"""Cost summary response model"""
|
| 19 |
+
total_cost_usd: float = Field(..., description="Total cost in USD")
|
| 20 |
+
total_requests: int = Field(..., description="Total number of requests")
|
| 21 |
+
successful_requests: int = Field(..., description="Number of successful requests")
|
| 22 |
+
failed_requests: int = Field(..., description="Number of failed requests")
|
| 23 |
+
success_rate: float = Field(..., description="Success rate (0-1)")
|
| 24 |
+
average_cost_per_request: float = Field(..., description="Average cost per request")
|
| 25 |
+
daily_budget_used: float = Field(..., description="Daily budget utilization percentage")
|
| 26 |
+
budget_remaining_usd: float = Field(..., description="Remaining budget in USD")
|
| 27 |
+
by_provider: Dict[str, Dict[str, Any]] = Field(..., description="Cost breakdown by provider")
|
| 28 |
+
period_hours: int = Field(..., description="Time period in hours")
|
| 29 |
+
|
| 30 |
+
class CostAnalyticsResponse(BaseModel):
|
| 31 |
+
"""Comprehensive cost analytics response"""
|
| 32 |
+
time_period: str = Field(..., description="Analysis time period")
|
| 33 |
+
total_cost_usd: float = Field(..., description="Total cost")
|
| 34 |
+
total_requests: int = Field(..., description="Total requests")
|
| 35 |
+
successful_requests: int = Field(..., description="Successful requests")
|
| 36 |
+
failed_requests: int = Field(..., description="Failed requests")
|
| 37 |
+
average_cost_per_request: float = Field(..., description="Average cost per request")
|
| 38 |
+
total_tokens: int = Field(..., description="Total tokens processed")
|
| 39 |
+
average_response_time: float = Field(..., description="Average response time in seconds")
|
| 40 |
+
cost_per_1k_tokens: float = Field(..., description="Cost per 1000 tokens")
|
| 41 |
+
tokens_per_second: float = Field(..., description="Processing speed in tokens/second")
|
| 42 |
+
top_expensive_models: List[Dict[str, Any]] = Field(..., description="Most expensive models")
|
| 43 |
+
cost_by_agent: Dict[str, float] = Field(..., description="Cost breakdown by agent")
|
| 44 |
+
cost_by_provider: Dict[str, float] = Field(..., description="Cost breakdown by provider")
|
| 45 |
+
daily_budget_utilization: float = Field(..., description="Daily budget usage percentage")
|
| 46 |
+
cost_trend_24h: List[Dict[str, Any]] = Field(..., description="24-hour cost trend")
|
| 47 |
+
efficiency_score: float = Field(..., description="Cost efficiency score (tokens per dollar)")
|
| 48 |
+
|
| 49 |
+
class PerformanceBenchmarkResponse(BaseModel):
|
| 50 |
+
"""Performance benchmark response"""
|
| 51 |
+
provider: str = Field(..., description="Provider name")
|
| 52 |
+
model: str = Field(..., description="Model name")
|
| 53 |
+
avg_response_time: float = Field(..., description="Average response time")
|
| 54 |
+
tokens_per_second: float = Field(..., description="Tokens per second")
|
| 55 |
+
cost_per_token: float = Field(..., description="Cost per token")
|
| 56 |
+
success_rate: float = Field(..., description="Success rate (0-1)")
|
| 57 |
+
cost_efficiency_score: float = Field(..., description="Cost efficiency score")
|
| 58 |
+
sample_size: int = Field(..., description="Number of samples")
|
| 59 |
+
|
| 60 |
+
class BudgetStatusResponse(BaseModel):
|
| 61 |
+
"""Budget status response"""
|
| 62 |
+
daily_budget_usd: float = Field(..., description="Daily budget limit")
|
| 63 |
+
current_daily_cost: float = Field(..., description="Current daily cost")
|
| 64 |
+
budget_used_percentage: float = Field(..., description="Budget usage percentage")
|
| 65 |
+
budget_remaining_usd: float = Field(..., description="Remaining budget")
|
| 66 |
+
alert_threshold_percentage: float = Field(..., description="Alert threshold")
|
| 67 |
+
is_over_threshold: bool = Field(..., description="Whether over alert threshold")
|
| 68 |
+
is_budget_exceeded: bool = Field(..., description="Whether budget is exceeded")
|
| 69 |
+
estimated_requests_remaining: int = Field(..., description="Estimated requests remaining in budget")
|
| 70 |
+
|
| 71 |
+
# API Endpoints
|
| 72 |
+
|
| 73 |
+
@router.get("/summary", response_model=CostSummaryResponse)
|
| 74 |
+
async def get_cost_summary(
|
| 75 |
+
hours: int = Query(24, ge=1, le=168, description="Time period in hours (1-168)")
|
| 76 |
+
) -> CostSummaryResponse:
|
| 77 |
+
"""
|
| 78 |
+
Get cost summary for specified time period
|
| 79 |
+
|
| 80 |
+
Returns comprehensive cost metrics including:
|
| 81 |
+
- Total costs and request counts
|
| 82 |
+
- Success/failure rates
|
| 83 |
+
- Budget utilization
|
| 84 |
+
- Provider breakdowns
|
| 85 |
+
"""
|
| 86 |
+
try:
|
| 87 |
+
analytics = await cost_efficiency_logger.get_cost_analytics(hours)
|
| 88 |
+
daily_cost = await cost_efficiency_logger.get_daily_cost()
|
| 89 |
+
settings = get_settings()
|
| 90 |
+
|
| 91 |
+
budget_remaining = max(0, settings.agents.daily_cost_budget - daily_cost)
|
| 92 |
+
budget_used_percentage = (daily_cost / settings.agents.daily_cost_budget) * 100
|
| 93 |
+
|
| 94 |
+
# Create provider breakdown
|
| 95 |
+
by_provider = {}
|
| 96 |
+
for provider, cost in analytics.cost_by_provider.items():
|
| 97 |
+
by_provider[provider] = {
|
| 98 |
+
"cost": cost,
|
| 99 |
+
"requests": 0, # Will be populated from analytics if available
|
| 100 |
+
"tokens": 0
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return CostSummaryResponse(
|
| 104 |
+
total_cost_usd=analytics.total_cost_usd,
|
| 105 |
+
total_requests=analytics.total_requests,
|
| 106 |
+
successful_requests=analytics.successful_requests,
|
| 107 |
+
failed_requests=analytics.failed_requests,
|
| 108 |
+
success_rate=analytics.successful_requests / analytics.total_requests if analytics.total_requests > 0 else 0,
|
| 109 |
+
average_cost_per_request=analytics.average_cost_per_request,
|
| 110 |
+
daily_budget_used=budget_used_percentage,
|
| 111 |
+
budget_remaining_usd=budget_remaining,
|
| 112 |
+
by_provider=by_provider,
|
| 113 |
+
period_hours=hours
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
except Exception as e:
|
| 117 |
+
raise HTTPException(status_code=500, detail=f"Failed to retrieve cost summary: {str(e)}")
|
| 118 |
+
|
| 119 |
+
@router.get("/analytics", response_model=CostAnalyticsResponse)
|
| 120 |
+
async def get_cost_analytics(
|
| 121 |
+
hours: int = Query(24, ge=1, le=168, description="Time period in hours (1-168)")
|
| 122 |
+
) -> CostAnalyticsResponse:
|
| 123 |
+
"""
|
| 124 |
+
Get comprehensive cost analytics
|
| 125 |
+
|
| 126 |
+
Provides detailed cost analysis including:
|
| 127 |
+
- Token metrics and efficiency scores
|
| 128 |
+
- Agent and provider breakdowns
|
| 129 |
+
- Cost trends and expensive models
|
| 130 |
+
- Performance metrics
|
| 131 |
+
"""
|
| 132 |
+
try:
|
| 133 |
+
analytics = await cost_efficiency_logger.get_cost_analytics(hours)
|
| 134 |
+
|
| 135 |
+
return CostAnalyticsResponse(
|
| 136 |
+
time_period=analytics.time_period,
|
| 137 |
+
total_cost_usd=analytics.total_cost_usd,
|
| 138 |
+
total_requests=analytics.total_requests,
|
| 139 |
+
successful_requests=analytics.successful_requests,
|
| 140 |
+
failed_requests=analytics.failed_requests,
|
| 141 |
+
average_cost_per_request=analytics.average_cost_per_request,
|
| 142 |
+
total_tokens=analytics.total_tokens,
|
| 143 |
+
average_response_time=analytics.average_response_time,
|
| 144 |
+
cost_per_1k_tokens=analytics.cost_per_1k_tokens,
|
| 145 |
+
tokens_per_second=analytics.tokens_per_second,
|
| 146 |
+
top_expensive_models=analytics.top_expensive_models,
|
| 147 |
+
cost_by_agent=analytics.cost_by_agent,
|
| 148 |
+
cost_by_provider=analytics.cost_by_provider,
|
| 149 |
+
daily_budget_utilization=analytics.daily_budget_utilization,
|
| 150 |
+
cost_trend_24h=analytics.cost_trend_24h,
|
| 151 |
+
efficiency_score=analytics.efficiency_score
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
except Exception as e:
|
| 155 |
+
raise HTTPException(status_code=500, detail=f"Failed to retrieve cost analytics: {str(e)}")
|
| 156 |
+
|
| 157 |
+
@router.get("/benchmarks", response_model=List[PerformanceBenchmarkResponse])
|
| 158 |
+
async def get_performance_benchmarks(
|
| 159 |
+
hours: int = Query(24, ge=1, le=168, description="Time period in hours (1-168)")
|
| 160 |
+
) -> List[PerformanceBenchmarkResponse]:
|
| 161 |
+
"""
|
| 162 |
+
Get performance benchmarks by provider and model
|
| 163 |
+
|
| 164 |
+
Returns performance metrics for cost-efficiency analysis:
|
| 165 |
+
- Response times and processing speeds
|
| 166 |
+
- Cost per token comparisons
|
| 167 |
+
- Success rates and efficiency scores
|
| 168 |
+
"""
|
| 169 |
+
try:
|
| 170 |
+
benchmarks = await cost_efficiency_logger.get_performance_benchmarks(hours)
|
| 171 |
+
|
| 172 |
+
return [
|
| 173 |
+
PerformanceBenchmarkResponse(
|
| 174 |
+
provider=benchmark.provider,
|
| 175 |
+
model=benchmark.model,
|
| 176 |
+
avg_response_time=benchmark.avg_response_time,
|
| 177 |
+
tokens_per_second=benchmark.tokens_per_second,
|
| 178 |
+
cost_per_token=benchmark.cost_per_token,
|
| 179 |
+
success_rate=benchmark.success_rate,
|
| 180 |
+
cost_efficiency_score=benchmark.cost_efficiency_score,
|
| 181 |
+
sample_size=benchmark.sample_size
|
| 182 |
+
)
|
| 183 |
+
for benchmark in benchmarks
|
| 184 |
+
]
|
| 185 |
+
|
| 186 |
+
except Exception as e:
|
| 187 |
+
raise HTTPException(status_code=500, detail=f"Failed to retrieve performance benchmarks: {str(e)}")
|
| 188 |
+
|
| 189 |
+
@router.get("/budget", response_model=BudgetStatusResponse)
|
| 190 |
+
async def get_budget_status() -> BudgetStatusResponse:
|
| 191 |
+
"""
|
| 192 |
+
Get current budget status and utilization
|
| 193 |
+
|
| 194 |
+
Provides real-time budget monitoring:
|
| 195 |
+
- Daily budget limits and usage
|
| 196 |
+
- Alert thresholds and warnings
|
| 197 |
+
- Estimated remaining capacity
|
| 198 |
+
"""
|
| 199 |
+
try:
|
| 200 |
+
settings = get_settings()
|
| 201 |
+
daily_cost = await cost_efficiency_logger.get_daily_cost()
|
| 202 |
+
|
| 203 |
+
daily_budget = settings.agents.daily_cost_budget
|
| 204 |
+
budget_used_percentage = (daily_cost / daily_budget) * 100
|
| 205 |
+
budget_remaining = max(0, daily_budget - daily_cost)
|
| 206 |
+
alert_threshold = settings.agents.warning_cost_threshold
|
| 207 |
+
|
| 208 |
+
is_over_threshold = budget_used_percentage >= (alert_threshold * 100)
|
| 209 |
+
is_budget_exceeded = daily_cost >= daily_budget
|
| 210 |
+
|
| 211 |
+
# Estimate remaining requests based on average cost
|
| 212 |
+
analytics = await cost_efficiency_logger.get_cost_analytics(24)
|
| 213 |
+
avg_cost_per_request = analytics.average_cost_per_request
|
| 214 |
+
|
| 215 |
+
estimated_requests_remaining = 0
|
| 216 |
+
if avg_cost_per_request > 0 and budget_remaining > 0:
|
| 217 |
+
estimated_requests_remaining = int(budget_remaining / avg_cost_per_request)
|
| 218 |
+
|
| 219 |
+
return BudgetStatusResponse(
|
| 220 |
+
daily_budget_usd=daily_budget,
|
| 221 |
+
current_daily_cost=daily_cost,
|
| 222 |
+
budget_used_percentage=budget_used_percentage,
|
| 223 |
+
budget_remaining_usd=budget_remaining,
|
| 224 |
+
alert_threshold_percentage=alert_threshold * 100,
|
| 225 |
+
is_over_threshold=is_over_threshold,
|
| 226 |
+
is_budget_exceeded=is_budget_exceeded,
|
| 227 |
+
estimated_requests_remaining=estimated_requests_remaining
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
raise HTTPException(status_code=500, detail=f"Failed to retrieve budget status: {str(e)}")
|
| 232 |
+
|
| 233 |
+
@router.get("/report")
|
| 234 |
+
async def get_cost_report(
|
| 235 |
+
hours: int = Query(24, ge=1, le=168, description="Time period in hours (1-168)")
|
| 236 |
+
) -> Dict[str, str]:
|
| 237 |
+
"""
|
| 238 |
+
Generate detailed cost efficiency report
|
| 239 |
+
|
| 240 |
+
Returns a formatted text report with:
|
| 241 |
+
- Cost summaries and token metrics
|
| 242 |
+
- Provider and agent breakdowns
|
| 243 |
+
- Performance benchmarks
|
| 244 |
+
- Efficiency recommendations
|
| 245 |
+
"""
|
| 246 |
+
try:
|
| 247 |
+
report = await cost_efficiency_logger.generate_cost_report(hours)
|
| 248 |
+
|
| 249 |
+
return {
|
| 250 |
+
"report": report,
|
| 251 |
+
"generated_at": datetime.now().isoformat(),
|
| 252 |
+
"time_period_hours": hours
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
except Exception as e:
|
| 256 |
+
raise HTTPException(status_code=500, detail=f"Failed to generate cost report: {str(e)}")
|
| 257 |
+
|
| 258 |
+
@router.post("/reset-daily")
|
| 259 |
+
async def reset_daily_costs() -> Dict[str, str]:
|
| 260 |
+
"""
|
| 261 |
+
Reset daily cost tracking (admin function)
|
| 262 |
+
|
| 263 |
+
Should be called at midnight to reset daily budgets and alerts.
|
| 264 |
+
"""
|
| 265 |
+
try:
|
| 266 |
+
# Get current daily cost before reset
|
| 267 |
+
current_daily_cost = await cost_efficiency_logger.get_daily_cost()
|
| 268 |
+
|
| 269 |
+
# Reset alerts (cost tracking reset should be handled by the enhanced agent manager)
|
| 270 |
+
cost_efficiency_logger.reset_daily_alerts()
|
| 271 |
+
|
| 272 |
+
return {
|
| 273 |
+
"message": "Daily costs and alerts reset successfully",
|
| 274 |
+
"previous_daily_cost": f"${current_daily_cost:.6f}",
|
| 275 |
+
"reset_at": datetime.now().isoformat()
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
except Exception as e:
|
| 279 |
+
raise HTTPException(status_code=500, detail=f"Failed to reset daily costs: {str(e)}")
|
| 280 |
+
|
| 281 |
+
@router.delete("/cleanup")
|
| 282 |
+
async def cleanup_old_data(
|
| 283 |
+
days_to_keep: int = Query(30, ge=7, le=365, description="Days of data to keep (7-365)")
|
| 284 |
+
) -> Dict[str, str]:
|
| 285 |
+
"""
|
| 286 |
+
Clean up old cost tracking data
|
| 287 |
+
|
| 288 |
+
Removes cost records older than specified days to manage database size.
|
| 289 |
+
"""
|
| 290 |
+
try:
|
| 291 |
+
await cost_efficiency_logger.cleanup_old_data(days_to_keep)
|
| 292 |
+
|
| 293 |
+
return {
|
| 294 |
+
"message": f"Old cost data cleanup completed",
|
| 295 |
+
"days_kept": days_to_keep,
|
| 296 |
+
"cleanup_at": datetime.now().isoformat()
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
except Exception as e:
|
| 300 |
+
raise HTTPException(status_code=500, detail=f"Failed to cleanup old data: {str(e)}")
|
| 301 |
+
|
| 302 |
+
# WebSocket endpoint for real-time cost monitoring would go here
|
| 303 |
+
# This could stream live cost updates to the frontend dashboard
|
backend/api/hybrid_endpoints.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hybrid API Endpoints for SAAP OpenRouter Integration
|
| 3 |
+
Additional endpoints to support multi-provider functionality
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 7 |
+
from typing import Dict, Optional, Any
|
| 8 |
+
import logging
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
from services.agent_manager_hybrid import HybridAgentManagerService
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
# Router for hybrid endpoints
|
| 16 |
+
hybrid_router = APIRouter(prefix="/api/v1/hybrid", tags=["hybrid"])
|
| 17 |
+
|
| 18 |
+
def get_hybrid_manager() -> HybridAgentManagerService:
|
| 19 |
+
"""Dependency to get hybrid agent manager (if available)"""
|
| 20 |
+
# This will be injected by main.py if hybrid mode is enabled
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
# =====================================================
|
| 24 |
+
# PROVIDER COMPARISON & PERFORMANCE ENDPOINTS
|
| 25 |
+
# =====================================================
|
| 26 |
+
|
| 27 |
+
@hybrid_router.post("/agents/{agent_id}/compare")
|
| 28 |
+
async def compare_providers(
|
| 29 |
+
agent_id: str,
|
| 30 |
+
message_data: Dict[str, str],
|
| 31 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 32 |
+
):
|
| 33 |
+
"""
|
| 34 |
+
🆚 Send same message to both colossus and OpenRouter for comparison
|
| 35 |
+
Useful for benchmarking performance and cost analysis
|
| 36 |
+
"""
|
| 37 |
+
if not hybrid_manager:
|
| 38 |
+
raise HTTPException(status_code=503, detail="Hybrid mode not enabled")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
message = message_data.get("message", "")
|
| 42 |
+
if not message:
|
| 43 |
+
raise HTTPException(status_code=400, detail="Message content required")
|
| 44 |
+
|
| 45 |
+
logger.info(f"📊 Provider comparison requested for {agent_id}")
|
| 46 |
+
|
| 47 |
+
# Send to both providers
|
| 48 |
+
comparison = await hybrid_manager.compare_providers(agent_id, message)
|
| 49 |
+
|
| 50 |
+
if "error" in comparison:
|
| 51 |
+
return {
|
| 52 |
+
"success": False,
|
| 53 |
+
"error": comparison["error"],
|
| 54 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
logger.info(f"✅ Provider comparison completed for {agent_id}")
|
| 58 |
+
|
| 59 |
+
return {
|
| 60 |
+
"success": True,
|
| 61 |
+
"comparison": comparison,
|
| 62 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logger.error(f"❌ Provider comparison error: {e}")
|
| 67 |
+
raise HTTPException(status_code=500, detail=f"Comparison failed: {str(e)}")
|
| 68 |
+
|
| 69 |
+
@hybrid_router.get("/stats/providers")
|
| 70 |
+
async def get_provider_statistics(
|
| 71 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 72 |
+
):
|
| 73 |
+
"""
|
| 74 |
+
📊 Get comprehensive provider performance statistics
|
| 75 |
+
Returns success rates, response times, and cost data
|
| 76 |
+
"""
|
| 77 |
+
if not hybrid_manager:
|
| 78 |
+
raise HTTPException(status_code=503, detail="Hybrid mode not enabled")
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
stats = hybrid_manager.get_provider_stats()
|
| 82 |
+
|
| 83 |
+
return {
|
| 84 |
+
"success": True,
|
| 85 |
+
"statistics": stats,
|
| 86 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logger.error(f"❌ Provider stats error: {e}")
|
| 91 |
+
raise HTTPException(status_code=500, detail=f"Statistics failed: {str(e)}")
|
| 92 |
+
|
| 93 |
+
@hybrid_router.get("/costs/openrouter")
|
| 94 |
+
async def get_openrouter_costs(
|
| 95 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 96 |
+
):
|
| 97 |
+
"""
|
| 98 |
+
💰 Get OpenRouter cost summary and budget status
|
| 99 |
+
"""
|
| 100 |
+
if not hybrid_manager:
|
| 101 |
+
raise HTTPException(status_code=503, detail="Hybrid mode not enabled")
|
| 102 |
+
|
| 103 |
+
if not hybrid_manager.openrouter_client:
|
| 104 |
+
raise HTTPException(status_code=503, detail="OpenRouter client not available")
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
cost_summary = hybrid_manager.openrouter_client.get_cost_summary()
|
| 108 |
+
|
| 109 |
+
return {
|
| 110 |
+
"success": True,
|
| 111 |
+
"costs": cost_summary,
|
| 112 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"❌ OpenRouter cost error: {e}")
|
| 117 |
+
raise HTTPException(status_code=500, detail=f"Cost summary failed: {str(e)}")
|
| 118 |
+
|
| 119 |
+
# =====================================================
|
| 120 |
+
# PROVIDER SWITCHING & CONFIGURATION
|
| 121 |
+
# =====================================================
|
| 122 |
+
|
| 123 |
+
@hybrid_router.post("/config/primary-provider")
|
| 124 |
+
async def set_primary_provider(
|
| 125 |
+
config_data: Dict[str, str],
|
| 126 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 127 |
+
):
|
| 128 |
+
"""
|
| 129 |
+
🔄 Switch primary provider (colossus/openrouter)
|
| 130 |
+
"""
|
| 131 |
+
if not hybrid_manager:
|
| 132 |
+
raise HTTPException(status_code=503, detail="Hybrid mode not enabled")
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
provider = config_data.get("provider", "")
|
| 136 |
+
if provider not in ["colossus", "openrouter"]:
|
| 137 |
+
raise HTTPException(status_code=400, detail="Provider must be 'colossus' or 'openrouter'")
|
| 138 |
+
|
| 139 |
+
success = await hybrid_manager.set_primary_provider(provider)
|
| 140 |
+
|
| 141 |
+
if success:
|
| 142 |
+
return {
|
| 143 |
+
"success": True,
|
| 144 |
+
"message": f"Primary provider set to {provider}",
|
| 145 |
+
"provider": provider,
|
| 146 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 147 |
+
}
|
| 148 |
+
else:
|
| 149 |
+
raise HTTPException(status_code=400, detail=f"Failed to switch to {provider}")
|
| 150 |
+
|
| 151 |
+
except HTTPException:
|
| 152 |
+
raise
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.error(f"❌ Provider switch error: {e}")
|
| 155 |
+
raise HTTPException(status_code=500, detail=f"Provider switch failed: {str(e)}")
|
| 156 |
+
|
| 157 |
+
@hybrid_router.get("/config/status")
|
| 158 |
+
async def get_hybrid_status(
|
| 159 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 160 |
+
):
|
| 161 |
+
"""
|
| 162 |
+
ℹ️ Get hybrid system configuration and status
|
| 163 |
+
"""
|
| 164 |
+
if not hybrid_manager:
|
| 165 |
+
return {
|
| 166 |
+
"hybrid_enabled": False,
|
| 167 |
+
"message": "Hybrid mode not available",
|
| 168 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
try:
|
| 172 |
+
# Check provider availability
|
| 173 |
+
providers_status = {
|
| 174 |
+
"colossus": {
|
| 175 |
+
"available": hybrid_manager.colossus_client is not None,
|
| 176 |
+
"status": hybrid_manager.colossus_connection_status if hasattr(hybrid_manager, 'colossus_connection_status') else "unknown"
|
| 177 |
+
},
|
| 178 |
+
"openrouter": {
|
| 179 |
+
"available": hybrid_manager.openrouter_client is not None,
|
| 180 |
+
"status": "unknown"
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
# Test OpenRouter if available
|
| 185 |
+
if hybrid_manager.openrouter_client:
|
| 186 |
+
try:
|
| 187 |
+
or_health = await hybrid_manager.openrouter_client.health_check()
|
| 188 |
+
providers_status["openrouter"]["status"] = or_health.get("status", "unknown")
|
| 189 |
+
providers_status["openrouter"]["daily_cost"] = or_health.get("daily_cost", 0)
|
| 190 |
+
providers_status["openrouter"]["budget_remaining"] = or_health.get("budget_remaining", 0)
|
| 191 |
+
except Exception as e:
|
| 192 |
+
providers_status["openrouter"]["status"] = f"error: {e}"
|
| 193 |
+
|
| 194 |
+
return {
|
| 195 |
+
"hybrid_enabled": True,
|
| 196 |
+
"primary_provider": hybrid_manager.primary_provider,
|
| 197 |
+
"failover_enabled": hybrid_manager.enable_failover,
|
| 198 |
+
"cost_comparison_enabled": hybrid_manager.enable_cost_comparison,
|
| 199 |
+
"providers": providers_status,
|
| 200 |
+
"loaded_agents": len(hybrid_manager.agents),
|
| 201 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
except Exception as e:
|
| 205 |
+
logger.error(f"❌ Hybrid status error: {e}")
|
| 206 |
+
raise HTTPException(status_code=500, detail=f"Status check failed: {str(e)}")
|
| 207 |
+
|
| 208 |
+
# =====================================================
|
| 209 |
+
# OPTIONAL: DIRECT PROVIDER ENDPOINTS
|
| 210 |
+
# =====================================================
|
| 211 |
+
|
| 212 |
+
@hybrid_router.post("/agents/{agent_id}/chat/colossus")
|
| 213 |
+
async def chat_with_colossus(
|
| 214 |
+
agent_id: str,
|
| 215 |
+
message_data: Dict[str, str],
|
| 216 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 217 |
+
):
|
| 218 |
+
"""
|
| 219 |
+
🤖 Direct chat with agent via colossus (bypass primary provider setting)
|
| 220 |
+
"""
|
| 221 |
+
if not hybrid_manager:
|
| 222 |
+
raise HTTPException(status_code=503, detail="Hybrid mode not enabled")
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
message = message_data.get("message", "")
|
| 226 |
+
if not message:
|
| 227 |
+
raise HTTPException(status_code=400, detail="Message content required")
|
| 228 |
+
|
| 229 |
+
# Force colossus provider
|
| 230 |
+
response = await hybrid_manager.send_message_to_agent(agent_id, message, "colossus")
|
| 231 |
+
|
| 232 |
+
if "error" in response:
|
| 233 |
+
return {
|
| 234 |
+
"success": False,
|
| 235 |
+
"error": response["error"],
|
| 236 |
+
"provider": "colossus",
|
| 237 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
return {
|
| 241 |
+
"success": True,
|
| 242 |
+
"agent_id": agent_id,
|
| 243 |
+
"message": message,
|
| 244 |
+
"response": response,
|
| 245 |
+
"provider": "colossus",
|
| 246 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
except Exception as e:
|
| 250 |
+
logger.error(f"❌ colossus chat error: {e}")
|
| 251 |
+
raise HTTPException(status_code=500, detail=f"colossus chat failed: {str(e)}")
|
| 252 |
+
|
| 253 |
+
@hybrid_router.post("/agents/{agent_id}/chat/openrouter")
|
| 254 |
+
async def chat_with_openrouter(
|
| 255 |
+
agent_id: str,
|
| 256 |
+
message_data: Dict[str, str],
|
| 257 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 258 |
+
):
|
| 259 |
+
"""
|
| 260 |
+
🌐 Direct chat with agent via OpenRouter (bypass primary provider setting)
|
| 261 |
+
"""
|
| 262 |
+
if not hybrid_manager:
|
| 263 |
+
raise HTTPException(status_code=503, detail="Hybrid mode not enabled")
|
| 264 |
+
|
| 265 |
+
if not hybrid_manager.openrouter_client:
|
| 266 |
+
raise HTTPException(status_code=503, detail="OpenRouter client not available")
|
| 267 |
+
|
| 268 |
+
try:
|
| 269 |
+
message = message_data.get("message", "")
|
| 270 |
+
if not message:
|
| 271 |
+
raise HTTPException(status_code=400, detail="Message content required")
|
| 272 |
+
|
| 273 |
+
# Force OpenRouter provider
|
| 274 |
+
response = await hybrid_manager.send_message_to_agent(agent_id, message, "openrouter")
|
| 275 |
+
|
| 276 |
+
if "error" in response:
|
| 277 |
+
return {
|
| 278 |
+
"success": False,
|
| 279 |
+
"error": response["error"],
|
| 280 |
+
"provider": "openrouter",
|
| 281 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
return {
|
| 285 |
+
"success": True,
|
| 286 |
+
"agent_id": agent_id,
|
| 287 |
+
"message": message,
|
| 288 |
+
"response": response,
|
| 289 |
+
"provider": "openrouter",
|
| 290 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
except Exception as e:
|
| 294 |
+
logger.error(f"❌ OpenRouter chat error: {e}")
|
| 295 |
+
raise HTTPException(status_code=500, detail=f"OpenRouter chat failed: {str(e)}")
|
| 296 |
+
|
| 297 |
+
# =====================================================
|
| 298 |
+
# HEALTH CHECK FOR HYBRID SYSTEM
|
| 299 |
+
# =====================================================
|
| 300 |
+
|
| 301 |
+
@hybrid_router.get("/health")
|
| 302 |
+
async def hybrid_health_check(
|
| 303 |
+
hybrid_manager: HybridAgentManagerService = Depends(get_hybrid_manager)
|
| 304 |
+
):
|
| 305 |
+
"""
|
| 306 |
+
🏥 Comprehensive health check for hybrid system
|
| 307 |
+
"""
|
| 308 |
+
if not hybrid_manager:
|
| 309 |
+
return {
|
| 310 |
+
"status": "hybrid_disabled",
|
| 311 |
+
"message": "Hybrid mode not enabled - using standard mode",
|
| 312 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
try:
|
| 316 |
+
health_status = {
|
| 317 |
+
"status": "healthy",
|
| 318 |
+
"providers": {},
|
| 319 |
+
"agents_loaded": len(hybrid_manager.agents),
|
| 320 |
+
"primary_provider": hybrid_manager.primary_provider,
|
| 321 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
# Check colossus
|
| 325 |
+
if hybrid_manager.colossus_client:
|
| 326 |
+
try:
|
| 327 |
+
# Test colossus connection if method available
|
| 328 |
+
if hasattr(hybrid_manager, '_test_colossus_connection'):
|
| 329 |
+
await hybrid_manager._test_colossus_connection()
|
| 330 |
+
health_status["providers"]["colossus"] = {
|
| 331 |
+
"status": getattr(hybrid_manager, 'colossus_connection_status', 'unknown'),
|
| 332 |
+
"available": True
|
| 333 |
+
}
|
| 334 |
+
except Exception as e:
|
| 335 |
+
health_status["providers"]["colossus"] = {
|
| 336 |
+
"status": f"error: {e}",
|
| 337 |
+
"available": False
|
| 338 |
+
}
|
| 339 |
+
else:
|
| 340 |
+
health_status["providers"]["colossus"] = {
|
| 341 |
+
"status": "not_configured",
|
| 342 |
+
"available": False
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
# Check OpenRouter
|
| 346 |
+
if hybrid_manager.openrouter_client:
|
| 347 |
+
try:
|
| 348 |
+
or_health = await hybrid_manager.openrouter_client.health_check()
|
| 349 |
+
health_status["providers"]["openrouter"] = {
|
| 350 |
+
"status": or_health.get("status", "unknown"),
|
| 351 |
+
"available": or_health.get("status") == "healthy",
|
| 352 |
+
"daily_cost": or_health.get("daily_cost", 0),
|
| 353 |
+
"budget_remaining": or_health.get("budget_remaining", 0)
|
| 354 |
+
}
|
| 355 |
+
except Exception as e:
|
| 356 |
+
health_status["providers"]["openrouter"] = {
|
| 357 |
+
"status": f"error: {e}",
|
| 358 |
+
"available": False
|
| 359 |
+
}
|
| 360 |
+
else:
|
| 361 |
+
health_status["providers"]["openrouter"] = {
|
| 362 |
+
"status": "not_configured",
|
| 363 |
+
"available": False
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
# Overall health status
|
| 367 |
+
provider_count = sum(1 for p in health_status["providers"].values() if p["available"])
|
| 368 |
+
if provider_count == 0:
|
| 369 |
+
health_status["status"] = "unhealthy"
|
| 370 |
+
health_status["message"] = "No providers available"
|
| 371 |
+
elif provider_count == 1:
|
| 372 |
+
health_status["status"] = "degraded"
|
| 373 |
+
health_status["message"] = "Only one provider available"
|
| 374 |
+
else:
|
| 375 |
+
health_status["status"] = "healthy"
|
| 376 |
+
health_status["message"] = "All providers operational"
|
| 377 |
+
|
| 378 |
+
return health_status
|
| 379 |
+
|
| 380 |
+
except Exception as e:
|
| 381 |
+
logger.error(f"❌ Hybrid health check error: {e}")
|
| 382 |
+
return {
|
| 383 |
+
"status": "error",
|
| 384 |
+
"error": str(e),
|
| 385 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
# Export router for main.py integration
|
| 389 |
+
__all__ = ["hybrid_router", "get_hybrid_manager"]
|
backend/api/multi_agent_endpoints.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Multi-Agent Communication API Endpoints for SAAP Platform
|
| 4 |
+
Provides REST API interface for multi-agent coordination and task delegation
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 8 |
+
from typing import Dict, Any, Optional, List
|
| 9 |
+
import logging
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
|
| 13 |
+
from services.multi_agent_coordinator import MultiAgentCoordinator, TaskPriority, get_coordinator
|
| 14 |
+
|
| 15 |
+
# Configure logging
|
| 16 |
+
logging.basicConfig(level=logging.INFO)
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
# Create router for multi-agent endpoints
|
| 20 |
+
multi_agent_router = APIRouter(prefix="/api/v1/multi-agent", tags=["Multi-Agent Communication"])
|
| 21 |
+
|
| 22 |
+
# 🔒 PRIVACY DETECTION HELPER
|
| 23 |
+
async def _determine_provider(user_message: str, provider_preference: Optional[str] = None) -> Dict[str, Any]:
|
| 24 |
+
"""
|
| 25 |
+
🔒 Autonomous Privacy-First Provider Selection
|
| 26 |
+
|
| 27 |
+
Analyzes user message for sensitive data and automatically selects appropriate provider:
|
| 28 |
+
- Colossus (internal): For sensitive/personal data (GDPR-protected)
|
| 29 |
+
- OpenRouter (external): For general queries (faster, cost-efficient)
|
| 30 |
+
|
| 31 |
+
Detection Categories:
|
| 32 |
+
- Medical: Patient data, diagnoses, symptoms, medications
|
| 33 |
+
- Financial: Account numbers, IBANs, credit cards, financial details
|
| 34 |
+
- Personal: Birthdates, addresses, phone numbers, ID numbers
|
| 35 |
+
- Legal: Legal cases, confidential information
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
Dict with 'provider', 'reason', 'detected_categories', 'confidence'
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
# Force provider if explicitly requested
|
| 42 |
+
if provider_preference and provider_preference != "auto":
|
| 43 |
+
logger.info(f"🔒 Provider forced by user: {provider_preference}")
|
| 44 |
+
return {
|
| 45 |
+
"provider": provider_preference,
|
| 46 |
+
"reason": "User preference",
|
| 47 |
+
"selected_provider": provider_preference,
|
| 48 |
+
"detected_categories": [],
|
| 49 |
+
"confidence": "explicit",
|
| 50 |
+
"auto_detected": False
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# Sensitive keyword detection
|
| 54 |
+
message_lower = user_message.lower()
|
| 55 |
+
detected_categories = []
|
| 56 |
+
|
| 57 |
+
# Medical keywords (German + English)
|
| 58 |
+
medical_keywords = [
|
| 59 |
+
'patient', 'diagnose', 'diagnosis', 'krankheit', 'disease', 'symptom',
|
| 60 |
+
'arzt', 'doctor', 'medikament', 'medication', 'therapie', 'therapy',
|
| 61 |
+
'behandlung', 'treatment', 'gesundheit', 'health', 'krankenhaus', 'hospital',
|
| 62 |
+
'medizin', 'medicine', 'blut', 'blood', 'labor', 'test'
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
# Financial keywords
|
| 66 |
+
financial_keywords = [
|
| 67 |
+
'konto', 'account', 'iban', 'bic', 'kreditkarte', 'credit card',
|
| 68 |
+
'gehalt', 'salary', 'steuer', 'tax', 'bank', 'überweisung', 'transfer',
|
| 69 |
+
'rechnung', 'invoice', 'zahlung', 'payment', 'finanz', 'financial'
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
# Personal data keywords
|
| 73 |
+
personal_keywords = [
|
| 74 |
+
'geburtsdatum', 'birthdate', 'adresse', 'address', 'telefon', 'phone',
|
| 75 |
+
'ausweis', 'id', 'pass', 'passport', 'sozialversicherung', 'social security',
|
| 76 |
+
'privat', 'private', 'persönlich', 'personal'
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
# Legal keywords
|
| 80 |
+
legal_keywords = [
|
| 81 |
+
'vertrag', 'contract', 'klage', 'lawsuit', 'anwalt', 'lawyer',
|
| 82 |
+
'gericht', 'court', 'urteil', 'verdict', 'rechtlich', 'legal',
|
| 83 |
+
'compliance', 'datenschutz', 'gdpr', 'dsgvo'
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
# Check for sensitive keywords
|
| 87 |
+
if any(keyword in message_lower for keyword in medical_keywords):
|
| 88 |
+
detected_categories.append('medical')
|
| 89 |
+
|
| 90 |
+
if any(keyword in message_lower for keyword in financial_keywords):
|
| 91 |
+
detected_categories.append('financial')
|
| 92 |
+
|
| 93 |
+
if any(keyword in message_lower for keyword in personal_keywords):
|
| 94 |
+
detected_categories.append('personal')
|
| 95 |
+
|
| 96 |
+
if any(keyword in message_lower for keyword in legal_keywords):
|
| 97 |
+
detected_categories.append('legal')
|
| 98 |
+
|
| 99 |
+
# Pattern-based detection (PII)
|
| 100 |
+
import re
|
| 101 |
+
|
| 102 |
+
# Email pattern
|
| 103 |
+
if re.search(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', user_message):
|
| 104 |
+
detected_categories.append('email')
|
| 105 |
+
|
| 106 |
+
# Phone number pattern (German + International)
|
| 107 |
+
if re.search(r'\b(?:\+49|0)\s?\d{3,4}\s?\d{6,8}\b', user_message):
|
| 108 |
+
detected_categories.append('phone')
|
| 109 |
+
|
| 110 |
+
# IBAN pattern
|
| 111 |
+
if re.search(r'\b[A-Z]{2}\d{2}[A-Z0-9]{13,29}\b', user_message):
|
| 112 |
+
detected_categories.append('iban')
|
| 113 |
+
|
| 114 |
+
# Credit card pattern
|
| 115 |
+
if re.search(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b', user_message):
|
| 116 |
+
detected_categories.append('credit_card')
|
| 117 |
+
|
| 118 |
+
# Decision logic
|
| 119 |
+
if detected_categories:
|
| 120 |
+
# Sensitive data detected → Force Colossus
|
| 121 |
+
logger.warning(f"🔒 SENSITIVE DATA DETECTED: {detected_categories} → Colossus enforced")
|
| 122 |
+
return {
|
| 123 |
+
"provider": "colossus",
|
| 124 |
+
"reason": f"Sensitive data protection ({', '.join(detected_categories)})",
|
| 125 |
+
"selected_provider": "colossus",
|
| 126 |
+
"detected_categories": detected_categories,
|
| 127 |
+
"confidence": "high",
|
| 128 |
+
"auto_detected": True,
|
| 129 |
+
"privacy_level": "high"
|
| 130 |
+
}
|
| 131 |
+
else:
|
| 132 |
+
# No sensitive data → OpenRouter for speed/efficiency
|
| 133 |
+
logger.info("🌐 No sensitive data → OpenRouter selected")
|
| 134 |
+
return {
|
| 135 |
+
"provider": "openrouter",
|
| 136 |
+
"reason": "General query - optimized for speed",
|
| 137 |
+
"selected_provider": "openrouter",
|
| 138 |
+
"detected_categories": [],
|
| 139 |
+
"confidence": "high",
|
| 140 |
+
"auto_detected": True,
|
| 141 |
+
"privacy_level": "standard"
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class MultiAgentChatRequest(BaseModel):
|
| 146 |
+
user_message: str
|
| 147 |
+
user_context: Optional[Dict[str, Any]] = None
|
| 148 |
+
preferred_agent: Optional[str] = None
|
| 149 |
+
task_priority: TaskPriority = TaskPriority.NORMAL
|
| 150 |
+
provider: Optional[str] = None # "auto", "colossus", "openrouter"
|
| 151 |
+
privacy_mode: Optional[str] = None # Alias for provider
|
| 152 |
+
|
| 153 |
+
class MultiAgentChatResponse(BaseModel):
|
| 154 |
+
success: bool
|
| 155 |
+
coordinator_response: str
|
| 156 |
+
delegated_agent: Optional[str] = None
|
| 157 |
+
specialist_response: Optional[str] = None
|
| 158 |
+
coordination_chain: List[str] = []
|
| 159 |
+
processing_time: float = 0.0
|
| 160 |
+
workflow_type: str = "single_agent"
|
| 161 |
+
task_id: Optional[str] = None
|
| 162 |
+
cost_info: Optional[Dict[str, Any]] = None
|
| 163 |
+
privacy_protection: Optional[Dict[str, Any]] = None # Privacy info
|
| 164 |
+
error: Optional[str] = None
|
| 165 |
+
|
| 166 |
+
@multi_agent_router.post("/chat", response_model=MultiAgentChatResponse)
|
| 167 |
+
async def multi_agent_chat(
|
| 168 |
+
request: MultiAgentChatRequest,
|
| 169 |
+
coordinator: MultiAgentCoordinator = Depends(get_coordinator)
|
| 170 |
+
):
|
| 171 |
+
"""
|
| 172 |
+
🤖 Multi-Agent Chat Endpoint - Jane Alesi Master Coordinator
|
| 173 |
+
|
| 174 |
+
Automatically analyzes user intent and either:
|
| 175 |
+
1. Handles request directly (Jane as coordinator)
|
| 176 |
+
2. Delegates to appropriate specialist agent
|
| 177 |
+
3. Orchestrates multi-agent workflow for complex tasks
|
| 178 |
+
|
| 179 |
+
Examples:
|
| 180 |
+
- "Entwickle eine Python App" → Jane delegates to John Alesi (Development)
|
| 181 |
+
- "Medizinische Beratung für Diabetes" → Jane delegates to Lara Alesi (Medical)
|
| 182 |
+
- "Legal Compliance Check" → Jane delegates to Justus Alesi (Legal)
|
| 183 |
+
- "SAAP Platform Status" → Jane handles directly as Coordinator
|
| 184 |
+
"""
|
| 185 |
+
start_time = datetime.now()
|
| 186 |
+
|
| 187 |
+
try:
|
| 188 |
+
logger.info(f"🤖 Multi-Agent Chat Request: {request.user_message[:100]}...")
|
| 189 |
+
|
| 190 |
+
# 🔒 PRIVACY-FIRST: Determine provider based on sensitivity
|
| 191 |
+
selected_provider = await _determine_provider(
|
| 192 |
+
user_message=request.user_message,
|
| 193 |
+
provider_preference=request.provider or request.privacy_mode
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
logger.info(f"🔒 Provider selection: {selected_provider['provider']} (reason: {selected_provider['reason']})")
|
| 197 |
+
|
| 198 |
+
# Execute multi-agent coordination with provider selection
|
| 199 |
+
coordination_result = await coordinator.coordinate_multi_agent_task(
|
| 200 |
+
user_message=request.user_message,
|
| 201 |
+
user_context=request.user_context or {},
|
| 202 |
+
provider=selected_provider['provider'] # Pass provider to coordinator
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
# Add privacy protection info to result
|
| 206 |
+
coordination_result['privacy_protection'] = selected_provider
|
| 207 |
+
|
| 208 |
+
processing_time = (datetime.now() - start_time).total_seconds()
|
| 209 |
+
|
| 210 |
+
if coordination_result.get("success", False):
|
| 211 |
+
# Successful coordination
|
| 212 |
+
workflow_type = coordination_result.get("workflow_type", "single_agent")
|
| 213 |
+
|
| 214 |
+
if workflow_type == "multi_agent":
|
| 215 |
+
# Complex multi-agent workflow
|
| 216 |
+
specialists = coordination_result.get("specialists", [])
|
| 217 |
+
workflow_steps = coordination_result.get("workflow_steps", [])
|
| 218 |
+
|
| 219 |
+
# Build coordination chain
|
| 220 |
+
coordination_chain = ["jane_alesi"] # Jane always starts
|
| 221 |
+
coordination_chain.extend(specialists)
|
| 222 |
+
|
| 223 |
+
# Get final response from synthesis step
|
| 224 |
+
coordinator_response = coordination_result.get("final_response", "Multi-agent workflow completed successfully.")
|
| 225 |
+
|
| 226 |
+
# Get specialist response (first specialist for simplicity)
|
| 227 |
+
specialist_response = None
|
| 228 |
+
delegated_agent = None
|
| 229 |
+
if workflow_steps:
|
| 230 |
+
for step in workflow_steps:
|
| 231 |
+
if step.get("step") == "specialist_analysis":
|
| 232 |
+
delegated_agent = step.get("agent")
|
| 233 |
+
specialist_response = step.get("result", {}).get("response", "Specialist analysis completed.")
|
| 234 |
+
break
|
| 235 |
+
|
| 236 |
+
logger.info(f"✅ Multi-Agent Workflow: {len(workflow_steps)} steps, {len(specialists)} specialists")
|
| 237 |
+
|
| 238 |
+
return MultiAgentChatResponse(
|
| 239 |
+
success=True,
|
| 240 |
+
coordinator_response=coordinator_response,
|
| 241 |
+
delegated_agent=delegated_agent,
|
| 242 |
+
specialist_response=specialist_response,
|
| 243 |
+
coordination_chain=coordination_chain,
|
| 244 |
+
processing_time=processing_time,
|
| 245 |
+
workflow_type="multi_agent",
|
| 246 |
+
task_id=coordination_result.get("task_id"),
|
| 247 |
+
cost_info={
|
| 248 |
+
"total_cost": 0.0, # Multi-agent coordination is free
|
| 249 |
+
"task_count": coordination_result.get("task_count", 1),
|
| 250 |
+
"agents_involved": len(coordination_chain)
|
| 251 |
+
},
|
| 252 |
+
privacy_protection=coordination_result.get('privacy_protection')
|
| 253 |
+
)
|
| 254 |
+
else:
|
| 255 |
+
# Single agent delegation
|
| 256 |
+
primary_agent = coordination_result.get("primary_agent", "jane_alesi")
|
| 257 |
+
response_text = coordination_result.get("response", "Task completed successfully.")
|
| 258 |
+
|
| 259 |
+
# Determine coordination chain
|
| 260 |
+
coordination_chain = ["jane_alesi"] # Jane analyzes intent
|
| 261 |
+
if primary_agent != "jane_alesi":
|
| 262 |
+
coordination_chain.append(primary_agent) # Delegate to specialist
|
| 263 |
+
coordination_chain.append("jane_alesi") # Jane provides final coordination
|
| 264 |
+
|
| 265 |
+
logger.info(f"✅ Single Agent Delegation: jane_alesi → {primary_agent}")
|
| 266 |
+
|
| 267 |
+
return MultiAgentChatResponse(
|
| 268 |
+
success=True,
|
| 269 |
+
coordinator_response=f"Als Master Coordinatorin habe ich deinen Request analysiert und {'direkt bearbeitet' if primary_agent == 'jane_alesi' else f'an {primary_agent} delegiert'}.",
|
| 270 |
+
delegated_agent=primary_agent if primary_agent != "jane_alesi" else None,
|
| 271 |
+
specialist_response=response_text if primary_agent != "jane_alesi" else None,
|
| 272 |
+
coordination_chain=coordination_chain,
|
| 273 |
+
processing_time=processing_time,
|
| 274 |
+
workflow_type="single_agent",
|
| 275 |
+
task_id=coordination_result.get("task_id"),
|
| 276 |
+
cost_info={
|
| 277 |
+
"total_cost": 0.0,
|
| 278 |
+
"agents_involved": len(coordination_chain)
|
| 279 |
+
},
|
| 280 |
+
privacy_protection=coordination_result.get('privacy_protection')
|
| 281 |
+
)
|
| 282 |
+
else:
|
| 283 |
+
# Coordination failed
|
| 284 |
+
error_msg = coordination_result.get("error", "Unknown coordination error")
|
| 285 |
+
logger.error(f"❌ Multi-Agent Coordination failed: {error_msg}")
|
| 286 |
+
|
| 287 |
+
return MultiAgentChatResponse(
|
| 288 |
+
success=False,
|
| 289 |
+
coordinator_response="Als Master Coordinatorin konnte ich deinen Request leider nicht erfolgreich bearbeiten.",
|
| 290 |
+
processing_time=processing_time,
|
| 291 |
+
error=error_msg
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
processing_time = (datetime.now() - start_time).total_seconds()
|
| 296 |
+
logger.error(f"❌ Multi-Agent Chat API Error: {e}")
|
| 297 |
+
|
| 298 |
+
return MultiAgentChatResponse(
|
| 299 |
+
success=False,
|
| 300 |
+
coordinator_response="Entschuldigung, es ist ein technischer Fehler im Multi-Agent System aufgetreten.",
|
| 301 |
+
processing_time=processing_time,
|
| 302 |
+
error=str(e)
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
@multi_agent_router.get("/status")
|
| 306 |
+
async def get_multi_agent_status(
|
| 307 |
+
coordinator: MultiAgentCoordinator = Depends(get_coordinator)
|
| 308 |
+
):
|
| 309 |
+
"""
|
| 310 |
+
Get current multi-agent coordination status and statistics
|
| 311 |
+
"""
|
| 312 |
+
try:
|
| 313 |
+
stats = await coordinator.get_coordination_stats()
|
| 314 |
+
|
| 315 |
+
return {
|
| 316 |
+
"status": "active",
|
| 317 |
+
"coordinator": "jane_alesi",
|
| 318 |
+
"available_specialists": [
|
| 319 |
+
{"id": "john_alesi", "name": "John Alesi", "specialization": "Development"},
|
| 320 |
+
{"id": "lara_alesi", "name": "Lara Alesi", "specialization": "Medical"},
|
| 321 |
+
{"id": "justus_alesi", "name": "Justus Alesi", "specialization": "Legal"},
|
| 322 |
+
{"id": "theo_alesi", "name": "Theo Alesi", "specialization": "Finance"},
|
| 323 |
+
{"id": "leon_alesi", "name": "Leon Alesi", "specialization": "System"},
|
| 324 |
+
{"id": "luna_alesi", "name": "Luna Alesi", "specialization": "Coaching"}
|
| 325 |
+
],
|
| 326 |
+
"coordination_stats": stats,
|
| 327 |
+
"features": {
|
| 328 |
+
"intent_analysis": True,
|
| 329 |
+
"automatic_delegation": True,
|
| 330 |
+
"multi_agent_workflows": True,
|
| 331 |
+
"real_time_coordination": True,
|
| 332 |
+
"task_orchestration": True
|
| 333 |
+
},
|
| 334 |
+
"timestamp": datetime.now().isoformat()
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
except Exception as e:
|
| 338 |
+
logger.error(f"❌ Multi-Agent Status Error: {e}")
|
| 339 |
+
raise HTTPException(status_code=500, detail=f"Status check failed: {str(e)}")
|
| 340 |
+
|
| 341 |
+
@multi_agent_router.get("/capabilities")
|
| 342 |
+
async def get_agent_capabilities(
|
| 343 |
+
coordinator: MultiAgentCoordinator = Depends(get_coordinator)
|
| 344 |
+
):
|
| 345 |
+
"""
|
| 346 |
+
Get detailed agent capabilities for intelligent task delegation
|
| 347 |
+
"""
|
| 348 |
+
try:
|
| 349 |
+
capabilities = {}
|
| 350 |
+
|
| 351 |
+
for agent_id, agent_caps in coordinator.agent_capabilities.items():
|
| 352 |
+
capabilities[agent_id] = {
|
| 353 |
+
"agent_name": {
|
| 354 |
+
"jane_alesi": "Jane Alesi - Master Coordinator",
|
| 355 |
+
"john_alesi": "John Alesi - Software Developer",
|
| 356 |
+
"lara_alesi": "Lara Alesi - Medical Expert",
|
| 357 |
+
"justus_alesi": "Justus Alesi - Legal Expert",
|
| 358 |
+
"theo_alesi": "Theo Alesi - Financial Analyst",
|
| 359 |
+
"leon_alesi": "Leon Alesi - System Administrator",
|
| 360 |
+
"luna_alesi": "Luna Alesi - Coaching Specialist"
|
| 361 |
+
}.get(agent_id, agent_id),
|
| 362 |
+
"capabilities": [
|
| 363 |
+
{
|
| 364 |
+
"name": cap.name,
|
| 365 |
+
"description": cap.description,
|
| 366 |
+
"keywords": cap.keywords,
|
| 367 |
+
"complexity_level": cap.complexity_level
|
| 368 |
+
}
|
| 369 |
+
for cap in agent_caps
|
| 370 |
+
],
|
| 371 |
+
"specialization": {
|
| 372 |
+
"jane_alesi": "Coordination & Architecture",
|
| 373 |
+
"john_alesi": "Software Development",
|
| 374 |
+
"lara_alesi": "Medical Analysis",
|
| 375 |
+
"justus_alesi": "Legal Compliance",
|
| 376 |
+
"theo_alesi": "Financial Analysis",
|
| 377 |
+
"leon_alesi": "System Administration",
|
| 378 |
+
"luna_alesi": "Coaching & Process"
|
| 379 |
+
}.get(agent_id, "General")
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
return {
|
| 383 |
+
"total_agents": len(capabilities),
|
| 384 |
+
"coordinator": "jane_alesi",
|
| 385 |
+
"specialists_count": len(capabilities) - 1,
|
| 386 |
+
"capabilities": capabilities,
|
| 387 |
+
"timestamp": datetime.now().isoformat()
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
except Exception as e:
|
| 391 |
+
logger.error(f"❌ Agent Capabilities Error: {e}")
|
| 392 |
+
raise HTTPException(status_code=500, detail=f"Capabilities retrieval failed: {str(e)}")
|
| 393 |
+
|
| 394 |
+
@multi_agent_router.get("/workload/{agent_id}")
|
| 395 |
+
async def get_agent_workload(
|
| 396 |
+
agent_id: str,
|
| 397 |
+
coordinator: MultiAgentCoordinator = Depends(get_coordinator)
|
| 398 |
+
):
|
| 399 |
+
"""
|
| 400 |
+
Get current workload and task statistics for a specific agent
|
| 401 |
+
"""
|
| 402 |
+
try:
|
| 403 |
+
workload = await coordinator.get_agent_workload(agent_id)
|
| 404 |
+
return workload
|
| 405 |
+
|
| 406 |
+
except Exception as e:
|
| 407 |
+
logger.error(f"❌ Agent Workload Error for {agent_id}: {e}")
|
| 408 |
+
raise HTTPException(status_code=500, detail=f"Workload check failed: {str(e)}")
|
backend/api/openrouter_client.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenRouter API Client for SAAP
|
| 3 |
+
Provides OpenAI-compatible interface with cost tracking and performance metrics
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import logging
|
| 8 |
+
import time
|
| 9 |
+
import os
|
| 10 |
+
from typing import Dict, List, Optional, Any, Tuple
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
import aiohttp
|
| 13 |
+
import json
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from dotenv import load_dotenv
|
| 16 |
+
|
| 17 |
+
# Load environment variables
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class OpenRouterResponse:
|
| 24 |
+
"""OpenRouter API response with cost tracking"""
|
| 25 |
+
success: bool
|
| 26 |
+
content: Optional[str] = None
|
| 27 |
+
error: Optional[str] = None
|
| 28 |
+
response_time: float = 0.0
|
| 29 |
+
tokens_used: int = 0
|
| 30 |
+
input_tokens: int = 0
|
| 31 |
+
output_tokens: int = 0
|
| 32 |
+
cost_usd: float = 0.0
|
| 33 |
+
model: str = ""
|
| 34 |
+
provider: str = "openrouter"
|
| 35 |
+
timestamp: datetime = None
|
| 36 |
+
|
| 37 |
+
def __post_init__(self):
|
| 38 |
+
if self.timestamp is None:
|
| 39 |
+
self.timestamp = datetime.utcnow()
|
| 40 |
+
|
| 41 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 42 |
+
"""Convert to dictionary for logging and API responses"""
|
| 43 |
+
return {
|
| 44 |
+
"success": self.success,
|
| 45 |
+
"content": self.content,
|
| 46 |
+
"error": self.error,
|
| 47 |
+
"response_time": self.response_time,
|
| 48 |
+
"tokens_used": self.tokens_used,
|
| 49 |
+
"input_tokens": self.input_tokens,
|
| 50 |
+
"output_tokens": self.output_tokens,
|
| 51 |
+
"cost_usd": self.cost_usd,
|
| 52 |
+
"model": self.model,
|
| 53 |
+
"provider": self.provider,
|
| 54 |
+
"timestamp": self.timestamp.isoformat(),
|
| 55 |
+
"cost_efficiency": f"${self.cost_usd:.6f} ({self.tokens_used} tokens, {self.response_time:.1f}s)" if self.success else "N/A"
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
class OpenRouterClient:
|
| 59 |
+
"""
|
| 60 |
+
OpenRouter API Client with Cost Optimization for SAAP
|
| 61 |
+
|
| 62 |
+
Features:
|
| 63 |
+
- OpenAI-compatible API interface
|
| 64 |
+
- Agent-specific model selection (Jane: GPT-4o-mini, John: Claude-3.5-Sonnet, etc.)
|
| 65 |
+
- Cost tracking and budget management
|
| 66 |
+
- Performance monitoring and fallback models
|
| 67 |
+
- Async/await support for high-performance integration
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def __init__(self, api_key: str, base_url: str = "https://openrouter.ai/api/v1"):
|
| 71 |
+
self.api_key = api_key
|
| 72 |
+
self.base_url = base_url.rstrip('/')
|
| 73 |
+
self.session: Optional[aiohttp.ClientSession] = None
|
| 74 |
+
self.daily_budget = 10.0 # $10/day default
|
| 75 |
+
self.current_daily_cost = 0.0
|
| 76 |
+
self.cost_alert_threshold = 0.8 # 80% of budget
|
| 77 |
+
|
| 78 |
+
# 🚀 PERFORMANCE OPTIMIZATION: Reduced token limits for faster responses
|
| 79 |
+
# Phase 1.3 Quick Win: 40-50% token reduction = 0.5-1s faster per request
|
| 80 |
+
# Agent-specific model configurations with cost data
|
| 81 |
+
self.agent_models = {
|
| 82 |
+
"jane_alesi": {
|
| 83 |
+
"model": "openai/gpt-4o-mini",
|
| 84 |
+
"max_tokens": 400, # Reduced from 800 (-50%)
|
| 85 |
+
"temperature": 0.7,
|
| 86 |
+
"cost_per_1m_input": 0.15, # $0.15/1M tokens
|
| 87 |
+
"cost_per_1m_output": 0.60, # $0.60/1M tokens
|
| 88 |
+
"description": "Efficient coordination and management"
|
| 89 |
+
},
|
| 90 |
+
"john_alesi": {
|
| 91 |
+
"model": "anthropic/claude-3-5-sonnet-20241022",
|
| 92 |
+
"max_tokens": 600, # Reduced from 1200 (-50%)
|
| 93 |
+
"temperature": 0.5,
|
| 94 |
+
"cost_per_1m_input": 3.00, # $3.00/1M tokens
|
| 95 |
+
"cost_per_1m_output": 15.00, # $15.00/1M tokens
|
| 96 |
+
"description": "Advanced code generation and development"
|
| 97 |
+
},
|
| 98 |
+
"lara_alesi": {
|
| 99 |
+
"model": "openai/gpt-4o-mini",
|
| 100 |
+
"max_tokens": 500, # Reduced from 1000 (-50%)
|
| 101 |
+
"temperature": 0.3,
|
| 102 |
+
"cost_per_1m_input": 0.15, # $0.15/1M tokens
|
| 103 |
+
"cost_per_1m_output": 0.60, # $0.60/1M tokens
|
| 104 |
+
"description": "Precise medical and analytical tasks"
|
| 105 |
+
},
|
| 106 |
+
"fallback": {
|
| 107 |
+
"model": "meta-llama/llama-3.2-3b-instruct:free",
|
| 108 |
+
"max_tokens": 600,
|
| 109 |
+
"temperature": 0.7,
|
| 110 |
+
"cost_per_1m_input": 0.0, # FREE
|
| 111 |
+
"cost_per_1m_output": 0.0, # FREE
|
| 112 |
+
"description": "Free backup model for budget protection"
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
logger.info(f"🌐 OpenRouter Client initialized with {len(self.agent_models)} agent models")
|
| 117 |
+
logger.info(f"💰 Daily budget: ${self.daily_budget}, Alert threshold: {self.cost_alert_threshold*100}%")
|
| 118 |
+
|
| 119 |
+
async def __aenter__(self):
|
| 120 |
+
"""Async context manager entry"""
|
| 121 |
+
self.session = aiohttp.ClientSession(
|
| 122 |
+
timeout=aiohttp.ClientTimeout(total=60),
|
| 123 |
+
headers={
|
| 124 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 125 |
+
"Content-Type": "application/json",
|
| 126 |
+
"HTTP-Referer": "https://saap.satware.ai", # Optional: your app URL
|
| 127 |
+
"X-Title": "SAAP Agent Platform" # Optional: app title
|
| 128 |
+
}
|
| 129 |
+
)
|
| 130 |
+
logger.info("🔌 OpenRouter session created")
|
| 131 |
+
return self
|
| 132 |
+
|
| 133 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 134 |
+
"""Async context manager exit"""
|
| 135 |
+
if self.session:
|
| 136 |
+
await self.session.close()
|
| 137 |
+
logger.info("🔌 OpenRouter session closed")
|
| 138 |
+
|
| 139 |
+
def get_model_config(self, agent_id: str) -> Dict[str, Any]:
|
| 140 |
+
"""Get model configuration for specific agent"""
|
| 141 |
+
return self.agent_models.get(agent_id, self.agent_models["fallback"])
|
| 142 |
+
|
| 143 |
+
def estimate_cost(self, message: str, agent_id: str) -> float:
|
| 144 |
+
"""Estimate request cost before sending"""
|
| 145 |
+
config = self.get_model_config(agent_id)
|
| 146 |
+
|
| 147 |
+
# Rough token estimation: ~4 characters per token
|
| 148 |
+
estimated_input_tokens = len(message) / 4
|
| 149 |
+
estimated_output_tokens = config["max_tokens"] * 0.5 # Assume 50% of max tokens
|
| 150 |
+
|
| 151 |
+
cost_usd = (
|
| 152 |
+
(estimated_input_tokens / 1_000_000) * config["cost_per_1m_input"] +
|
| 153 |
+
(estimated_output_tokens / 1_000_000) * config["cost_per_1m_output"]
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
return cost_usd
|
| 157 |
+
|
| 158 |
+
async def chat_completion(
|
| 159 |
+
self,
|
| 160 |
+
messages: List[Dict[str, str]],
|
| 161 |
+
agent_id: str,
|
| 162 |
+
max_tokens: Optional[int] = None,
|
| 163 |
+
temperature: Optional[float] = None
|
| 164 |
+
) -> OpenRouterResponse:
|
| 165 |
+
"""
|
| 166 |
+
Send chat completion request to OpenRouter with cost tracking
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
messages: List of message dicts with 'role' and 'content'
|
| 170 |
+
agent_id: Agent identifier for model selection
|
| 171 |
+
max_tokens: Override max tokens
|
| 172 |
+
temperature: Override temperature
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
OpenRouterResponse with content, cost, and performance data
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
if not self.session:
|
| 179 |
+
return OpenRouterResponse(
|
| 180 |
+
success=False,
|
| 181 |
+
error="OpenRouter client session not initialized - call async context manager",
|
| 182 |
+
model="",
|
| 183 |
+
provider="openrouter"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Get agent-specific model config
|
| 187 |
+
config = self.get_model_config(agent_id)
|
| 188 |
+
model = config["model"]
|
| 189 |
+
|
| 190 |
+
# Budget check before expensive request
|
| 191 |
+
estimated_cost = self.estimate_cost(str(messages), agent_id)
|
| 192 |
+
if self.current_daily_cost + estimated_cost > self.daily_budget:
|
| 193 |
+
logger.warning(f"💸 Daily budget would be exceeded - switching to free fallback")
|
| 194 |
+
config = self.agent_models["fallback"]
|
| 195 |
+
model = config["model"]
|
| 196 |
+
|
| 197 |
+
start_time = time.time()
|
| 198 |
+
|
| 199 |
+
# Prepare request payload
|
| 200 |
+
payload = {
|
| 201 |
+
"model": model,
|
| 202 |
+
"messages": messages,
|
| 203 |
+
"max_tokens": max_tokens or config["max_tokens"],
|
| 204 |
+
"temperature": temperature or config["temperature"],
|
| 205 |
+
"stream": False # We want complete responses for cost calculation
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
logger.info(f"📤 OpenRouter request: {agent_id} → {model}")
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
async with self.session.post(
|
| 212 |
+
f"{self.base_url}/chat/completions",
|
| 213 |
+
json=payload
|
| 214 |
+
) as response:
|
| 215 |
+
|
| 216 |
+
response_time = time.time() - start_time
|
| 217 |
+
|
| 218 |
+
if response.status == 200:
|
| 219 |
+
data = await response.json()
|
| 220 |
+
|
| 221 |
+
# Extract response content
|
| 222 |
+
content = ""
|
| 223 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 224 |
+
choice = data["choices"][0]
|
| 225 |
+
if "message" in choice and "content" in choice["message"]:
|
| 226 |
+
content = choice["message"]["content"]
|
| 227 |
+
|
| 228 |
+
# Extract token usage and calculate cost
|
| 229 |
+
usage = data.get("usage", {})
|
| 230 |
+
input_tokens = usage.get("prompt_tokens", 0)
|
| 231 |
+
output_tokens = usage.get("completion_tokens", 0)
|
| 232 |
+
total_tokens = usage.get("total_tokens", 0)
|
| 233 |
+
|
| 234 |
+
# Calculate actual cost
|
| 235 |
+
cost_usd = (
|
| 236 |
+
(input_tokens / 1_000_000) * config["cost_per_1m_input"] +
|
| 237 |
+
(output_tokens / 1_000_000) * config["cost_per_1m_output"]
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# Update daily cost tracking
|
| 241 |
+
self.current_daily_cost += cost_usd
|
| 242 |
+
|
| 243 |
+
# Log cost alert if needed
|
| 244 |
+
if self.current_daily_cost >= (self.daily_budget * self.cost_alert_threshold):
|
| 245 |
+
logger.warning(f"⚠️ OpenRouter cost alert: ${self.current_daily_cost:.4f} / ${self.daily_budget} ({self.current_daily_cost/self.daily_budget*100:.1f}%)")
|
| 246 |
+
|
| 247 |
+
logger.info(f"✅ OpenRouter success: {response_time:.2f}s, ${cost_usd:.6f}, {total_tokens} tokens")
|
| 248 |
+
|
| 249 |
+
return OpenRouterResponse(
|
| 250 |
+
success=True,
|
| 251 |
+
content=content,
|
| 252 |
+
response_time=response_time,
|
| 253 |
+
tokens_used=total_tokens,
|
| 254 |
+
input_tokens=input_tokens,
|
| 255 |
+
output_tokens=output_tokens,
|
| 256 |
+
cost_usd=cost_usd,
|
| 257 |
+
model=model,
|
| 258 |
+
provider="openrouter"
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
else:
|
| 262 |
+
error_text = await response.text()
|
| 263 |
+
error_msg = f"HTTP {response.status}: {error_text}"
|
| 264 |
+
|
| 265 |
+
# Handle rate limiting and payment errors with fallback
|
| 266 |
+
if response.status in [429, 402]:
|
| 267 |
+
logger.warning(f"⚠️ OpenRouter limit reached: {error_msg}")
|
| 268 |
+
if config != self.agent_models["fallback"]:
|
| 269 |
+
logger.info("🔄 Attempting fallback to free model...")
|
| 270 |
+
return await self.chat_completion(messages, "fallback", max_tokens, temperature)
|
| 271 |
+
|
| 272 |
+
logger.error(f"❌ OpenRouter API error: {error_msg}")
|
| 273 |
+
|
| 274 |
+
return OpenRouterResponse(
|
| 275 |
+
success=False,
|
| 276 |
+
error=error_msg,
|
| 277 |
+
response_time=response_time,
|
| 278 |
+
model=model,
|
| 279 |
+
provider="openrouter"
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
except asyncio.TimeoutError:
|
| 283 |
+
error_msg = f"Request timeout after {time.time() - start_time:.1f}s"
|
| 284 |
+
logger.error(f"❌ OpenRouter timeout: {error_msg}")
|
| 285 |
+
|
| 286 |
+
return OpenRouterResponse(
|
| 287 |
+
success=False,
|
| 288 |
+
error=error_msg,
|
| 289 |
+
response_time=time.time() - start_time,
|
| 290 |
+
model=model,
|
| 291 |
+
provider="openrouter"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
error_msg = f"OpenRouter request failed: {str(e)}"
|
| 296 |
+
logger.error(f"❌ OpenRouter error: {error_msg}")
|
| 297 |
+
|
| 298 |
+
return OpenRouterResponse(
|
| 299 |
+
success=False,
|
| 300 |
+
error=error_msg,
|
| 301 |
+
response_time=time.time() - start_time,
|
| 302 |
+
model=model,
|
| 303 |
+
provider="openrouter"
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
async def health_check(self) -> Dict[str, Any]:
|
| 307 |
+
"""Check OpenRouter API health and available models"""
|
| 308 |
+
if not self.session:
|
| 309 |
+
return {
|
| 310 |
+
"status": "unhealthy",
|
| 311 |
+
"error": "Session not initialized"
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
try:
|
| 315 |
+
# Test with a simple completion
|
| 316 |
+
test_messages = [{"role": "user", "content": "Reply with just 'OK' to confirm API connection."}]
|
| 317 |
+
|
| 318 |
+
result = await self.chat_completion(test_messages, "fallback") # Use free model for health check
|
| 319 |
+
|
| 320 |
+
return {
|
| 321 |
+
"status": "healthy" if result.success else "unhealthy",
|
| 322 |
+
"response_time": result.response_time,
|
| 323 |
+
"error": result.error if not result.success else None,
|
| 324 |
+
"daily_cost": self.current_daily_cost,
|
| 325 |
+
"budget_remaining": max(0, self.daily_budget - self.current_daily_cost),
|
| 326 |
+
"available_models": len(self.agent_models),
|
| 327 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
except Exception as e:
|
| 331 |
+
return {
|
| 332 |
+
"status": "error",
|
| 333 |
+
"error": str(e),
|
| 334 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
def get_cost_summary(self) -> Dict[str, Any]:
|
| 338 |
+
"""Get current cost and budget status"""
|
| 339 |
+
return {
|
| 340 |
+
"daily_cost_usd": round(self.current_daily_cost, 4),
|
| 341 |
+
"daily_budget_usd": self.daily_budget,
|
| 342 |
+
"budget_used_percent": round(self.current_daily_cost / self.daily_budget * 100, 1),
|
| 343 |
+
"budget_remaining_usd": max(0, self.daily_budget - self.current_daily_cost),
|
| 344 |
+
"alert_threshold_percent": self.cost_alert_threshold * 100,
|
| 345 |
+
"cost_alert_active": self.current_daily_cost >= (self.daily_budget * self.cost_alert_threshold),
|
| 346 |
+
"agent_models_available": list(self.agent_models.keys()),
|
| 347 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
def reset_daily_costs(self):
|
| 351 |
+
"""Reset daily cost tracking (call at midnight)"""
|
| 352 |
+
yesterday_cost = self.current_daily_cost
|
| 353 |
+
self.current_daily_cost = 0.0
|
| 354 |
+
logger.info(f"📅 OpenRouter daily cost reset - Yesterday: ${yesterday_cost:.4f}")
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
if __name__ == "__main__":
|
| 358 |
+
# Demo OpenRouter integration
|
| 359 |
+
async def demo_openrouter():
|
| 360 |
+
api_key = os.getenv("OPENROUTER_API_KEY", "")
|
| 361 |
+
|
| 362 |
+
if not api_key:
|
| 363 |
+
print("❌ Error: OPENROUTER_API_KEY environment variable not set")
|
| 364 |
+
print("Please add OPENROUTER_API_KEY to backend/.env file")
|
| 365 |
+
return
|
| 366 |
+
|
| 367 |
+
async with OpenRouterClient(api_key) as client:
|
| 368 |
+
print("🌐 OpenRouter Client Demo")
|
| 369 |
+
|
| 370 |
+
# Health check
|
| 371 |
+
health = await client.health_check()
|
| 372 |
+
print(f"Health: {health['status']}")
|
| 373 |
+
|
| 374 |
+
if health["status"] == "healthy":
|
| 375 |
+
# Test different agents
|
| 376 |
+
for agent in ["jane_alesi", "john_alesi", "fallback"]:
|
| 377 |
+
config = client.get_model_config(agent)
|
| 378 |
+
print(f"\n🤖 Testing {agent} - Model: {config['model']}")
|
| 379 |
+
|
| 380 |
+
messages = [
|
| 381 |
+
{"role": "user", "content": f"Hello! I'm testing the {agent} agent. Please respond briefly."}
|
| 382 |
+
]
|
| 383 |
+
|
| 384 |
+
result = await client.chat_completion(messages, agent)
|
| 385 |
+
|
| 386 |
+
if result.success:
|
| 387 |
+
print(f"✅ Response: {result.content[:100]}...")
|
| 388 |
+
print(f"💰 Cost: ${result.cost_usd:.6f}")
|
| 389 |
+
print(f"⏱️ Time: {result.response_time:.2f}s")
|
| 390 |
+
print(f"🔢 Tokens: {result.tokens_used}")
|
| 391 |
+
else:
|
| 392 |
+
print(f"❌ Error: {result.error}")
|
| 393 |
+
|
| 394 |
+
# Cost summary
|
| 395 |
+
print(f"\n💰 Cost Summary: {client.get_cost_summary()}")
|
| 396 |
+
|
| 397 |
+
asyncio.run(demo_openrouter())
|
backend/api/openrouter_endpoints.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenRouter Chat Endpoints for SAAP Frontend
|
| 3 |
+
Separate endpoints for OpenRouter-specific chat functionality
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 7 |
+
from typing import Dict, Any
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
|
| 12 |
+
from services.agent_manager import AgentManagerService
|
| 13 |
+
from agents.openrouter_saap_agent import OpenRouterSAAPAgent
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
# Create router for OpenRouter endpoints
|
| 18 |
+
openrouter_router = APIRouter(prefix="/api/v1/agents", tags=["OpenRouter"])
|
| 19 |
+
|
| 20 |
+
def get_agent_manager() -> AgentManagerService:
|
| 21 |
+
"""Dependency injection for agent manager"""
|
| 22 |
+
from main import saap_app
|
| 23 |
+
if not saap_app.agent_manager:
|
| 24 |
+
raise HTTPException(status_code=503, detail="Agent Manager not initialized")
|
| 25 |
+
return saap_app.agent_manager
|
| 26 |
+
|
| 27 |
+
@openrouter_router.post("/{agent_id}/chat/openrouter")
|
| 28 |
+
async def chat_with_agent_via_openrouter(
|
| 29 |
+
agent_id: str,
|
| 30 |
+
message_data: Dict[str, Any],
|
| 31 |
+
agent_manager: AgentManagerService = Depends(get_agent_manager)
|
| 32 |
+
):
|
| 33 |
+
"""
|
| 34 |
+
🚀 NEW: Chat with agent using OpenRouter (Fast, Cost-efficient)
|
| 35 |
+
Endpoint: POST /api/v1/agents/{agent_id}/chat/openrouter
|
| 36 |
+
"""
|
| 37 |
+
try:
|
| 38 |
+
message = message_data.get("message", "")
|
| 39 |
+
if not message:
|
| 40 |
+
raise HTTPException(status_code=400, detail="Message content required")
|
| 41 |
+
|
| 42 |
+
logger.info(f"🌐 OpenRouter Chat request: {agent_id} - {message[:50]}...")
|
| 43 |
+
|
| 44 |
+
# Get agent from manager
|
| 45 |
+
agent = agent_manager.get_agent(agent_id)
|
| 46 |
+
if not agent:
|
| 47 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
| 48 |
+
|
| 49 |
+
# Get OpenRouter API key from environment
|
| 50 |
+
api_key = os.getenv("OPENROUTER_API_KEY")
|
| 51 |
+
if not api_key:
|
| 52 |
+
raise HTTPException(
|
| 53 |
+
status_code=500,
|
| 54 |
+
detail="OpenRouter API key not configured"
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# Create OpenRouter agent
|
| 58 |
+
openrouter_agent = OpenRouterSAAPAgent(
|
| 59 |
+
agent_id,
|
| 60 |
+
agent.type.value if agent.type else "Assistant",
|
| 61 |
+
api_key
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Get agent-specific model configuration
|
| 65 |
+
model_map = {
|
| 66 |
+
"jane_alesi": os.getenv("JANE_ALESI_MODEL", "openai/gpt-4o-mini"),
|
| 67 |
+
"john_alesi": os.getenv("JOHN_ALESI_MODEL", "deepseek/deepseek-coder"),
|
| 68 |
+
"lara_alesi": os.getenv("LARA_ALESI_MODEL", "anthropic/claude-3-haiku")
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
preferred_model = model_map.get(agent_id, "openai/gpt-4o-mini")
|
| 72 |
+
openrouter_agent.model_name = preferred_model
|
| 73 |
+
|
| 74 |
+
# Send request to OpenRouter
|
| 75 |
+
response = await openrouter_agent.send_request_to_openrouter(
|
| 76 |
+
message,
|
| 77 |
+
max_tokens=1000
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
if response.get("success"):
|
| 81 |
+
logger.info(f"✅ OpenRouter chat successful: {agent_id}")
|
| 82 |
+
|
| 83 |
+
return {
|
| 84 |
+
"success": True,
|
| 85 |
+
"agent_id": agent_id,
|
| 86 |
+
"provider": "OpenRouter",
|
| 87 |
+
"model": preferred_model,
|
| 88 |
+
"message": message,
|
| 89 |
+
"response": {
|
| 90 |
+
"content": response.get("response", ""),
|
| 91 |
+
"provider": f"OpenRouter ({preferred_model})",
|
| 92 |
+
"response_time": response.get("response_time", 0),
|
| 93 |
+
"tokens_used": response.get("token_count", 0),
|
| 94 |
+
"cost_estimate": response.get("cost_estimate", 0.0)
|
| 95 |
+
},
|
| 96 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 97 |
+
}
|
| 98 |
+
else:
|
| 99 |
+
logger.error(f"❌ OpenRouter chat failed: {response.get('error')}")
|
| 100 |
+
|
| 101 |
+
return {
|
| 102 |
+
"success": False,
|
| 103 |
+
"agent_id": agent_id,
|
| 104 |
+
"provider": "OpenRouter",
|
| 105 |
+
"error": response.get("error", "Unknown OpenRouter error"),
|
| 106 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
except HTTPException:
|
| 110 |
+
raise
|
| 111 |
+
except Exception as e:
|
| 112 |
+
logger.error(f"❌ OpenRouter endpoint error: {e}")
|
| 113 |
+
|
| 114 |
+
return {
|
| 115 |
+
"success": False,
|
| 116 |
+
"agent_id": agent_id,
|
| 117 |
+
"provider": "OpenRouter",
|
| 118 |
+
"error": f"OpenRouter endpoint error: {str(e)}",
|
| 119 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
@openrouter_router.post("/{agent_id}/chat/compare")
|
| 123 |
+
async def compare_providers(
|
| 124 |
+
agent_id: str,
|
| 125 |
+
message_data: Dict[str, Any],
|
| 126 |
+
agent_manager: AgentManagerService = Depends(get_agent_manager)
|
| 127 |
+
):
|
| 128 |
+
"""
|
| 129 |
+
🆚 Compare colossus vs OpenRouter performance side-by-side
|
| 130 |
+
"""
|
| 131 |
+
try:
|
| 132 |
+
message = message_data.get("message", "")
|
| 133 |
+
if not message:
|
| 134 |
+
raise HTTPException(status_code=400, detail="Message content required")
|
| 135 |
+
|
| 136 |
+
logger.info(f"🆚 Provider comparison request: {agent_id}")
|
| 137 |
+
|
| 138 |
+
# Send to both providers concurrently
|
| 139 |
+
import asyncio
|
| 140 |
+
|
| 141 |
+
# Colossus request
|
| 142 |
+
async def colossus_request():
|
| 143 |
+
try:
|
| 144 |
+
return await agent_manager.send_message_to_agent(agent_id, message)
|
| 145 |
+
except Exception as e:
|
| 146 |
+
return {"error": f"colossus error: {str(e)}", "provider": "colossus"}
|
| 147 |
+
|
| 148 |
+
# OpenRouter request
|
| 149 |
+
async def openrouter_request():
|
| 150 |
+
try:
|
| 151 |
+
agent = agent_manager.get_agent(agent_id)
|
| 152 |
+
if not agent:
|
| 153 |
+
return {"error": "Agent not found", "provider": "OpenRouter"}
|
| 154 |
+
|
| 155 |
+
api_key = os.getenv("OPENROUTER_API_KEY")
|
| 156 |
+
if not api_key:
|
| 157 |
+
return {"error": "API key not configured", "provider": "OpenRouter"}
|
| 158 |
+
|
| 159 |
+
openrouter_agent = OpenRouterSAAPAgent(agent_id, "Assistant", api_key)
|
| 160 |
+
response = await openrouter_agent.send_request_to_openrouter(message)
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
"provider": "OpenRouter",
|
| 164 |
+
"content": response.get("response", ""),
|
| 165 |
+
"response_time": response.get("response_time", 0),
|
| 166 |
+
"success": response.get("success", False),
|
| 167 |
+
"error": response.get("error") if not response.get("success") else None
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
return {"error": f"OpenRouter error: {str(e)}", "provider": "OpenRouter"}
|
| 172 |
+
|
| 173 |
+
# Run both concurrently with timeout
|
| 174 |
+
colossus_result, openrouter_result = await asyncio.gather(
|
| 175 |
+
colossus_request(),
|
| 176 |
+
openrouter_request(),
|
| 177 |
+
return_exceptions=True
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
return {
|
| 181 |
+
"success": True,
|
| 182 |
+
"comparison": {
|
| 183 |
+
"colossus": colossus_result,
|
| 184 |
+
"openrouter": openrouter_result,
|
| 185 |
+
"performance_winner": (
|
| 186 |
+
"OpenRouter" if (
|
| 187 |
+
openrouter_result.get("response_time", float('inf')) <
|
| 188 |
+
colossus_result.get("response_time", float('inf'))
|
| 189 |
+
) else "colossus"
|
| 190 |
+
)
|
| 191 |
+
},
|
| 192 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
except Exception as e:
|
| 196 |
+
logger.error(f"❌ Provider comparison error: {e}")
|
| 197 |
+
raise HTTPException(status_code=500, detail=f"Comparison failed: {str(e)}")
|
| 198 |
+
|
| 199 |
+
@openrouter_router.get("/{agent_id}/openrouter/status")
|
| 200 |
+
async def openrouter_status(agent_id: str):
|
| 201 |
+
"""
|
| 202 |
+
ℹ️ Get OpenRouter availability status for specific agent
|
| 203 |
+
"""
|
| 204 |
+
try:
|
| 205 |
+
api_key = os.getenv("OPENROUTER_API_KEY")
|
| 206 |
+
if not api_key:
|
| 207 |
+
return {
|
| 208 |
+
"available": False,
|
| 209 |
+
"error": "OpenRouter API key not configured",
|
| 210 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
# Test OpenRouter connection
|
| 214 |
+
test_agent = OpenRouterSAAPAgent(agent_id, "Test", api_key)
|
| 215 |
+
health = await test_agent.health_check()
|
| 216 |
+
|
| 217 |
+
return {
|
| 218 |
+
"available": health["status"] == "healthy",
|
| 219 |
+
"model": test_agent.model_name,
|
| 220 |
+
"response_time": health.get("response_time"),
|
| 221 |
+
"error": health.get("error"),
|
| 222 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
except Exception as e:
|
| 226 |
+
return {
|
| 227 |
+
"available": False,
|
| 228 |
+
"error": str(e),
|
| 229 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 230 |
+
}
|
backend/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Configuration package
|
backend/config/settings.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP Configuration Settings - Production Ready with OpenRouter Integration
|
| 3 |
+
Environment-based configuration management for On-Premise deployment
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Optional, List
|
| 9 |
+
from pydantic import Field, field_validator
|
| 10 |
+
from pydantic_settings import BaseSettings
|
| 11 |
+
from functools import lru_cache
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
class DatabaseSettings(BaseSettings):
|
| 15 |
+
"""Database configuration settings"""
|
| 16 |
+
|
| 17 |
+
# Database URL - supports SQLite, PostgreSQL, MySQL
|
| 18 |
+
database_url: str = Field(
|
| 19 |
+
default="sqlite:///./saap_production.db",
|
| 20 |
+
env="DATABASE_URL",
|
| 21 |
+
description="Database connection URL"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Connection pool settings
|
| 25 |
+
pool_size: int = Field(default=10, env="DB_POOL_SIZE")
|
| 26 |
+
max_overflow: int = Field(default=20, env="DB_MAX_OVERFLOW")
|
| 27 |
+
pool_timeout: int = Field(default=30, env="DB_POOL_TIMEOUT")
|
| 28 |
+
pool_recycle: int = Field(default=3600, env="DB_POOL_RECYCLE")
|
| 29 |
+
|
| 30 |
+
# SQLite specific
|
| 31 |
+
sqlite_check_same_thread: bool = Field(default=False, env="SQLITE_CHECK_SAME_THREAD")
|
| 32 |
+
|
| 33 |
+
model_config = {
|
| 34 |
+
"env_file": ".env",
|
| 35 |
+
"env_file_encoding": "utf-8",
|
| 36 |
+
"case_sensitive": False,
|
| 37 |
+
"extra": "allow" # Allow extra fields
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
@field_validator('database_url')
|
| 41 |
+
def validate_database_url(cls, v):
|
| 42 |
+
"""Ensure database URL is properly formatted"""
|
| 43 |
+
if not v.startswith(('sqlite:///', 'postgresql://', 'mysql://')):
|
| 44 |
+
raise ValueError('Unsupported database type. Use sqlite, postgresql, or mysql.')
|
| 45 |
+
return v
|
| 46 |
+
|
| 47 |
+
class ColossusSettings(BaseSettings):
|
| 48 |
+
"""colossus Server configuration"""
|
| 49 |
+
|
| 50 |
+
api_base: str = Field(
|
| 51 |
+
default="https://ai.adrian-schupp.de",
|
| 52 |
+
env="COLOSSUS_API_BASE",
|
| 53 |
+
description="colossus server base URL"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
api_key: str = Field(
|
| 57 |
+
default="sk-dBoxml3krytIRLdjr35Lnw",
|
| 58 |
+
env="COLOSSUS_API_KEY",
|
| 59 |
+
description="colossus API key"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
default_model: str = Field(
|
| 63 |
+
default="mistral-small3.2:24b-instruct-2506",
|
| 64 |
+
env="COLOSSUS_DEFAULT_MODEL"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
timeout: int = Field(default=60, env="COLOSSUS_TIMEOUT")
|
| 68 |
+
max_retries: int = Field(default=3, env="COLOSSUS_MAX_RETRIES")
|
| 69 |
+
|
| 70 |
+
model_config = {
|
| 71 |
+
"env_file": ".env",
|
| 72 |
+
"env_file_encoding": "utf-8",
|
| 73 |
+
"case_sensitive": False,
|
| 74 |
+
"extra": "allow" # Allow extra fields
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
class OpenRouterSettings(BaseSettings):
|
| 78 |
+
"""OpenRouter API configuration for cost-efficient models"""
|
| 79 |
+
|
| 80 |
+
# API Configuration
|
| 81 |
+
api_key: str = Field(
|
| 82 |
+
default="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",
|
| 83 |
+
env="OPENROUTER_API_KEY",
|
| 84 |
+
description="OpenRouter API key for cost-efficient LLM access"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
base_url: str = Field(
|
| 88 |
+
default="https://openrouter.ai/api/v1",
|
| 89 |
+
env="OPENROUTER_BASE_URL"
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
enabled: bool = Field(default=True, env="OPENROUTER_ENABLED")
|
| 93 |
+
|
| 94 |
+
# Cost Optimization Settings
|
| 95 |
+
use_cost_optimization: bool = Field(default=True, env="OPENROUTER_USE_COST_OPTIMIZATION")
|
| 96 |
+
max_cost_per_request: float = Field(default=0.01, env="OPENROUTER_MAX_COST_PER_REQUEST")
|
| 97 |
+
fallback_to_free: bool = Field(default=True, env="OPENROUTER_FALLBACK_TO_FREE")
|
| 98 |
+
|
| 99 |
+
# Agent-Specific Model Configuration
|
| 100 |
+
jane_model: str = Field(default="openai/gpt-4o-mini", env="JANE_ALESI_MODEL")
|
| 101 |
+
jane_max_tokens: int = Field(default=800, env="JANE_ALESI_MAX_TOKENS")
|
| 102 |
+
jane_temperature: float = Field(default=0.7, env="JANE_ALESI_TEMPERATURE")
|
| 103 |
+
|
| 104 |
+
john_model: str = Field(default="anthropic/claude-3-haiku", env="JOHN_ALESI_MODEL")
|
| 105 |
+
john_max_tokens: int = Field(default=1200, env="JOHN_ALESI_MAX_TOKENS")
|
| 106 |
+
john_temperature: float = Field(default=0.5, env="JOHN_ALESI_TEMPERATURE")
|
| 107 |
+
|
| 108 |
+
lara_model: str = Field(default="openai/gpt-4o-mini", env="LARA_ALESI_MODEL")
|
| 109 |
+
lara_max_tokens: int = Field(default=1000, env="LARA_ALESI_MAX_TOKENS")
|
| 110 |
+
lara_temperature: float = Field(default=0.3, env="LARA_ALESI_TEMPERATURE")
|
| 111 |
+
|
| 112 |
+
# Free Model Fallbacks
|
| 113 |
+
fallback_model: str = Field(default="meta-llama/llama-3.2-3b-instruct:free", env="FALLBACK_MODEL")
|
| 114 |
+
analyst_model: str = Field(default="meta-llama/llama-3.2-3b-instruct:free", env="ANALYST_MODEL")
|
| 115 |
+
|
| 116 |
+
# Cost Tracking
|
| 117 |
+
enable_cost_tracking: bool = Field(default=True, env="ENABLE_COST_TRACKING")
|
| 118 |
+
cost_alert_threshold: float = Field(default=5.0, env="COST_ALERT_THRESHOLD")
|
| 119 |
+
log_performance_metrics: bool = Field(default=True, env="LOG_PERFORMANCE_METRICS")
|
| 120 |
+
save_cost_analytics: bool = Field(default=True, env="SAVE_COST_ANALYTICS")
|
| 121 |
+
|
| 122 |
+
model_config = {
|
| 123 |
+
"env_file": ".env",
|
| 124 |
+
"env_file_encoding": "utf-8",
|
| 125 |
+
"case_sensitive": False,
|
| 126 |
+
"extra": "allow"
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
def get_agent_model_config(self, agent_name: str) -> dict:
|
| 130 |
+
"""Get model configuration for specific agent"""
|
| 131 |
+
configs = {
|
| 132 |
+
"jane_alesi": {
|
| 133 |
+
"model": self.jane_model,
|
| 134 |
+
"max_tokens": self.jane_max_tokens,
|
| 135 |
+
"temperature": self.jane_temperature,
|
| 136 |
+
"cost_per_1m": 0.15 # GPT-4o-mini
|
| 137 |
+
},
|
| 138 |
+
"john_alesi": {
|
| 139 |
+
"model": self.john_model,
|
| 140 |
+
"max_tokens": self.john_max_tokens,
|
| 141 |
+
"temperature": self.john_temperature,
|
| 142 |
+
"cost_per_1m": 0.25 # Claude-3-Haiku
|
| 143 |
+
},
|
| 144 |
+
"lara_alesi": {
|
| 145 |
+
"model": self.lara_model,
|
| 146 |
+
"max_tokens": self.lara_max_tokens,
|
| 147 |
+
"temperature": self.lara_temperature,
|
| 148 |
+
"cost_per_1m": 0.15 # GPT-4o-mini
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
return configs.get(agent_name.lower(), {
|
| 153 |
+
"model": self.fallback_model,
|
| 154 |
+
"max_tokens": 600,
|
| 155 |
+
"temperature": 0.7,
|
| 156 |
+
"cost_per_1m": 0.0 # Free model
|
| 157 |
+
})
|
| 158 |
+
|
| 159 |
+
class RedisSettings(BaseSettings):
|
| 160 |
+
"""Redis configuration for message queuing"""
|
| 161 |
+
|
| 162 |
+
host: str = Field(default="localhost", env="REDIS_HOST")
|
| 163 |
+
port: int = Field(default=6379, env="REDIS_PORT")
|
| 164 |
+
password: Optional[str] = Field(default=None, env="REDIS_PASSWORD")
|
| 165 |
+
database: int = Field(default=0, env="REDIS_DB")
|
| 166 |
+
max_connections: int = Field(default=50, env="REDIS_MAX_CONNECTIONS")
|
| 167 |
+
|
| 168 |
+
model_config = {
|
| 169 |
+
"env_file": ".env",
|
| 170 |
+
"env_file_encoding": "utf-8",
|
| 171 |
+
"case_sensitive": False,
|
| 172 |
+
"extra": "allow" # Allow extra fields
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
class SecuritySettings(BaseSettings):
|
| 176 |
+
"""Security and authentication settings"""
|
| 177 |
+
|
| 178 |
+
secret_key: str = Field(
|
| 179 |
+
default="saap-development-secret-change-in-production",
|
| 180 |
+
env="SECRET_KEY",
|
| 181 |
+
min_length=32
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# JWT Settings
|
| 185 |
+
jwt_algorithm: str = Field(default="HS256", env="JWT_ALGORITHM")
|
| 186 |
+
jwt_expire_minutes: int = Field(default=1440, env="JWT_EXPIRE_MINUTES") # 24 hours
|
| 187 |
+
|
| 188 |
+
# API Rate limiting
|
| 189 |
+
rate_limit_requests: int = Field(default=1000, env="RATE_LIMIT_REQUESTS")
|
| 190 |
+
rate_limit_window: int = Field(default=3600, env="RATE_LIMIT_WINDOW") # 1 hour
|
| 191 |
+
|
| 192 |
+
# CORS settings - Updated for network access + HuggingFace Spaces
|
| 193 |
+
allowed_origins: str = Field(
|
| 194 |
+
default="http://localhost:5173,http://localhost:8080,http://localhost:3000,http://100.64.0.45:5173,http://100.64.0.45:8080,http://100.64.0.45:3000,http://100.64.0.45:8000,https://*.hf.space,https://huggingface.co",
|
| 195 |
+
env="ALLOWED_ORIGINS"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
model_config = {
|
| 199 |
+
"env_file": ".env",
|
| 200 |
+
"env_file_encoding": "utf-8",
|
| 201 |
+
"case_sensitive": False,
|
| 202 |
+
"extra": "allow" # Allow extra fields
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
@field_validator('allowed_origins')
|
| 206 |
+
def parse_allowed_origins(cls, v):
|
| 207 |
+
"""Parse comma-separated origins string into list"""
|
| 208 |
+
if isinstance(v, str):
|
| 209 |
+
return [origin.strip() for origin in v.split(',') if origin.strip()]
|
| 210 |
+
return v
|
| 211 |
+
|
| 212 |
+
def get_allowed_origins_list(self) -> List[str]:
|
| 213 |
+
"""Get allowed origins as a list"""
|
| 214 |
+
if isinstance(self.allowed_origins, str):
|
| 215 |
+
return [origin.strip() for origin in self.allowed_origins.split(',') if origin.strip()]
|
| 216 |
+
return self.allowed_origins
|
| 217 |
+
|
| 218 |
+
class LoggingSettings(BaseSettings):
|
| 219 |
+
"""Enhanced logging configuration with cost tracking"""
|
| 220 |
+
|
| 221 |
+
log_level: str = Field(default="INFO", env="LOG_LEVEL")
|
| 222 |
+
log_format: str = Field(
|
| 223 |
+
default="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 224 |
+
env="LOG_FORMAT"
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# File logging
|
| 228 |
+
log_to_file: bool = Field(default=True, env="LOG_TO_FILE")
|
| 229 |
+
log_file_path: str = Field(default="logs/saap.log", env="LOG_FILE_PATH")
|
| 230 |
+
log_file_max_size: int = Field(default=10485760, env="LOG_FILE_MAX_SIZE") # 10MB
|
| 231 |
+
log_file_backup_count: int = Field(default=5, env="LOG_FILE_BACKUP_COUNT")
|
| 232 |
+
|
| 233 |
+
# Cost & Performance Logging
|
| 234 |
+
log_cost_metrics: bool = Field(default=True, env="LOG_COST_METRICS")
|
| 235 |
+
cost_log_path: str = Field(default="logs/saap_costs.log", env="COST_LOG_PATH")
|
| 236 |
+
performance_log_path: str = Field(default="logs/saap_performance.log", env="PERFORMANCE_LOG_PATH")
|
| 237 |
+
|
| 238 |
+
model_config = {
|
| 239 |
+
"env_file": ".env",
|
| 240 |
+
"env_file_encoding": "utf-8",
|
| 241 |
+
"case_sensitive": False,
|
| 242 |
+
"extra": "allow" # Allow extra fields
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
class AgentSettings(BaseSettings):
|
| 246 |
+
"""Enhanced agent-specific configuration with multi-provider support"""
|
| 247 |
+
|
| 248 |
+
default_agent_timeout: int = Field(default=60, env="DEFAULT_AGENT_TIMEOUT")
|
| 249 |
+
max_concurrent_agents: int = Field(default=10, env="MAX_CONCURRENT_AGENTS")
|
| 250 |
+
agent_health_check_interval: int = Field(default=300, env="AGENT_HEALTH_CHECK_INTERVAL") # 5 minutes
|
| 251 |
+
|
| 252 |
+
# Performance settings
|
| 253 |
+
max_message_history: int = Field(default=1000, env="MAX_MESSAGE_HISTORY")
|
| 254 |
+
cleanup_old_messages_days: int = Field(default=30, env="CLEANUP_OLD_MESSAGES_DAYS")
|
| 255 |
+
|
| 256 |
+
# Multi-Provider Strategy
|
| 257 |
+
primary_provider: str = Field(default="colossus", env="PRIMARY_PROVIDER") # colossus, openrouter
|
| 258 |
+
fallback_provider: str = Field(default="openrouter", env="FALLBACK_PROVIDER")
|
| 259 |
+
auto_fallback_on_error: bool = Field(default=True, env="AUTO_FALLBACK_ON_ERROR")
|
| 260 |
+
fallback_timeout_threshold: int = Field(default=30, env="FALLBACK_TIMEOUT_THRESHOLD") # seconds
|
| 261 |
+
|
| 262 |
+
# Cost Efficiency Targets
|
| 263 |
+
target_response_time: float = Field(default=2.0, env="TARGET_RESPONSE_TIME") # seconds
|
| 264 |
+
target_cost_per_request: float = Field(default=0.002, env="TARGET_COST_PER_REQUEST") # $0.002
|
| 265 |
+
cost_vs_speed_priority: str = Field(default="balanced", env="COST_VS_SPEED_PRIORITY") # cost, speed, balanced
|
| 266 |
+
|
| 267 |
+
# Daily Cost Budgets
|
| 268 |
+
daily_cost_budget: float = Field(default=10.0, env="DAILY_COST_BUDGET") # $10/day
|
| 269 |
+
agent_cost_budget: float = Field(default=2.0, env="AGENT_COST_BUDGET") # $2/agent/day
|
| 270 |
+
warning_cost_threshold: float = Field(default=0.80, env="WARNING_COST_THRESHOLD") # 80%
|
| 271 |
+
|
| 272 |
+
# Model Selection Strategy
|
| 273 |
+
use_free_models_first: bool = Field(default=False, env="USE_FREE_MODELS_FIRST")
|
| 274 |
+
smart_model_selection: bool = Field(default=True, env="SMART_MODEL_SELECTION")
|
| 275 |
+
cost_learning_enabled: bool = Field(default=True, env="COST_LEARNING_ENABLED")
|
| 276 |
+
|
| 277 |
+
model_config = {
|
| 278 |
+
"env_file": ".env",
|
| 279 |
+
"env_file_encoding": "utf-8",
|
| 280 |
+
"case_sensitive": False,
|
| 281 |
+
"extra": "allow" # Allow extra fields
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
@field_validator('primary_provider', 'fallback_provider')
|
| 285 |
+
def validate_provider(cls, v):
|
| 286 |
+
"""Validate provider names"""
|
| 287 |
+
allowed = ['colossus', 'openrouter']
|
| 288 |
+
if v not in allowed:
|
| 289 |
+
raise ValueError(f'Provider must be one of: {allowed}')
|
| 290 |
+
return v
|
| 291 |
+
|
| 292 |
+
@field_validator('cost_vs_speed_priority')
|
| 293 |
+
def validate_priority(cls, v):
|
| 294 |
+
"""Validate priority setting"""
|
| 295 |
+
allowed = ['cost', 'speed', 'balanced']
|
| 296 |
+
if v not in allowed:
|
| 297 |
+
raise ValueError(f'Priority must be one of: {allowed}')
|
| 298 |
+
return v
|
| 299 |
+
|
| 300 |
+
class SaapSettings(BaseSettings):
|
| 301 |
+
"""Main SAAP Application Settings with OpenRouter Integration"""
|
| 302 |
+
|
| 303 |
+
# Application Info
|
| 304 |
+
app_name: str = Field(default="SAAP - satware AI Autonomous Agent Platform", env="APP_NAME")
|
| 305 |
+
app_version: str = Field(default="1.0.0", env="APP_VERSION")
|
| 306 |
+
environment: str = Field(default="production", env="ENVIRONMENT")
|
| 307 |
+
debug: bool = Field(default=False, env="DEBUG")
|
| 308 |
+
|
| 309 |
+
# Server settings - Updated for network access
|
| 310 |
+
host: str = Field(default="100.64.0.45", env="HOST") # 🌐 Changed from 0.0.0.0
|
| 311 |
+
port: int = Field(default=8000, env="PORT")
|
| 312 |
+
reload: bool = Field(default=False, env="RELOAD")
|
| 313 |
+
|
| 314 |
+
# Component Settings
|
| 315 |
+
database: DatabaseSettings = DatabaseSettings()
|
| 316 |
+
colossus: ColossusSettings = ColossusSettings()
|
| 317 |
+
openrouter: OpenRouterSettings = OpenRouterSettings() # NEW!
|
| 318 |
+
redis: RedisSettings = RedisSettings()
|
| 319 |
+
security: SecuritySettings = SecuritySettings()
|
| 320 |
+
logging: LoggingSettings = LoggingSettings()
|
| 321 |
+
agents: AgentSettings = AgentSettings()
|
| 322 |
+
|
| 323 |
+
model_config = {
|
| 324 |
+
"env_file": ".env",
|
| 325 |
+
"env_file_encoding": "utf-8",
|
| 326 |
+
"case_sensitive": False,
|
| 327 |
+
"extra": "allow" # Allow extra fields from .env that aren't explicitly defined
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
@field_validator('environment')
|
| 331 |
+
def validate_environment(cls, v):
|
| 332 |
+
"""Validate environment setting"""
|
| 333 |
+
allowed_envs = ['development', 'staging', 'production', 'testing']
|
| 334 |
+
if v.lower() not in allowed_envs:
|
| 335 |
+
raise ValueError(f'Environment must be one of: {allowed_envs}')
|
| 336 |
+
return v.lower()
|
| 337 |
+
|
| 338 |
+
def get_database_url(self) -> str:
|
| 339 |
+
"""Get database URL with environment-specific adjustments"""
|
| 340 |
+
if self.environment == 'testing':
|
| 341 |
+
return "sqlite:///./test_saap.db"
|
| 342 |
+
elif self.environment == 'development':
|
| 343 |
+
return "sqlite:///./saap_dev.db"
|
| 344 |
+
else:
|
| 345 |
+
return self.database.database_url
|
| 346 |
+
|
| 347 |
+
def is_production(self) -> bool:
|
| 348 |
+
"""Check if running in production"""
|
| 349 |
+
return self.environment == 'production'
|
| 350 |
+
|
| 351 |
+
def get_cors_origins(self) -> List[str]:
|
| 352 |
+
"""Get CORS allowed origins as list"""
|
| 353 |
+
return self.security.get_allowed_origins_list()
|
| 354 |
+
|
| 355 |
+
def get_log_config(self) -> dict:
|
| 356 |
+
"""Get logging configuration for uvicorn/fastapi"""
|
| 357 |
+
return {
|
| 358 |
+
"version": 1,
|
| 359 |
+
"disable_existing_loggers": False,
|
| 360 |
+
"formatters": {
|
| 361 |
+
"default": {
|
| 362 |
+
"format": self.logging.log_format,
|
| 363 |
+
},
|
| 364 |
+
"cost": {
|
| 365 |
+
"format": "%(asctime)s - COST - %(message)s",
|
| 366 |
+
},
|
| 367 |
+
"performance": {
|
| 368 |
+
"format": "%(asctime)s - PERF - %(message)s",
|
| 369 |
+
}
|
| 370 |
+
},
|
| 371 |
+
"handlers": {
|
| 372 |
+
"default": {
|
| 373 |
+
"formatter": "default",
|
| 374 |
+
"class": "logging.StreamHandler",
|
| 375 |
+
"stream": "ext://sys.stdout",
|
| 376 |
+
},
|
| 377 |
+
"file": {
|
| 378 |
+
"formatter": "default",
|
| 379 |
+
"class": "logging.handlers.RotatingFileHandler",
|
| 380 |
+
"filename": self.logging.log_file_path,
|
| 381 |
+
"maxBytes": self.logging.log_file_max_size,
|
| 382 |
+
"backupCount": self.logging.log_file_backup_count,
|
| 383 |
+
} if self.logging.log_to_file else None,
|
| 384 |
+
"cost_file": {
|
| 385 |
+
"formatter": "cost",
|
| 386 |
+
"class": "logging.handlers.RotatingFileHandler",
|
| 387 |
+
"filename": self.logging.cost_log_path,
|
| 388 |
+
"maxBytes": self.logging.log_file_max_size,
|
| 389 |
+
"backupCount": self.logging.log_file_backup_count,
|
| 390 |
+
} if self.logging.log_cost_metrics else None,
|
| 391 |
+
"performance_file": {
|
| 392 |
+
"formatter": "performance",
|
| 393 |
+
"class": "logging.handlers.RotatingFileHandler",
|
| 394 |
+
"filename": self.logging.performance_log_path,
|
| 395 |
+
"maxBytes": self.logging.log_file_max_size,
|
| 396 |
+
"backupCount": self.logging.log_file_backup_count,
|
| 397 |
+
} if self.logging.log_cost_metrics else None
|
| 398 |
+
},
|
| 399 |
+
"loggers": {
|
| 400 |
+
"": {
|
| 401 |
+
"handlers": [h for h in ["default", "file"] if h],
|
| 402 |
+
"level": self.logging.log_level,
|
| 403 |
+
},
|
| 404 |
+
"saap.cost": {
|
| 405 |
+
"handlers": [h for h in ["cost_file"] if h],
|
| 406 |
+
"level": "INFO",
|
| 407 |
+
"propagate": False,
|
| 408 |
+
},
|
| 409 |
+
"saap.performance": {
|
| 410 |
+
"handlers": [h for h in ["performance_file"] if h],
|
| 411 |
+
"level": "INFO",
|
| 412 |
+
"propagate": False,
|
| 413 |
+
}
|
| 414 |
+
},
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
def get_openrouter_config_for_agent(self, agent_name: str) -> dict:
|
| 418 |
+
"""Get OpenRouter configuration for specific agent"""
|
| 419 |
+
return self.openrouter.get_agent_model_config(agent_name)
|
| 420 |
+
|
| 421 |
+
@lru_cache()
|
| 422 |
+
def get_settings() -> SaapSettings:
|
| 423 |
+
"""Get cached settings instance"""
|
| 424 |
+
return SaapSettings()
|
| 425 |
+
|
| 426 |
+
# Global settings instance
|
| 427 |
+
settings = get_settings()
|
| 428 |
+
|
| 429 |
+
# Create logs directory if needed
|
| 430 |
+
if settings.logging.log_to_file:
|
| 431 |
+
Path(settings.logging.log_file_path).parent.mkdir(parents=True, exist_ok=True)
|
| 432 |
+
if settings.logging.log_cost_metrics:
|
| 433 |
+
Path(settings.logging.cost_log_path).parent.mkdir(parents=True, exist_ok=True)
|
| 434 |
+
Path(settings.logging.performance_log_path).parent.mkdir(parents=True, exist_ok=True)
|
| 435 |
+
|
| 436 |
+
# Environment-specific logging setup
|
| 437 |
+
def setup_logging():
|
| 438 |
+
"""Setup logging configuration"""
|
| 439 |
+
import logging.config
|
| 440 |
+
|
| 441 |
+
log_config = settings.get_log_config()
|
| 442 |
+
logging.config.dictConfig(log_config)
|
| 443 |
+
|
| 444 |
+
logger = logging.getLogger(__name__)
|
| 445 |
+
|
| 446 |
+
logger.info(f"🚀 SAAP Configuration loaded:")
|
| 447 |
+
logger.info(f" Environment: {settings.environment}")
|
| 448 |
+
logger.info(f" Database: {settings.get_database_url()}")
|
| 449 |
+
logger.info(f" colossus: {settings.colossus.api_base}")
|
| 450 |
+
logger.info(f" OpenRouter: {settings.openrouter.enabled}")
|
| 451 |
+
logger.info(f" Redis: {settings.redis.host}:{settings.redis.port}")
|
| 452 |
+
logger.info(f" Debug Mode: {settings.debug}")
|
| 453 |
+
logger.info(f"🌐 Server Host: {settings.host}:{settings.port}") # Log the network settings
|
| 454 |
+
logger.info(f"🔒 CORS Origins: {settings.get_cors_origins()}")
|
| 455 |
+
|
| 456 |
+
# Cost tracking logger
|
| 457 |
+
if settings.logging.log_cost_metrics:
|
| 458 |
+
cost_logger = logging.getLogger("saap.cost")
|
| 459 |
+
cost_logger.info("💰 Cost tracking initialized")
|
| 460 |
+
|
| 461 |
+
performance_logger = logging.getLogger("saap.performance")
|
| 462 |
+
performance_logger.info("📊 Performance monitoring initialized")
|
| 463 |
+
|
| 464 |
+
if __name__ == "__main__":
|
| 465 |
+
# Test configuration
|
| 466 |
+
setup_logging()
|
| 467 |
+
logger = logging.getLogger(__name__)
|
| 468 |
+
|
| 469 |
+
logger.info("🧪 Configuration Test:")
|
| 470 |
+
logger.info(f" App Name: {settings.app_name}")
|
| 471 |
+
logger.info(f" Database URL: {settings.get_database_url()}")
|
| 472 |
+
logger.info(f" Production Mode: {settings.is_production()}")
|
| 473 |
+
logger.info(f" OpenRouter Enabled: {settings.openrouter.enabled}")
|
| 474 |
+
logger.info(f"🌐 Network Settings:")
|
| 475 |
+
logger.info(f" Host: {settings.host}")
|
| 476 |
+
logger.info(f" Port: {settings.port}")
|
| 477 |
+
logger.info(f" CORS Origins: {settings.get_cors_origins()}")
|
| 478 |
+
|
| 479 |
+
# Test agent model configs
|
| 480 |
+
for agent in ['jane_alesi', 'john_alesi', 'lara_alesi']:
|
| 481 |
+
config = settings.get_openrouter_config_for_agent(agent)
|
| 482 |
+
logger.info(f" {agent}: {config['model']} (${config['cost_per_1m']}/1M tokens)")
|
backend/connection.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP Database Connection Management - Production Ready
|
| 3 |
+
SQLAlchemy database connection, session management, and health monitoring
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import logging
|
| 8 |
+
from contextlib import asynccontextmanager
|
| 9 |
+
from sqlalchemy import create_engine, text, event
|
| 10 |
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
| 11 |
+
from sqlalchemy.orm import sessionmaker, Session
|
| 12 |
+
from sqlalchemy.pool import QueuePool, NullPool, AsyncAdaptedQueuePool
|
| 13 |
+
from sqlalchemy.exc import SQLAlchemyError, OperationalError
|
| 14 |
+
from typing import AsyncGenerator, Optional, Dict, Any
|
| 15 |
+
from datetime import datetime, timedelta
|
| 16 |
+
|
| 17 |
+
from config.settings import settings
|
| 18 |
+
from database.models import Base, DBHealthCheck
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
class DatabaseManager:
|
| 23 |
+
"""
|
| 24 |
+
Production-ready database connection manager
|
| 25 |
+
Features:
|
| 26 |
+
- Connection pooling with health monitoring
|
| 27 |
+
- Async and sync session management
|
| 28 |
+
- Automatic retry and fallback mechanisms
|
| 29 |
+
- Database health checks and metrics
|
| 30 |
+
- Migration support
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
def __init__(self):
|
| 34 |
+
self.engine = None
|
| 35 |
+
self.async_engine = None
|
| 36 |
+
self.SessionLocal = None
|
| 37 |
+
self.AsyncSessionLocal = None
|
| 38 |
+
self.is_initialized = False
|
| 39 |
+
self.last_health_check = None
|
| 40 |
+
self.health_status = {"status": "initializing"}
|
| 41 |
+
|
| 42 |
+
def _get_sync_engine_kwargs(self) -> Dict[str, Any]:
|
| 43 |
+
"""Get sync engine configuration based on database type"""
|
| 44 |
+
database_url = settings.get_database_url()
|
| 45 |
+
|
| 46 |
+
base_kwargs = {
|
| 47 |
+
"echo": settings.debug,
|
| 48 |
+
"future": True
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if database_url.startswith("sqlite"):
|
| 52 |
+
# SQLite-specific configuration
|
| 53 |
+
base_kwargs.update({
|
| 54 |
+
"poolclass": NullPool, # SQLite doesn't need connection pooling
|
| 55 |
+
"connect_args": {
|
| 56 |
+
"check_same_thread": settings.database.sqlite_check_same_thread
|
| 57 |
+
}
|
| 58 |
+
})
|
| 59 |
+
else:
|
| 60 |
+
# PostgreSQL/MySQL configuration with connection pooling
|
| 61 |
+
base_kwargs.update({
|
| 62 |
+
"poolclass": QueuePool, # Use QueuePool for sync engines
|
| 63 |
+
"pool_size": settings.database.pool_size,
|
| 64 |
+
"max_overflow": settings.database.max_overflow,
|
| 65 |
+
"pool_timeout": settings.database.pool_timeout,
|
| 66 |
+
"pool_recycle": settings.database.pool_recycle,
|
| 67 |
+
"pool_pre_ping": True # Verify connections before use
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
return base_kwargs
|
| 71 |
+
|
| 72 |
+
def _get_async_engine_kwargs(self) -> Dict[str, Any]:
|
| 73 |
+
"""Get async engine configuration based on database type"""
|
| 74 |
+
database_url = settings.get_database_url()
|
| 75 |
+
|
| 76 |
+
base_kwargs = {
|
| 77 |
+
"echo": settings.debug,
|
| 78 |
+
"future": True
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if database_url.startswith("sqlite"):
|
| 82 |
+
# SQLite-specific configuration for async
|
| 83 |
+
base_kwargs.update({
|
| 84 |
+
"poolclass": NullPool, # SQLite doesn't need connection pooling
|
| 85 |
+
})
|
| 86 |
+
else:
|
| 87 |
+
# PostgreSQL/MySQL configuration with async connection pooling
|
| 88 |
+
base_kwargs.update({
|
| 89 |
+
"poolclass": AsyncAdaptedQueuePool, # Use AsyncAdaptedQueuePool for async engines
|
| 90 |
+
"pool_size": settings.database.pool_size,
|
| 91 |
+
"max_overflow": settings.database.max_overflow,
|
| 92 |
+
"pool_timeout": settings.database.pool_timeout,
|
| 93 |
+
"pool_recycle": settings.database.pool_recycle,
|
| 94 |
+
"pool_pre_ping": True # Verify connections before use
|
| 95 |
+
})
|
| 96 |
+
|
| 97 |
+
return base_kwargs
|
| 98 |
+
|
| 99 |
+
def _setup_sql_logging(self, engine):
|
| 100 |
+
"""Setup SQL logging for debugging"""
|
| 101 |
+
if settings.debug:
|
| 102 |
+
@event.listens_for(engine, "before_cursor_execute")
|
| 103 |
+
def log_sql(conn, cursor, statement, parameters, context, executemany):
|
| 104 |
+
"""Log SQL statements in debug mode"""
|
| 105 |
+
logger.debug(f"SQL: {statement}")
|
| 106 |
+
if parameters:
|
| 107 |
+
logger.debug(f"Parameters: {parameters}")
|
| 108 |
+
|
| 109 |
+
async def initialize(self):
|
| 110 |
+
"""Initialize database connections and create tables"""
|
| 111 |
+
try:
|
| 112 |
+
logger.info("🚀 Initializing SAAP Database Connection...")
|
| 113 |
+
|
| 114 |
+
database_url = settings.get_database_url()
|
| 115 |
+
|
| 116 |
+
# Create sync engine for migrations and admin tasks
|
| 117 |
+
sync_kwargs = self._get_sync_engine_kwargs()
|
| 118 |
+
self.engine = create_engine(database_url, **sync_kwargs)
|
| 119 |
+
|
| 120 |
+
# Setup SQL logging for sync engine
|
| 121 |
+
self._setup_sql_logging(self.engine)
|
| 122 |
+
|
| 123 |
+
# Create async engine for main application
|
| 124 |
+
async_url = database_url.replace("sqlite://", "sqlite+aiosqlite://")
|
| 125 |
+
if not database_url.startswith("sqlite"):
|
| 126 |
+
# For PostgreSQL: replace postgresql:// with postgresql+asyncpg://
|
| 127 |
+
async_url = database_url.replace("postgresql://", "postgresql+asyncpg://")
|
| 128 |
+
|
| 129 |
+
async_kwargs = self._get_async_engine_kwargs()
|
| 130 |
+
self.async_engine = create_async_engine(async_url, **async_kwargs)
|
| 131 |
+
|
| 132 |
+
# Setup SQL logging for async engine
|
| 133 |
+
self._setup_sql_logging(self.async_engine.sync_engine)
|
| 134 |
+
|
| 135 |
+
# Create session factories
|
| 136 |
+
self.SessionLocal = sessionmaker(
|
| 137 |
+
bind=self.engine,
|
| 138 |
+
autocommit=False,
|
| 139 |
+
autoflush=False,
|
| 140 |
+
expire_on_commit=False
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
self.AsyncSessionLocal = async_sessionmaker(
|
| 144 |
+
bind=self.async_engine,
|
| 145 |
+
class_=AsyncSession,
|
| 146 |
+
autocommit=False,
|
| 147 |
+
autoflush=False,
|
| 148 |
+
expire_on_commit=False
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# Create database tables
|
| 152 |
+
await self._create_tables()
|
| 153 |
+
|
| 154 |
+
# Perform initial health check
|
| 155 |
+
await self._update_health_status()
|
| 156 |
+
|
| 157 |
+
self.is_initialized = True
|
| 158 |
+
logger.info(f"✅ Database initialized successfully: {database_url}")
|
| 159 |
+
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logger.error(f"❌ Database initialization failed: {e}")
|
| 162 |
+
self.health_status = {"status": "failed", "error": str(e)}
|
| 163 |
+
raise
|
| 164 |
+
|
| 165 |
+
async def _create_tables(self):
|
| 166 |
+
"""Create database tables if they don't exist"""
|
| 167 |
+
try:
|
| 168 |
+
if settings.debug:
|
| 169 |
+
logger.debug("🔧 Creating database tables...")
|
| 170 |
+
|
| 171 |
+
if settings.get_database_url().startswith("sqlite"):
|
| 172 |
+
# For SQLite, use sync engine
|
| 173 |
+
Base.metadata.create_all(bind=self.engine)
|
| 174 |
+
logger.info("✅ Database tables created (SQLite)")
|
| 175 |
+
else:
|
| 176 |
+
# For PostgreSQL/MySQL, use async engine
|
| 177 |
+
async with self.async_engine.begin() as conn:
|
| 178 |
+
await conn.run_sync(Base.metadata.create_all)
|
| 179 |
+
logger.info("✅ Database tables created (Async)")
|
| 180 |
+
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logger.error(f"❌ Failed to create database tables: {e}")
|
| 183 |
+
raise
|
| 184 |
+
|
| 185 |
+
@asynccontextmanager
|
| 186 |
+
async def get_async_session(self) -> AsyncGenerator[AsyncSession, None]:
|
| 187 |
+
"""Get async database session with automatic cleanup"""
|
| 188 |
+
if not self.is_initialized:
|
| 189 |
+
await self.initialize()
|
| 190 |
+
|
| 191 |
+
session = self.AsyncSessionLocal()
|
| 192 |
+
try:
|
| 193 |
+
yield session
|
| 194 |
+
await session.commit()
|
| 195 |
+
except Exception as e:
|
| 196 |
+
await session.rollback()
|
| 197 |
+
logger.error(f"❌ Database session error: {e}")
|
| 198 |
+
raise
|
| 199 |
+
finally:
|
| 200 |
+
await session.close()
|
| 201 |
+
|
| 202 |
+
def get_sync_session(self) -> Session:
|
| 203 |
+
"""Get sync database session (for migrations and admin tasks)"""
|
| 204 |
+
if not self.engine:
|
| 205 |
+
raise RuntimeError("Database not initialized")
|
| 206 |
+
return self.SessionLocal()
|
| 207 |
+
|
| 208 |
+
async def _update_health_status(self):
|
| 209 |
+
"""Update database health monitoring status"""
|
| 210 |
+
try:
|
| 211 |
+
start_time = datetime.utcnow()
|
| 212 |
+
|
| 213 |
+
# Test database connectivity
|
| 214 |
+
async with self.get_async_session() as session:
|
| 215 |
+
result = await session.execute(text("SELECT 1"))
|
| 216 |
+
result.fetchone()
|
| 217 |
+
|
| 218 |
+
end_time = datetime.utcnow()
|
| 219 |
+
response_time = (end_time - start_time).total_seconds() * 1000 # Convert to milliseconds
|
| 220 |
+
|
| 221 |
+
# Get agent count - with proper error handling
|
| 222 |
+
agent_count = 0
|
| 223 |
+
active_agent_count = 0
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
async with self.get_async_session() as session:
|
| 227 |
+
# Check if agents table exists first
|
| 228 |
+
check_table_query = text("""
|
| 229 |
+
SELECT COUNT(*) FROM information_schema.tables
|
| 230 |
+
WHERE table_name = 'agents'
|
| 231 |
+
""")
|
| 232 |
+
|
| 233 |
+
# For SQLite, use different query
|
| 234 |
+
if settings.get_database_url().startswith("sqlite"):
|
| 235 |
+
check_table_query = text("""
|
| 236 |
+
SELECT COUNT(*) FROM sqlite_master
|
| 237 |
+
WHERE type='table' AND name='agents'
|
| 238 |
+
""")
|
| 239 |
+
|
| 240 |
+
table_exists_result = await session.execute(check_table_query)
|
| 241 |
+
table_exists = table_exists_result.scalar() > 0
|
| 242 |
+
|
| 243 |
+
if table_exists:
|
| 244 |
+
agent_count_result = await session.execute(text("SELECT COUNT(*) FROM agents"))
|
| 245 |
+
agent_count = agent_count_result.scalar()
|
| 246 |
+
|
| 247 |
+
active_agent_count_result = await session.execute(
|
| 248 |
+
text("SELECT COUNT(*) FROM agents WHERE status = 'active'")
|
| 249 |
+
)
|
| 250 |
+
active_agent_count = active_agent_count_result.scalar()
|
| 251 |
+
|
| 252 |
+
except Exception as table_error:
|
| 253 |
+
logger.debug(f"Tables not ready yet: {table_error}")
|
| 254 |
+
# This is expected during initial setup
|
| 255 |
+
|
| 256 |
+
self.health_status = {
|
| 257 |
+
"status": "healthy",
|
| 258 |
+
"database_type": settings.get_database_url().split("://")[0],
|
| 259 |
+
"response_time_ms": response_time,
|
| 260 |
+
"agent_count": agent_count,
|
| 261 |
+
"active_agent_count": active_agent_count,
|
| 262 |
+
"connection_pool": {
|
| 263 |
+
"size": getattr(self.async_engine.pool, 'size', 0) if self.async_engine else 0,
|
| 264 |
+
"checked_out": getattr(self.async_engine.pool, 'checked_out', 0) if self.async_engine else 0
|
| 265 |
+
},
|
| 266 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
self.last_health_check = datetime.utcnow()
|
| 270 |
+
|
| 271 |
+
# Save health check to database (optional)
|
| 272 |
+
await self._save_health_check(response_time, agent_count, active_agent_count)
|
| 273 |
+
|
| 274 |
+
except Exception as e:
|
| 275 |
+
logger.error(f"❌ Database health check failed: {e}")
|
| 276 |
+
self.health_status = {
|
| 277 |
+
"status": "error",
|
| 278 |
+
"error": str(e),
|
| 279 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
async def _save_health_check(self, response_time: float, agent_count: int, active_agent_count: int):
|
| 283 |
+
"""Save health check result to database"""
|
| 284 |
+
try:
|
| 285 |
+
async with self.get_async_session() as session:
|
| 286 |
+
health_check = DBHealthCheck(
|
| 287 |
+
component="database",
|
| 288 |
+
status=self.health_status["status"],
|
| 289 |
+
response_time_ms=response_time,
|
| 290 |
+
agent_count=agent_count,
|
| 291 |
+
active_agent_count=active_agent_count,
|
| 292 |
+
details={"database_type": settings.get_database_url().split("://")[0]}
|
| 293 |
+
)
|
| 294 |
+
session.add(health_check)
|
| 295 |
+
await session.commit()
|
| 296 |
+
|
| 297 |
+
except Exception as e:
|
| 298 |
+
logger.warning(f"⚠️ Failed to save health check: {e}")
|
| 299 |
+
|
| 300 |
+
async def health_check(self) -> Dict[str, Any]:
|
| 301 |
+
"""Get current database health status"""
|
| 302 |
+
# Update health status if it's been more than 30 seconds
|
| 303 |
+
if not self.last_health_check or (datetime.utcnow() - self.last_health_check).seconds > 30:
|
| 304 |
+
await self._update_health_status()
|
| 305 |
+
|
| 306 |
+
return self.health_status
|
| 307 |
+
|
| 308 |
+
async def get_performance_metrics(self) -> Dict[str, Any]:
|
| 309 |
+
"""Get database performance metrics"""
|
| 310 |
+
try:
|
| 311 |
+
async with self.get_async_session() as session:
|
| 312 |
+
# Get recent health checks
|
| 313 |
+
recent_checks_query = text("""
|
| 314 |
+
SELECT * FROM health_checks
|
| 315 |
+
WHERE component = 'database'
|
| 316 |
+
ORDER BY created_at DESC
|
| 317 |
+
LIMIT 10
|
| 318 |
+
""")
|
| 319 |
+
|
| 320 |
+
recent_checks = await session.execute(recent_checks_query)
|
| 321 |
+
checks = recent_checks.fetchall()
|
| 322 |
+
|
| 323 |
+
if checks:
|
| 324 |
+
avg_response_time = sum(check.response_time_ms for check in checks) / len(checks)
|
| 325 |
+
latest_check = checks[0]
|
| 326 |
+
|
| 327 |
+
return {
|
| 328 |
+
"average_response_time_ms": avg_response_time,
|
| 329 |
+
"latest_agent_count": latest_check.agent_count,
|
| 330 |
+
"latest_active_agents": latest_check.active_agent_count,
|
| 331 |
+
"health_checks_count": len(checks),
|
| 332 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 333 |
+
}
|
| 334 |
+
else:
|
| 335 |
+
return {"message": "No performance data available"}
|
| 336 |
+
|
| 337 |
+
except Exception as e:
|
| 338 |
+
logger.error(f"❌ Failed to get performance metrics: {e}")
|
| 339 |
+
return {"error": str(e)}
|
| 340 |
+
|
| 341 |
+
async def cleanup_old_data(self, days: int = 30):
|
| 342 |
+
"""Clean up old data from database"""
|
| 343 |
+
try:
|
| 344 |
+
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
| 345 |
+
|
| 346 |
+
async with self.get_async_session() as session:
|
| 347 |
+
# Clean old chat messages
|
| 348 |
+
await session.execute(
|
| 349 |
+
text("DELETE FROM chat_messages WHERE created_at < :cutoff_date"),
|
| 350 |
+
{"cutoff_date": cutoff_date}
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
# Clean old health checks
|
| 354 |
+
await session.execute(
|
| 355 |
+
text("DELETE FROM health_checks WHERE created_at < :cutoff_date"),
|
| 356 |
+
{"cutoff_date": cutoff_date}
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# Clean old system logs
|
| 360 |
+
await session.execute(
|
| 361 |
+
text("DELETE FROM system_logs WHERE created_at < :cutoff_date"),
|
| 362 |
+
{"cutoff_date": cutoff_date}
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
await session.commit()
|
| 366 |
+
|
| 367 |
+
logger.info(f"✅ Cleaned up data older than {days} days")
|
| 368 |
+
|
| 369 |
+
except Exception as e:
|
| 370 |
+
logger.error(f"❌ Data cleanup failed: {e}")
|
| 371 |
+
|
| 372 |
+
async def close(self):
|
| 373 |
+
"""Close database connections"""
|
| 374 |
+
try:
|
| 375 |
+
logger.info("🔧 Closing database connections...")
|
| 376 |
+
|
| 377 |
+
if self.async_engine:
|
| 378 |
+
await self.async_engine.dispose()
|
| 379 |
+
|
| 380 |
+
if self.engine:
|
| 381 |
+
self.engine.dispose()
|
| 382 |
+
|
| 383 |
+
self.is_initialized = False
|
| 384 |
+
logger.info("✅ Database connections closed")
|
| 385 |
+
|
| 386 |
+
except Exception as e:
|
| 387 |
+
logger.error(f"❌ Error closing database: {e}")
|
| 388 |
+
|
| 389 |
+
# Global database manager instance
|
| 390 |
+
db_manager = DatabaseManager()
|
| 391 |
+
|
| 392 |
+
# Convenience functions for dependency injection
|
| 393 |
+
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
| 394 |
+
"""FastAPI dependency for getting database session"""
|
| 395 |
+
async with db_manager.get_async_session() as session:
|
| 396 |
+
yield session
|
| 397 |
+
|
| 398 |
+
def get_sync_db_session() -> Session:
|
| 399 |
+
"""Get synchronous database session"""
|
| 400 |
+
return db_manager.get_sync_session()
|
| 401 |
+
|
| 402 |
+
if __name__ == "__main__":
|
| 403 |
+
async def test_database():
|
| 404 |
+
"""Test database connectivity"""
|
| 405 |
+
await db_manager.initialize()
|
| 406 |
+
|
| 407 |
+
health = await db_manager.health_check()
|
| 408 |
+
print(f"🔍 Database Health: {health}")
|
| 409 |
+
|
| 410 |
+
metrics = await db_manager.get_performance_metrics()
|
| 411 |
+
print(f"📊 Performance Metrics: {metrics}")
|
| 412 |
+
|
| 413 |
+
await db_manager.close()
|
| 414 |
+
|
| 415 |
+
asyncio.run(test_database())
|
backend/cost_efficiency_logger.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SAAP Cost Efficiency Logger - Advanced Cost Tracking & Analytics
|
| 3 |
+
Monitors OpenRouter costs, performance metrics, and budget management
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
from typing import Dict, List, Optional, Any
|
| 11 |
+
from dataclasses import dataclass, asdict
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import sqlite3
|
| 14 |
+
import aiosqlite
|
| 15 |
+
from collections import defaultdict
|
| 16 |
+
|
| 17 |
+
from ..config.settings import get_settings
|
| 18 |
+
|
| 19 |
+
# Initialize cost logging
|
| 20 |
+
cost_logger = logging.getLogger("saap.cost")
|
| 21 |
+
performance_logger = logging.getLogger("saap.performance")
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class CostAnalytics:
|
| 25 |
+
"""Comprehensive cost analytics"""
|
| 26 |
+
time_period: str
|
| 27 |
+
total_cost_usd: float
|
| 28 |
+
total_requests: int
|
| 29 |
+
successful_requests: int
|
| 30 |
+
failed_requests: int
|
| 31 |
+
average_cost_per_request: float
|
| 32 |
+
total_tokens: int
|
| 33 |
+
average_response_time: float
|
| 34 |
+
cost_per_1k_tokens: float
|
| 35 |
+
tokens_per_second: float
|
| 36 |
+
top_expensive_models: List[Dict[str, Any]]
|
| 37 |
+
cost_by_agent: Dict[str, float]
|
| 38 |
+
cost_by_provider: Dict[str, float]
|
| 39 |
+
daily_budget_utilization: float
|
| 40 |
+
cost_trend_24h: List[Dict[str, Any]]
|
| 41 |
+
efficiency_score: float # Tokens per dollar
|
| 42 |
+
|
| 43 |
+
@dataclass
|
| 44 |
+
class PerformanceBenchmark:
|
| 45 |
+
"""Performance benchmarking data"""
|
| 46 |
+
provider: str
|
| 47 |
+
model: str
|
| 48 |
+
avg_response_time: float
|
| 49 |
+
tokens_per_second: float
|
| 50 |
+
cost_per_token: float
|
| 51 |
+
success_rate: float
|
| 52 |
+
cost_efficiency_score: float
|
| 53 |
+
sample_size: int
|
| 54 |
+
|
| 55 |
+
class CostEfficiencyLogger:
|
| 56 |
+
"""Advanced cost tracking and analytics system"""
|
| 57 |
+
|
| 58 |
+
def __init__(self):
|
| 59 |
+
self.settings = get_settings()
|
| 60 |
+
self.cost_db_path = "logs/saap_cost_tracking.db"
|
| 61 |
+
self.analytics_cache = {}
|
| 62 |
+
self.cost_alerts = []
|
| 63 |
+
|
| 64 |
+
# Ensure logs directory exists
|
| 65 |
+
Path("logs").mkdir(exist_ok=True)
|
| 66 |
+
|
| 67 |
+
# Initialize database
|
| 68 |
+
asyncio.create_task(self._initialize_database())
|
| 69 |
+
|
| 70 |
+
cost_logger.info("💰 Cost Efficiency Logger initialized")
|
| 71 |
+
|
| 72 |
+
async def _initialize_database(self):
|
| 73 |
+
"""Initialize SQLite database for cost tracking"""
|
| 74 |
+
async with aiosqlite.connect(self.cost_db_path) as db:
|
| 75 |
+
await db.execute("""
|
| 76 |
+
CREATE TABLE IF NOT EXISTS cost_metrics (
|
| 77 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 78 |
+
timestamp TEXT NOT NULL,
|
| 79 |
+
agent_id TEXT NOT NULL,
|
| 80 |
+
provider TEXT NOT NULL,
|
| 81 |
+
model TEXT NOT NULL,
|
| 82 |
+
input_tokens INTEGER NOT NULL,
|
| 83 |
+
output_tokens INTEGER NOT NULL,
|
| 84 |
+
total_tokens INTEGER NOT NULL,
|
| 85 |
+
cost_usd REAL NOT NULL,
|
| 86 |
+
response_time_seconds REAL NOT NULL,
|
| 87 |
+
request_success BOOLEAN NOT NULL,
|
| 88 |
+
cost_per_1k_tokens REAL,
|
| 89 |
+
tokens_per_second REAL,
|
| 90 |
+
metadata TEXT
|
| 91 |
+
)
|
| 92 |
+
""")
|
| 93 |
+
|
| 94 |
+
await db.execute("""
|
| 95 |
+
CREATE INDEX IF NOT EXISTS idx_timestamp ON cost_metrics(timestamp)
|
| 96 |
+
""")
|
| 97 |
+
|
| 98 |
+
await db.execute("""
|
| 99 |
+
CREATE INDEX IF NOT EXISTS idx_agent_id ON cost_metrics(agent_id)
|
| 100 |
+
""")
|
| 101 |
+
|
| 102 |
+
await db.execute("""
|
| 103 |
+
CREATE INDEX IF NOT EXISTS idx_provider ON cost_metrics(provider)
|
| 104 |
+
""")
|
| 105 |
+
|
| 106 |
+
await db.commit()
|
| 107 |
+
|
| 108 |
+
async def log_cost_metrics(self, metrics_data: Dict[str, Any]):
|
| 109 |
+
"""Log cost metrics to database and generate analytics"""
|
| 110 |
+
|
| 111 |
+
# Calculate derived metrics
|
| 112 |
+
metrics_data['cost_per_1k_tokens'] = (
|
| 113 |
+
metrics_data['cost_usd'] / (metrics_data['total_tokens'] / 1000)
|
| 114 |
+
if metrics_data['total_tokens'] > 0 else 0
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
metrics_data['tokens_per_second'] = (
|
| 118 |
+
metrics_data['total_tokens'] / metrics_data['response_time_seconds']
|
| 119 |
+
if metrics_data['response_time_seconds'] > 0 else 0
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# Store in database
|
| 123 |
+
async with aiosqlite.connect(self.cost_db_path) as db:
|
| 124 |
+
await db.execute("""
|
| 125 |
+
INSERT INTO cost_metrics (
|
| 126 |
+
timestamp, agent_id, provider, model, input_tokens, output_tokens,
|
| 127 |
+
total_tokens, cost_usd, response_time_seconds, request_success,
|
| 128 |
+
cost_per_1k_tokens, tokens_per_second, metadata
|
| 129 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 130 |
+
""", (
|
| 131 |
+
metrics_data['timestamp'],
|
| 132 |
+
metrics_data['agent_id'],
|
| 133 |
+
metrics_data['provider'],
|
| 134 |
+
metrics_data['model'],
|
| 135 |
+
metrics_data['input_tokens'],
|
| 136 |
+
metrics_data['output_tokens'],
|
| 137 |
+
metrics_data['total_tokens'],
|
| 138 |
+
metrics_data['cost_usd'],
|
| 139 |
+
metrics_data['response_time_seconds'],
|
| 140 |
+
metrics_data['request_success'],
|
| 141 |
+
metrics_data['cost_per_1k_tokens'],
|
| 142 |
+
metrics_data['tokens_per_second'],
|
| 143 |
+
json.dumps(metrics_data.get('metadata', {}))
|
| 144 |
+
))
|
| 145 |
+
await db.commit()
|
| 146 |
+
|
| 147 |
+
# Real-time cost logging
|
| 148 |
+
if metrics_data['request_success']:
|
| 149 |
+
cost_logger.info(
|
| 150 |
+
f"💰 COST: {metrics_data['agent_id']} | "
|
| 151 |
+
f"${metrics_data['cost_usd']:.6f} | "
|
| 152 |
+
f"{metrics_data['total_tokens']} tokens | "
|
| 153 |
+
f"{metrics_data['response_time_seconds']:.2f}s | "
|
| 154 |
+
f"{metrics_data['tokens_per_second']:.1f} tok/s | "
|
| 155 |
+
f"${metrics_data['cost_per_1k_tokens']:.4f}/1k"
|
| 156 |
+
)
|
| 157 |
+
else:
|
| 158 |
+
cost_logger.error(
|
| 159 |
+
f"❌ FAILED: {metrics_data['agent_id']} | "
|
| 160 |
+
f"{metrics_data['provider']} | "
|
| 161 |
+
f"{metrics_data['response_time_seconds']:.2f}s timeout"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
# Check for budget alerts
|
| 165 |
+
await self._check_budget_alerts()
|
| 166 |
+
|
| 167 |
+
async def _check_budget_alerts(self):
|
| 168 |
+
"""Check and generate budget alerts"""
|
| 169 |
+
daily_cost = await self.get_daily_cost()
|
| 170 |
+
daily_budget = self.settings.agents.daily_cost_budget
|
| 171 |
+
usage_percentage = (daily_cost / daily_budget) * 100
|
| 172 |
+
|
| 173 |
+
# Generate alerts at specific thresholds
|
| 174 |
+
thresholds = [50, 75, 90, 95, 100]
|
| 175 |
+
|
| 176 |
+
for threshold in thresholds:
|
| 177 |
+
if usage_percentage >= threshold:
|
| 178 |
+
alert_key = f"daily_budget_{threshold}"
|
| 179 |
+
if alert_key not in self.cost_alerts:
|
| 180 |
+
self.cost_alerts.append(alert_key)
|
| 181 |
+
|
| 182 |
+
if threshold < 100:
|
| 183 |
+
cost_logger.warning(
|
| 184 |
+
f"⚠️ BUDGET ALERT: ${daily_cost:.4f} / ${daily_budget} "
|
| 185 |
+
f"({usage_percentage:.1f}%) - {threshold}% threshold reached"
|
| 186 |
+
)
|
| 187 |
+
else:
|
| 188 |
+
cost_logger.critical(
|
| 189 |
+
f"🚨 BUDGET EXCEEDED: ${daily_cost:.4f} / ${daily_budget} "
|
| 190 |
+
f"({usage_percentage:.1f}%) - Switching to free models!"
|
| 191 |
+
)
|
| 192 |
+
break
|
| 193 |
+
|
| 194 |
+
async def get_daily_cost(self) -> float:
|
| 195 |
+
"""Get current daily cost"""
|
| 196 |
+
today = datetime.now().strftime('%Y-%m-%d')
|
| 197 |
+
|
| 198 |
+
async with aiosqlite.connect(self.cost_db_path) as db:
|
| 199 |
+
cursor = await db.execute("""
|
| 200 |
+
SELECT SUM(cost_usd) FROM cost_metrics
|
| 201 |
+
WHERE date(timestamp) = ? AND request_success = 1
|
| 202 |
+
""", (today,))
|
| 203 |
+
result = await cursor.fetchone()
|
| 204 |
+
return result[0] or 0.0
|
| 205 |
+
|
| 206 |
+
async def get_cost_analytics(self, hours: int = 24) -> CostAnalytics:
|
| 207 |
+
"""Generate comprehensive cost analytics"""
|
| 208 |
+
|
| 209 |
+
cutoff_time = (datetime.now() - timedelta(hours=hours)).isoformat()
|
| 210 |
+
|
| 211 |
+
async with aiosqlite.connect(self.cost_db_path) as db:
|
| 212 |
+
# Basic metrics
|
| 213 |
+
cursor = await db.execute("""
|
| 214 |
+
SELECT
|
| 215 |
+
COUNT(*) as total_requests,
|
| 216 |
+
SUM(CASE WHEN request_success = 1 THEN 1 ELSE 0 END) as successful_requests,
|
| 217 |
+
SUM(CASE WHEN request_success = 0 THEN 1 ELSE 0 END) as failed_requests,
|
| 218 |
+
SUM(cost_usd) as total_cost,
|
| 219 |
+
SUM(total_tokens) as total_tokens,
|
| 220 |
+
AVG(response_time_seconds) as avg_response_time
|
| 221 |
+
FROM cost_metrics
|
| 222 |
+
WHERE timestamp >= ?
|
| 223 |
+
""", (cutoff_time,))
|
| 224 |
+
|
| 225 |
+
basic_stats = await cursor.fetchone()
|
| 226 |
+
|
| 227 |
+
if not basic_stats or basic_stats[0] == 0:
|
| 228 |
+
return self._empty_analytics(hours)
|
| 229 |
+
|
| 230 |
+
total_requests, successful_requests, failed_requests, total_cost, total_tokens, avg_response_time = basic_stats
|
| 231 |
+
|
| 232 |
+
# Cost by agent
|
| 233 |
+
cursor = await db.execute("""
|
| 234 |
+
SELECT agent_id, SUM(cost_usd) as cost
|
| 235 |
+
FROM cost_metrics
|
| 236 |
+
WHERE timestamp >= ? AND request_success = 1
|
| 237 |
+
GROUP BY agent_id
|
| 238 |
+
ORDER BY cost DESC
|
| 239 |
+
""", (cutoff_time,))
|
| 240 |
+
|
| 241 |
+
cost_by_agent = {row[0]: row[1] for row in await cursor.fetchall()}
|
| 242 |
+
|
| 243 |
+
# Cost by provider
|
| 244 |
+
cursor = await db.execute("""
|
| 245 |
+
SELECT provider, SUM(cost_usd) as cost
|
| 246 |
+
FROM cost_metrics
|
| 247 |
+
WHERE timestamp >= ? AND request_success = 1
|
| 248 |
+
GROUP BY provider
|
| 249 |
+
ORDER BY cost DESC
|
| 250 |
+
""", (cutoff_time,))
|
| 251 |
+
|
| 252 |
+
cost_by_provider = {row[0]: row[1] for row in await cursor.fetchall()}
|
| 253 |
+
|
| 254 |
+
# Top expensive models
|
| 255 |
+
cursor = await db.execute("""
|
| 256 |
+
SELECT model, SUM(cost_usd) as total_cost, COUNT(*) as requests,
|
| 257 |
+
AVG(cost_per_1k_tokens) as avg_cost_per_1k
|
| 258 |
+
FROM cost_metrics
|
| 259 |
+
WHERE timestamp >= ? AND request_success = 1
|
| 260 |
+
GROUP BY model
|
| 261 |
+
ORDER BY total_cost DESC
|
| 262 |
+
LIMIT 5
|
| 263 |
+
""", (cutoff_time,))
|
| 264 |
+
|
| 265 |
+
top_expensive_models = [
|
| 266 |
+
{
|
| 267 |
+
'model': row[0],
|
| 268 |
+
'total_cost': row[1],
|
| 269 |
+
'requests': row[2],
|
| 270 |
+
'avg_cost_per_1k_tokens': row[3]
|
| 271 |
+
}
|
| 272 |
+
for row in await cursor.fetchall()
|
| 273 |
+
]
|
| 274 |
+
|
| 275 |
+
# Hourly cost trend (last 24 hours)
|
| 276 |
+
cursor = await db.execute("""
|
| 277 |
+
SELECT
|
| 278 |
+
strftime('%Y-%m-%d %H:00', timestamp) as hour,
|
| 279 |
+
SUM(cost_usd) as cost,
|
| 280 |
+
COUNT(*) as requests
|
| 281 |
+
FROM cost_metrics
|
| 282 |
+
WHERE timestamp >= datetime('now', '-24 hours') AND request_success = 1
|
| 283 |
+
GROUP BY strftime('%Y-%m-%d %H:00', timestamp)
|
| 284 |
+
ORDER BY hour
|
| 285 |
+
""", ())
|
| 286 |
+
|
| 287 |
+
cost_trend_24h = [
|
| 288 |
+
{'hour': row[0], 'cost': row[1], 'requests': row[2]}
|
| 289 |
+
for row in await cursor.fetchall()
|
| 290 |
+
]
|
| 291 |
+
|
| 292 |
+
# Calculate derived metrics
|
| 293 |
+
average_cost_per_request = total_cost / total_requests if total_requests > 0 else 0
|
| 294 |
+
cost_per_1k_tokens = (total_cost / (total_tokens / 1000)) if total_tokens > 0 else 0
|
| 295 |
+
tokens_per_second = total_tokens / (avg_response_time * total_requests) if avg_response_time and total_requests > 0 else 0
|
| 296 |
+
efficiency_score = total_tokens / total_cost if total_cost > 0 else 0
|
| 297 |
+
|
| 298 |
+
# Daily budget utilization
|
| 299 |
+
daily_cost = await self.get_daily_cost()
|
| 300 |
+
daily_budget_utilization = (daily_cost / self.settings.agents.daily_cost_budget) * 100
|
| 301 |
+
|
| 302 |
+
return CostAnalytics(
|
| 303 |
+
time_period=f"{hours}h",
|
| 304 |
+
total_cost_usd=total_cost or 0,
|
| 305 |
+
total_requests=total_requests or 0,
|
| 306 |
+
successful_requests=successful_requests or 0,
|
| 307 |
+
failed_requests=failed_requests or 0,
|
| 308 |
+
average_cost_per_request=average_cost_per_request,
|
| 309 |
+
total_tokens=total_tokens or 0,
|
| 310 |
+
average_response_time=avg_response_time or 0,
|
| 311 |
+
cost_per_1k_tokens=cost_per_1k_tokens,
|
| 312 |
+
tokens_per_second=tokens_per_second,
|
| 313 |
+
top_expensive_models=top_expensive_models,
|
| 314 |
+
cost_by_agent=cost_by_agent,
|
| 315 |
+
cost_by_provider=cost_by_provider,
|
| 316 |
+
daily_budget_utilization=daily_budget_utilization,
|
| 317 |
+
cost_trend_24h=cost_trend_24h,
|
| 318 |
+
efficiency_score=efficiency_score
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
def _empty_analytics(self, hours: int) -> CostAnalytics:
|
| 322 |
+
"""Return empty analytics object"""
|
| 323 |
+
return CostAnalytics(
|
| 324 |
+
time_period=f"{hours}h",
|
| 325 |
+
total_cost_usd=0.0,
|
| 326 |
+
total_requests=0,
|
| 327 |
+
successful_requests=0,
|
| 328 |
+
failed_requests=0,
|
| 329 |
+
average_cost_per_request=0.0,
|
| 330 |
+
total_tokens=0,
|
| 331 |
+
average_response_time=0.0,
|
| 332 |
+
cost_per_1k_tokens=0.0,
|
| 333 |
+
tokens_per_second=0.0,
|
| 334 |
+
top_expensive_models=[],
|
| 335 |
+
cost_by_agent={},
|
| 336 |
+
cost_by_provider={},
|
| 337 |
+
daily_budget_utilization=0.0,
|
| 338 |
+
cost_trend_24h=[],
|
| 339 |
+
efficiency_score=0.0
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
async def get_performance_benchmarks(self, hours: int = 24) -> List[PerformanceBenchmark]:
|
| 343 |
+
"""Get performance benchmarks by provider and model"""
|
| 344 |
+
|
| 345 |
+
cutoff_time = (datetime.now() - timedelta(hours=hours)).isoformat()
|
| 346 |
+
|
| 347 |
+
async with aiosqlite.connect(self.cost_db_path) as db:
|
| 348 |
+
cursor = await db.execute("""
|
| 349 |
+
SELECT
|
| 350 |
+
provider,
|
| 351 |
+
model,
|
| 352 |
+
AVG(response_time_seconds) as avg_response_time,
|
| 353 |
+
AVG(tokens_per_second) as avg_tokens_per_second,
|
| 354 |
+
AVG(cost_per_1k_tokens) as avg_cost_per_1k,
|
| 355 |
+
SUM(CASE WHEN request_success = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as success_rate,
|
| 356 |
+
COUNT(*) as sample_size,
|
| 357 |
+
SUM(total_tokens) as total_tokens,
|
| 358 |
+
SUM(cost_usd) as total_cost
|
| 359 |
+
FROM cost_metrics
|
| 360 |
+
WHERE timestamp >= ?
|
| 361 |
+
GROUP BY provider, model
|
| 362 |
+
HAVING COUNT(*) >= 3
|
| 363 |
+
ORDER BY avg_tokens_per_second DESC
|
| 364 |
+
""", (cutoff_time,))
|
| 365 |
+
|
| 366 |
+
benchmarks = []
|
| 367 |
+
|
| 368 |
+
for row in await cursor.fetchall():
|
| 369 |
+
provider, model, avg_response_time, avg_tokens_per_second, avg_cost_per_1k, success_rate, sample_size, total_tokens, total_cost = row
|
| 370 |
+
|
| 371 |
+
# Calculate cost efficiency score (tokens per dollar)
|
| 372 |
+
cost_efficiency_score = total_tokens / total_cost if total_cost > 0 else 0
|
| 373 |
+
cost_per_token = total_cost / total_tokens if total_tokens > 0 else 0
|
| 374 |
+
|
| 375 |
+
benchmarks.append(PerformanceBenchmark(
|
| 376 |
+
provider=provider,
|
| 377 |
+
model=model,
|
| 378 |
+
avg_response_time=avg_response_time,
|
| 379 |
+
tokens_per_second=avg_tokens_per_second or 0,
|
| 380 |
+
cost_per_token=cost_per_token,
|
| 381 |
+
success_rate=success_rate / 100, # Convert to decimal
|
| 382 |
+
cost_efficiency_score=cost_efficiency_score,
|
| 383 |
+
sample_size=sample_size
|
| 384 |
+
))
|
| 385 |
+
|
| 386 |
+
return benchmarks
|
| 387 |
+
|
| 388 |
+
async def generate_cost_report(self, hours: int = 24) -> str:
|
| 389 |
+
"""Generate detailed cost report"""
|
| 390 |
+
analytics = await self.get_cost_analytics(hours)
|
| 391 |
+
benchmarks = await self.get_performance_benchmarks(hours)
|
| 392 |
+
|
| 393 |
+
report_lines = [
|
| 394 |
+
"=" * 60,
|
| 395 |
+
f"📊 SAAP Cost Efficiency Report - Last {hours} Hours",
|
| 396 |
+
"=" * 60,
|
| 397 |
+
"",
|
| 398 |
+
"💰 COST SUMMARY:",
|
| 399 |
+
f" Total Cost: ${analytics.total_cost_usd:.6f}",
|
| 400 |
+
f" Requests: {analytics.total_requests} ({analytics.successful_requests} successful)",
|
| 401 |
+
f" Average Cost/Request: ${analytics.average_cost_per_request:.6f}",
|
| 402 |
+
f" Daily Budget Used: {analytics.daily_budget_utilization:.1f}%",
|
| 403 |
+
"",
|
| 404 |
+
"🔢 TOKEN METRICS:",
|
| 405 |
+
f" Total Tokens: {analytics.total_tokens:,}",
|
| 406 |
+
f" Cost per 1K Tokens: ${analytics.cost_per_1k_tokens:.4f}",
|
| 407 |
+
f" Tokens per Second: {analytics.tokens_per_second:.1f}",
|
| 408 |
+
f" Efficiency Score: {analytics.efficiency_score:.1f} tokens/$",
|
| 409 |
+
""
|
| 410 |
+
]
|
| 411 |
+
|
| 412 |
+
if analytics.cost_by_provider:
|
| 413 |
+
report_lines.extend([
|
| 414 |
+
"🏢 COST BY PROVIDER:",
|
| 415 |
+
*[f" {provider}: ${cost:.6f}" for provider, cost in analytics.cost_by_provider.items()],
|
| 416 |
+
""
|
| 417 |
+
])
|
| 418 |
+
|
| 419 |
+
if analytics.cost_by_agent:
|
| 420 |
+
report_lines.extend([
|
| 421 |
+
"🤖 COST BY AGENT:",
|
| 422 |
+
*[f" {agent}: ${cost:.6f}" for agent, cost in list(analytics.cost_by_agent.items())[:5]],
|
| 423 |
+
""
|
| 424 |
+
])
|
| 425 |
+
|
| 426 |
+
if analytics.top_expensive_models:
|
| 427 |
+
report_lines.extend([
|
| 428 |
+
"💸 TOP EXPENSIVE MODELS:",
|
| 429 |
+
*[f" {model['model']}: ${model['total_cost']:.6f} ({model['requests']} requests)"
|
| 430 |
+
for model in analytics.top_expensive_models[:3]],
|
| 431 |
+
""
|
| 432 |
+
])
|
| 433 |
+
|
| 434 |
+
if benchmarks:
|
| 435 |
+
report_lines.extend([
|
| 436 |
+
"⚡ PERFORMANCE BENCHMARKS:",
|
| 437 |
+
f"{'Provider':<15} {'Model':<25} {'Speed (t/s)':<12} {'Cost/Token':<12} {'Success':<8}",
|
| 438 |
+
"-" * 80
|
| 439 |
+
])
|
| 440 |
+
|
| 441 |
+
for bench in benchmarks[:5]:
|
| 442 |
+
report_lines.append(
|
| 443 |
+
f"{bench.provider:<15} {bench.model[:24]:<25} {bench.tokens_per_second:<12.1f} "
|
| 444 |
+
f"${bench.cost_per_token:<11.8f} {bench.success_rate:<8.1%}"
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
report_lines.append("")
|
| 448 |
+
|
| 449 |
+
report_lines.extend([
|
| 450 |
+
"=" * 60,
|
| 451 |
+
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 452 |
+
"=" * 60
|
| 453 |
+
])
|
| 454 |
+
|
| 455 |
+
return "\n".join(report_lines)
|
| 456 |
+
|
| 457 |
+
async def cleanup_old_data(self, days_to_keep: int = 30):
|
| 458 |
+
"""Cleanup old cost tracking data"""
|
| 459 |
+
cutoff_date = (datetime.now() - timedelta(days=days_to_keep)).isoformat()
|
| 460 |
+
|
| 461 |
+
async with aiosqlite.connect(self.cost_db_path) as db:
|
| 462 |
+
cursor = await db.execute("""
|
| 463 |
+
DELETE FROM cost_metrics WHERE timestamp < ?
|
| 464 |
+
""", (cutoff_date,))
|
| 465 |
+
|
| 466 |
+
deleted_rows = cursor.rowcount
|
| 467 |
+
await db.commit()
|
| 468 |
+
|
| 469 |
+
if deleted_rows > 0:
|
| 470 |
+
cost_logger.info(f"🧹 Cleaned up {deleted_rows} old cost records (>{days_to_keep} days)")
|
| 471 |
+
|
| 472 |
+
def reset_daily_alerts(self):
|
| 473 |
+
"""Reset daily cost alerts (called at midnight)"""
|
| 474 |
+
self.cost_alerts.clear()
|
| 475 |
+
cost_logger.info("🔔 Daily cost alerts reset")
|
| 476 |
+
|
| 477 |
+
# Global cost logger instance
|
| 478 |
+
cost_efficiency_logger = CostEfficiencyLogger()
|