Hwandji commited on
Commit
4343907
·
0 Parent(s):

feat: initial HuggingFace Space deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +65 -0
  2. .gitattributes +35 -0
  3. .github/workflows/ci-cd-pipeline.yml +470 -0
  4. .github/workflows/deploy-huggingface.yml +151 -0
  5. .gitignore +220 -0
  6. .gitleaks.toml +26 -0
  7. .pre-commit-config.yaml +38 -0
  8. DEPLOYMENT.md +389 -0
  9. Dockerfile +47 -0
  10. LICENSE +21 -0
  11. PROJEKT_VOLLSTAENDIGE_ANALYSE_2025-12-04.md +578 -0
  12. QUICKSTART.md +362 -0
  13. README.md +63 -0
  14. SECURITY_REMEDIATION_REQUIRED.md +198 -0
  15. SECURITY_SCAN_REPORT.md +294 -0
  16. SECURITY_SETUP_COMPLETE.md +240 -0
  17. backend/.dockerignore +75 -0
  18. backend/.env.example +230 -0
  19. backend/Dockerfile +74 -0
  20. backend/__init__.py +1 -0
  21. backend/agent.py +343 -0
  22. backend/agent_init_fix.py +45 -0
  23. backend/agent_manager.py +989 -0
  24. backend/agent_manager_database_enhanced.py +328 -0
  25. backend/agent_manager_enhanced.py +651 -0
  26. backend/agent_manager_fixed.py +978 -0
  27. backend/agent_manager_hybrid.py +575 -0
  28. backend/agent_manager_hybrid_fixed.py +494 -0
  29. backend/agent_schema.json +267 -0
  30. backend/agent_schema.py +784 -0
  31. backend/agent_templates.json +144 -0
  32. backend/agents/__init__.py +1 -0
  33. backend/agents/colossus_agent.py +377 -0
  34. backend/agents/colossus_saap_agent.py +384 -0
  35. backend/agents/openrouter_agent_enhanced.py +367 -0
  36. backend/agents/openrouter_saap_agent.py +301 -0
  37. backend/api/__init__.py +1 -0
  38. backend/api/agent_api.py +359 -0
  39. backend/api/agent_manager.py +419 -0
  40. backend/api/agents.py +820 -0
  41. backend/api/colossus_client.py +216 -0
  42. backend/api/cost_tracking.py +303 -0
  43. backend/api/hybrid_endpoints.py +389 -0
  44. backend/api/multi_agent_endpoints.py +408 -0
  45. backend/api/openrouter_client.py +397 -0
  46. backend/api/openrouter_endpoints.py +230 -0
  47. backend/config/__init__.py +1 -0
  48. backend/config/settings.py +482 -0
  49. backend/connection.py +415 -0
  50. 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
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()