Hwandji commited on
Commit
fd044a4
·
1 Parent(s): 63bfc55

Sync with GitHub: docs reorganization

Browse files
Files changed (46) hide show
  1. backend/agent.py +343 -0
  2. backend/agent_init_fix.py +45 -0
  3. backend/agent_manager.py +989 -0
  4. backend/agent_manager_database_enhanced.py +328 -0
  5. backend/agent_manager_enhanced.py +651 -0
  6. backend/agent_manager_fixed.py +978 -0
  7. backend/agent_manager_hybrid.py +575 -0
  8. backend/agent_manager_hybrid_fixed.py +494 -0
  9. backend/agent_schema.json +267 -0
  10. backend/agent_schema.py +784 -0
  11. backend/agent_templates.json +144 -0
  12. backend/api/cost_tracking.py +241 -375
  13. backend/api/multi_agent_endpoints.py +12 -242
  14. backend/config/settings.py +5 -4
  15. backend/connection.py +415 -0
  16. backend/cost_efficiency_logger.py +478 -0
  17. backend/database/connection.py +1 -6
  18. backend/database/models.py +0 -92
  19. backend/database_service.py +382 -0
  20. backend/main.py +15 -204
  21. backend/multi_agent_coordinator.py +606 -0
  22. backend/openrouter_integration.py +252 -0
  23. backend/privacy_detector.py +288 -0
  24. backend/requirements.txt +2 -9
  25. backend/service.py +320 -0
  26. backend/services/privacy_detector.py +0 -94
  27. backend/settings.py +482 -0
  28. backend/test_colossus_integration.py +259 -0
  29. backend/websocket_manager.py +361 -0
  30. frontend/dist/assets/icons-l0sNRNKZ.js +0 -2
  31. frontend/dist/assets/icons-l0sNRNKZ.js.map +0 -1
  32. frontend/dist/assets/index-C4sh0Su3.css +0 -0
  33. frontend/dist/assets/index-RrLk96-w.js +0 -0
  34. frontend/dist/assets/index-RrLk96-w.js.map +0 -0
  35. frontend/dist/assets/vendor-Ctwcxhgt.js +0 -26
  36. frontend/dist/assets/vendor-Ctwcxhgt.js.map +0 -0
  37. frontend/dist/index.html +0 -16
  38. frontend/nginx.conf +1 -1
  39. frontend/src/App.vue +9 -116
  40. frontend/src/components/modals/MultiAgentChatModal.vue +17 -445
  41. frontend/src/composables/useApi.ts +3 -17
  42. frontend/src/composables/useWebSocket.ts +4 -20
  43. frontend/src/main.js +3 -25
  44. frontend/src/router/index.js +0 -20
  45. frontend/src/services/saapApi.js +0 -79
  46. frontend/vite.config.js +2 -16
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/api/cost_tracking.py CHANGED
@@ -1,437 +1,303 @@
1
  """
2
- SAAP Cost Tracking API - PostgreSQL Backend
3
- Tracks and analyzes LLM costs for thesis evaluation (H3)
4
  """
5
- from fastapi import APIRouter, Depends, HTTPException, Query
6
- from sqlalchemy.ext.asyncio import AsyncSession
7
- from sqlalchemy import select, func, desc
8
- from typing import Dict, List, Optional
9
- from datetime import datetime, timedelta
10
- import logging
11
 
12
- from database.connection import db_manager
13
- from database.models import DBLLMCost, DBAgent
 
 
 
 
 
14
 
15
- logger = logging.getLogger(__name__)
16
  router = APIRouter(prefix="/api/v1/cost", tags=["Cost Tracking"])
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- async def get_db() -> AsyncSession:
20
- """Get database session"""
21
- async with db_manager.get_async_session() as session:
22
- yield session
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- @router.get("/analytics")
26
- async def get_cost_analytics(
27
- hours: int = Query(default=24, ge=1, le=8760),
28
- db: AsyncSession = Depends(get_db)
29
- ):
 
 
 
 
 
 
 
 
 
 
 
 
30
  """
31
- Get comprehensive cost analytics for thesis evaluation (H3)
32
 
33
- Returns:
34
- - Total costs, tokens, requests
35
- - Breakdown by agent, provider, model
36
- - Efficiency metrics
 
37
  """
38
  try:
39
- cutoff = datetime.utcnow() - timedelta(hours=hours)
40
-
41
- # Query all costs in time window
42
- result = await db.execute(
43
- select(DBLLMCost).where(DBLLMCost.created_at >= cutoff)
44
- )
45
- costs = result.scalars().all()
46
-
47
- if not costs:
48
- return {
49
- "period_hours": hours,
50
- "total_cost_usd": 0.0,
51
- "total_requests": 0,
52
- "total_input_tokens": 0,
53
- "total_output_tokens": 0,
54
- "by_agent": {},
55
- "by_provider": {},
56
- "by_model": {},
57
- "efficiency": {},
58
- "timestamp": datetime.utcnow().isoformat()
59
- }
60
-
61
- # Calculate aggregates
62
- total_cost = sum(c.cost_usd for c in costs)
63
- total_input = sum(c.input_tokens for c in costs)
64
- total_output = sum(c.output_tokens for c in costs)
65
- total_requests = len(costs)
66
- successful_requests = len([c for c in costs if c.success])
67
 
68
- # By agent
69
- by_agent = {}
70
- for cost in costs:
71
- agent_id = cost.agent_id
72
- if agent_id not in by_agent:
73
- by_agent[agent_id] = {"cost_usd": 0, "requests": 0, "tokens": 0}
74
- by_agent[agent_id]["cost_usd"] += cost.cost_usd
75
- by_agent[agent_id]["requests"] += 1
76
- by_agent[agent_id]["tokens"] += cost.input_tokens + cost.output_tokens
77
 
78
- # By provider
79
  by_provider = {}
80
- for cost in costs:
81
- provider = cost.provider
82
- if provider not in by_provider:
83
- by_provider[provider] = {"cost_usd": 0, "requests": 0, "tokens": 0, "avg_response_time": 0}
84
- by_provider[provider]["cost_usd"] += cost.cost_usd
85
- by_provider[provider]["requests"] += 1
86
- by_provider[provider]["tokens"] += cost.input_tokens + cost.output_tokens
87
-
88
- # Calculate average response times
89
- for provider in by_provider:
90
- provider_costs = [c for c in costs if c.provider == provider and c.response_time]
91
- if provider_costs:
92
- by_provider[provider]["avg_response_time"] = sum(c.response_time for c in provider_costs) / len(provider_costs)
93
-
94
- # By model
95
- by_model = {}
96
- for cost in costs:
97
- model = cost.model_name
98
- if model not in by_model:
99
- by_model[model] = {"cost_usd": 0, "requests": 0, "tokens": 0}
100
- by_model[model]["cost_usd"] += cost.cost_usd
101
- by_model[model]["requests"] += 1
102
- by_model[model]["tokens"] += cost.input_tokens + cost.output_tokens
103
-
104
- # Efficiency metrics
105
- total_tokens = total_input + total_output
106
- efficiency = {
107
- "cost_per_1k_tokens": (total_cost / (total_tokens / 1000)) if total_tokens > 0 else 0,
108
- "tokens_per_dollar": (total_tokens / total_cost) if total_cost > 0 else 0,
109
- "success_rate": (successful_requests / total_requests * 100) if total_requests > 0 else 0,
110
- "avg_tokens_per_request": (total_tokens / total_requests) if total_requests > 0 else 0
111
- }
112
-
113
- # Calculate additional metrics for frontend
114
- total_tokens = total_input + total_output
115
-
116
- # Calculate average response time
117
- response_times = [c.response_time for c in costs if c.response_time]
118
- avg_response_time = sum(response_times) / len(response_times) if response_times else 0
119
-
120
- # Calculate tokens per second
121
- total_time = sum(response_times) if response_times else 1
122
- tokens_per_second = total_tokens / total_time if total_time > 0 else 0
123
-
124
- # Flatten cost_by_agent to simple {agent: cost} format
125
- cost_by_agent = {agent: data["cost_usd"] for agent, data in by_agent.items()}
126
-
127
- # Flatten cost_by_provider to simple {provider: cost} format
128
- cost_by_provider = {provider: data["cost_usd"] for provider, data in by_provider.items()}
129
-
130
- # Top expensive models as array
131
- top_expensive_models = [
132
- {"model": model, "total_cost": data["cost_usd"], "requests": data["requests"]}
133
- for model, data in sorted(by_model.items(), key=lambda x: x[1]["cost_usd"], reverse=True)[:5]
134
- ]
135
 
136
- return {
137
- "period_hours": hours,
138
- "total_cost_usd": round(total_cost, 6),
139
- "total_requests": total_requests,
140
- "total_tokens": total_tokens,
141
- "total_input_tokens": total_input,
142
- "total_output_tokens": total_output,
143
- "successful_requests": successful_requests,
144
- "failed_requests": total_requests - successful_requests,
145
- "average_response_time": round(avg_response_time, 2),
146
- "tokens_per_second": round(tokens_per_second, 1),
147
- "cost_by_agent": cost_by_agent,
148
- "cost_by_provider": cost_by_provider,
149
- "top_expensive_models": top_expensive_models,
150
- "cost_per_1k_tokens": efficiency["cost_per_1k_tokens"],
151
- "efficiency_score": efficiency["tokens_per_dollar"],
152
- "by_agent_detailed": by_agent,
153
- "by_provider_detailed": by_provider,
154
- "by_model_detailed": by_model,
155
- "efficiency": efficiency,
156
- "timestamp": datetime.utcnow().isoformat()
157
- }
158
 
159
  except Exception as e:
160
- logger.error(f" Cost analytics error: {e}")
161
- raise HTTPException(status_code=500, detail=str(e))
162
-
163
 
164
- @router.get("/budget")
165
- async def get_budget_status(
166
- db: AsyncSession = Depends(get_db)
167
- ):
168
  """
169
- Get current budget status and projections
 
 
 
 
 
 
170
  """
171
  try:
172
- # Get today's costs
173
- today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
174
-
175
- result = await db.execute(
176
- select(func.sum(DBLLMCost.cost_usd)).where(DBLLMCost.created_at >= today_start)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  )
178
- today_cost = result.scalar() or 0.0
179
-
180
- # Get this week's costs
181
- week_start = today_start - timedelta(days=today_start.weekday())
182
- result = await db.execute(
183
- select(func.sum(DBLLMCost.cost_usd)).where(DBLLMCost.created_at >= week_start)
184
- )
185
- week_cost = result.scalar() or 0.0
186
-
187
- # Get this month's costs
188
- month_start = today_start.replace(day=1)
189
- result = await db.execute(
190
- select(func.sum(DBLLMCost.cost_usd)).where(DBLLMCost.created_at >= month_start)
191
- )
192
- month_cost = result.scalar() or 0.0
193
-
194
- # Budget limits (configurable via env vars)
195
- import os
196
- daily_budget = float(os.getenv("SAAP_DAILY_BUDGET", "5.00"))
197
- weekly_budget = float(os.getenv("SAAP_WEEKLY_BUDGET", "25.00"))
198
- monthly_budget = float(os.getenv("SAAP_MONTHLY_BUDGET", "100.00"))
199
-
200
- # Frontend expects these flat fields
201
- return {
202
- "current_daily_cost": round(today_cost, 4),
203
- "budget_remaining_usd": round(daily_budget - today_cost, 4),
204
- "budget_used_percentage": round((today_cost / daily_budget * 100) if daily_budget > 0 else 0, 1),
205
- "daily_budget": daily_budget,
206
- "daily": {
207
- "spent": round(today_cost, 4),
208
- "budget": daily_budget,
209
- "remaining": round(daily_budget - today_cost, 4),
210
- "percent_used": round((today_cost / daily_budget * 100) if daily_budget > 0 else 0, 1)
211
- },
212
- "weekly": {
213
- "spent": round(week_cost, 4),
214
- "budget": weekly_budget,
215
- "remaining": round(weekly_budget - week_cost, 4),
216
- "percent_used": round((week_cost / weekly_budget * 100) if weekly_budget > 0 else 0, 1)
217
- },
218
- "monthly": {
219
- "spent": round(month_cost, 4),
220
- "budget": monthly_budget,
221
- "remaining": round(monthly_budget - month_cost, 4),
222
- "percent_used": round((month_cost / monthly_budget * 100) if monthly_budget > 0 else 0, 1)
223
- },
224
- "timestamp": datetime.utcnow().isoformat()
225
- }
226
 
227
  except Exception as e:
228
- logger.error(f" Budget status error: {e}")
229
- raise HTTPException(status_code=500, detail=str(e))
230
-
231
 
232
- @router.get("/summary")
233
- async def get_cost_summary(
234
- hours: int = Query(default=24, ge=1, le=8760),
235
- db: AsyncSession = Depends(get_db)
236
- ):
237
  """
238
- Get quick cost summary for dashboard
 
 
 
 
 
239
  """
240
  try:
241
- cutoff = datetime.utcnow() - timedelta(hours=hours)
242
-
243
- # Aggregate query
244
- result = await db.execute(
245
- select(
246
- func.count(DBLLMCost.id).label("total_requests"),
247
- func.sum(DBLLMCost.cost_usd).label("total_cost"),
248
- func.sum(DBLLMCost.input_tokens).label("input_tokens"),
249
- func.sum(DBLLMCost.output_tokens).label("output_tokens"),
250
- func.avg(DBLLMCost.response_time).label("avg_response_time")
251
- ).where(DBLLMCost.created_at >= cutoff)
252
- )
253
- row = result.first()
254
-
255
- return {
256
- "period_hours": hours,
257
- "total_requests": row.total_requests or 0,
258
- "total_cost_usd": round(row.total_cost or 0, 6),
259
- "total_input_tokens": row.input_tokens or 0,
260
- "total_output_tokens": row.output_tokens or 0,
261
- "avg_response_time": round(row.avg_response_time or 0, 2),
262
- "timestamp": datetime.utcnow().isoformat()
263
- }
264
 
265
  except Exception as e:
266
- logger.error(f" Cost summary error: {e}")
267
- raise HTTPException(status_code=500, detail=str(e))
268
-
269
 
270
- @router.get("/benchmarks")
271
- async def get_performance_benchmarks(
272
- hours: int = Query(default=24, ge=1, le=8760),
273
- db: AsyncSession = Depends(get_db)
274
- ):
275
  """
276
- Get performance benchmarks for thesis evaluation (H4)
277
- Compares SAAP local (Colossus) vs Cloud (OpenRouter)
 
 
 
 
278
  """
279
  try:
280
- cutoff = datetime.utcnow() - timedelta(hours=hours)
281
-
282
- result = await db.execute(
283
- select(DBLLMCost).where(DBLLMCost.created_at >= cutoff)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  )
285
- costs = result.scalars().all()
286
 
287
- # Separate by provider
288
- colossus_costs = [c for c in costs if c.provider == "colossus"]
289
- openrouter_costs = [c for c in costs if c.provider == "openrouter"]
290
-
291
- def calc_provider_stats(provider_costs):
292
- if not provider_costs:
293
- return {"requests": 0, "avg_response_time": 0, "avg_cost": 0, "total_cost": 0}
294
- response_times = [c.response_time for c in provider_costs if c.response_time]
295
- return {
296
- "requests": len(provider_costs),
297
- "avg_response_time": round(sum(response_times) / len(response_times), 2) if response_times else 0,
298
- "avg_cost": round(sum(c.cost_usd for c in provider_costs) / len(provider_costs), 6),
299
- "total_cost": round(sum(c.cost_usd for c in provider_costs), 6),
300
- "success_rate": round(len([c for c in provider_costs if c.success]) / len(provider_costs) * 100, 1)
301
- }
302
-
303
- colossus_stats = calc_provider_stats(colossus_costs)
304
- openrouter_stats = calc_provider_stats(openrouter_costs)
305
-
306
- # Calculate savings (H3 hypothesis)
307
- cloud_equivalent_cost = openrouter_stats["avg_cost"] * (colossus_stats["requests"] + openrouter_stats["requests"])
308
- actual_cost = colossus_stats["total_cost"] + openrouter_stats["total_cost"]
309
- savings = cloud_equivalent_cost - actual_cost
310
 
311
  return {
312
- "period_hours": hours,
313
- "saap_local": {
314
- "provider": "colossus",
315
- "description": "Local LLM (Free)",
316
- **colossus_stats
317
- },
318
- "cloud": {
319
- "provider": "openrouter",
320
- "description": "Cloud API (Paid)",
321
- **openrouter_stats
322
- },
323
- "comparison": {
324
- "speed_advantage": round((openrouter_stats["avg_response_time"] - colossus_stats["avg_response_time"]) / colossus_stats["avg_response_time"] * 100, 1) if colossus_stats["avg_response_time"] > 0 else 0,
325
- "cost_savings_usd": round(savings, 4),
326
- "cost_savings_percent": round((savings / cloud_equivalent_cost * 100), 1) if cloud_equivalent_cost > 0 else 0,
327
- "hybrid_efficiency": "SAAP hybrid routing optimizes cost vs speed"
328
- },
329
- "thesis_hypothesis_h3": {
330
- "claim": "SAAP provides TCO advantage at high volume",
331
- "cloud_equivalent_cost": round(cloud_equivalent_cost, 4),
332
- "actual_cost": round(actual_cost, 4),
333
- "validated": savings > 0
334
- },
335
- "timestamp": datetime.utcnow().isoformat()
336
  }
337
 
338
  except Exception as e:
339
- logger.error(f" Benchmarks error: {e}")
340
- raise HTTPException(status_code=500, detail=str(e))
341
-
342
 
343
- @router.post("/record")
344
- async def record_cost(
345
- cost_data: Dict,
346
- db: AsyncSession = Depends(get_db)
347
- ):
348
  """
349
- Record a new LLM cost entry (called by agent_manager after each request)
 
 
350
  """
351
  try:
352
- cost_record = DBLLMCost(
353
- agent_id=cost_data.get("agent_id", "unknown"),
354
- model_name=cost_data.get("model_name", "unknown"),
355
- provider=cost_data.get("provider", "unknown"),
356
- input_tokens=cost_data.get("input_tokens", 0),
357
- output_tokens=cost_data.get("output_tokens", 0),
358
- cost_usd=cost_data.get("cost_usd", 0.0),
359
- response_time=cost_data.get("response_time"),
360
- success=cost_data.get("success", True),
361
- error_message=cost_data.get("error_message")
362
- )
363
-
364
- db.add(cost_record)
365
- await db.commit()
366
- await db.refresh(cost_record)
367
 
368
- logger.info(f"💰 Cost recorded: {cost_record.provider} - ${cost_record.cost_usd:.6f}")
 
369
 
370
  return {
371
- "success": True,
372
- "id": cost_record.id,
373
- "message": "Cost recorded successfully"
374
  }
375
 
376
  except Exception as e:
377
- logger.error(f" Record cost error: {e}")
378
- await db.rollback()
379
- raise HTTPException(status_code=500, detail=str(e))
380
 
381
-
382
- @router.get("/report")
383
- async def get_cost_report(
384
- hours: int = Query(default=24, ge=1, le=8760),
385
- format: str = Query(default="json", regex="^(json|csv)$"),
386
- db: AsyncSession = Depends(get_db)
387
- ):
388
  """
389
- Get detailed cost report for export
 
 
390
  """
391
  try:
392
- cutoff = datetime.utcnow() - timedelta(hours=hours)
393
-
394
- result = await db.execute(
395
- select(DBLLMCost)
396
- .where(DBLLMCost.created_at >= cutoff)
397
- .order_by(desc(DBLLMCost.created_at))
398
- )
399
- costs = result.scalars().all()
400
-
401
- records = []
402
- for cost in costs:
403
- records.append({
404
- "id": cost.id,
405
- "timestamp": cost.created_at.isoformat(),
406
- "agent_id": cost.agent_id,
407
- "provider": cost.provider,
408
- "model_name": cost.model_name,
409
- "input_tokens": cost.input_tokens,
410
- "output_tokens": cost.output_tokens,
411
- "cost_usd": cost.cost_usd,
412
- "response_time": cost.response_time,
413
- "success": cost.success,
414
- "error_message": cost.error_message
415
- })
416
-
417
- if format == "csv":
418
- # Return CSV string
419
- import csv
420
- import io
421
- output = io.StringIO()
422
- if records:
423
- writer = csv.DictWriter(output, fieldnames=records[0].keys())
424
- writer.writeheader()
425
- writer.writerows(records)
426
- return {"csv": output.getvalue(), "count": len(records)}
427
 
428
  return {
429
- "period_hours": hours,
430
- "count": len(records),
431
- "records": records,
432
- "timestamp": datetime.utcnow().isoformat()
433
  }
434
 
435
  except Exception as e:
436
- logger.error(f" Cost report error: {e}")
437
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
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/multi_agent_endpoints.py CHANGED
@@ -11,8 +11,6 @@ from datetime import datetime
11
  from pydantic import BaseModel
12
 
13
  from services.multi_agent_coordinator import MultiAgentCoordinator, TaskPriority, get_coordinator
14
- from database.connection import db_manager
15
- from database.models import DBLLMCost
16
 
17
  # Configure logging
18
  logging.basicConfig(level=logging.INFO)
@@ -21,51 +19,6 @@ logger = logging.getLogger(__name__)
21
  # Create router for multi-agent endpoints
22
  multi_agent_router = APIRouter(prefix="/api/v1/multi-agent", tags=["Multi-Agent Communication"])
23
 
24
-
25
- # 💰 COST TRACKING HELPER
26
- async def _save_cost_to_db(
27
- agent_id: str,
28
- provider: str,
29
- processing_time: float,
30
- success: bool = True,
31
- error_message: str = None,
32
- input_tokens: int = 100,
33
- output_tokens: int = 150,
34
- model_name: str = "gpt-4"
35
- ):
36
- """
37
- 💰 Save LLM cost record to PostgreSQL database
38
-
39
- Called after every successful multi-agent chat to track costs.
40
- Uses estimated tokens if actual values not available from LLM provider.
41
- """
42
- try:
43
- # Cost estimation (OpenRouter pricing, Colossus is free)
44
- cost_usd = 0.0
45
- if provider == "openrouter":
46
- # GPT-4 pricing estimate: $0.03/1K input, $0.06/1K output
47
- cost_usd = (input_tokens / 1000 * 0.03) + (output_tokens / 1000 * 0.06)
48
-
49
- async with db_manager.get_async_session() as session:
50
- cost_record = DBLLMCost(
51
- agent_id=agent_id,
52
- model_name=model_name,
53
- provider=provider,
54
- input_tokens=input_tokens,
55
- output_tokens=output_tokens,
56
- cost_usd=cost_usd,
57
- response_time=processing_time,
58
- success=success,
59
- error_message=error_message
60
- )
61
- session.add(cost_record)
62
- await session.commit()
63
- logger.info(f"💰 Cost saved: {agent_id} via {provider} - ${cost_usd:.6f}")
64
- return True
65
- except Exception as e:
66
- logger.error(f"❌ Failed to save cost: {e}")
67
- return False
68
-
69
  # 🔒 PRIVACY DETECTION HELPER
70
  async def _determine_provider(user_message: str, provider_preference: Optional[str] = None) -> Dict[str, Any]:
71
  """
@@ -196,59 +149,18 @@ class MultiAgentChatRequest(BaseModel):
196
  task_priority: TaskPriority = TaskPriority.NORMAL
197
  provider: Optional[str] = None # "auto", "colossus", "openrouter"
198
  privacy_mode: Optional[str] = None # Alias for provider
199
- document_text: Optional[str] = None # 📄 Extracted document content
200
- document_filename: Optional[str] = None # 📄 Original filename
201
- document_privacy_info: Optional[Dict[str, Any]] = None # 📄 Pre-analyzed privacy info
202
-
203
-
204
- # ═══════════════════════════════════════════════════════════════════════════════
205
- # 🔄 UNIFIED RESPONSE STRUCTURE (v2.0)
206
- # ═══════════════════════════════════════════════════════════════════════════════
207
- # Konsistente Response-Struktur für alle Multi-Agent API Responses
208
- # ═══════════════════════════════════════════════════════════════════════════════
209
-
210
- class ResponseInfo(BaseModel):
211
- """Informationen zur LLM-Antwort"""
212
- content: str # Die eigentliche Antwort
213
- provider: str # "colossus" oder "openrouter"
214
- response_time: float # Antwortzeit in Sekunden
215
- cost_usd: float = 0.0 # Kosten in USD (0 für Colossus)
216
- model: Optional[str] = None # Verwendetes Modell (falls verfügbar)
217
-
218
- class PrivacyProtection(BaseModel):
219
- """Privacy-Schutz-Informationen"""
220
- privacy_level: str # "private" (sensibel) oder "public" (allgemein)
221
- selected_provider: str # Gewählter Provider
222
- user_override: bool = False # True wenn User Provider erzwungen hat
223
- data_protected: bool # True wenn sensitive Daten erkannt wurden
224
- details: Optional[Dict[str, Any]] = None # Zusätzliche Details (detected_categories, reason)
225
-
226
- class CoordinationInfo(BaseModel):
227
- """Koordinations-Informationen für Multi-Agent Workflows"""
228
- coordinator: str = "jane_alesi" # Master Coordinator
229
- specialist_agent: Optional[str] = None # Delegierter Spezialist (falls vorhanden)
230
- delegation_used: bool = False # True wenn Delegation stattfand
231
- workflow_type: str = "single_agent" # "single_agent" oder "multi_agent"
232
- coordination_chain: List[str] = [] # Liste aller beteiligten Agenten
233
 
234
  class MultiAgentChatResponse(BaseModel):
235
- """
236
- 🔄 UNIFIED Multi-Agent Chat Response (v2.0)
237
-
238
- Konsistente Struktur für alle Responses:
239
- - success: Ob die Anfrage erfolgreich war
240
- - coordination: Informationen zur Agent-Koordination
241
- - response: Die eigentliche LLM-Antwort
242
- - privacy_protection: Privacy-Schutz-Informationen
243
- - timestamp: Zeitstempel der Antwort
244
- - error: Fehlermeldung (nur bei Fehlern)
245
- """
246
  success: bool
247
- coordination: CoordinationInfo
248
- response: ResponseInfo
249
- privacy_protection: PrivacyProtection
250
- timestamp: str # ISO 8601 Format
 
 
251
  task_id: Optional[str] = None
 
 
252
  error: Optional[str] = None
253
 
254
  @multi_agent_router.post("/chat", response_model=MultiAgentChatResponse)
@@ -264,59 +176,22 @@ async def multi_agent_chat(
264
  2. Delegates to appropriate specialist agent
265
  3. Orchestrates multi-agent workflow for complex tasks
266
 
267
- 📄 Document Support:
268
- - Supports uploaded documents (PDF, DOCX, TXT)
269
- - Automatic privacy detection for document content
270
- - Document context integrated with user message
271
-
272
  Examples:
273
  - "Entwickle eine Python App" → Jane delegates to John Alesi (Development)
274
  - "Medizinische Beratung für Diabetes" → Jane delegates to Lara Alesi (Medical)
275
  - "Legal Compliance Check" → Jane delegates to Justus Alesi (Legal)
276
  - "SAAP Platform Status" → Jane handles directly as Coordinator
277
- - "Analyze this medical report [PDF]" → Privacy detection → Colossus enforced
278
  """
279
  start_time = datetime.now()
280
 
281
  try:
282
  logger.info(f"🤖 Multi-Agent Chat Request: {request.user_message[:100]}...")
283
 
284
- # 📄 Handle document if present
285
- full_message = request.user_message
286
- document_context_added = False
287
-
288
- if request.document_text and request.document_text.strip():
289
- logger.info(f"📄 Document attached: {request.document_filename or 'unknown'} ({len(request.document_text)} chars)")
290
-
291
- # Add document content to message for analysis
292
- full_message = f"{request.user_message}\n\n📄 Angehängtes Dokument ({request.document_filename or 'document'}):\n{request.document_text}"
293
- document_context_added = True
294
-
295
- # If document has pre-analyzed privacy info, use it
296
- if request.document_privacy_info:
297
- logger.info(f"📄 Using pre-analyzed document privacy: {request.document_privacy_info.get('privacy_level', 'unknown')}")
298
-
299
  # 🔒 PRIVACY-FIRST: Determine provider based on sensitivity
300
- # If document has privacy info, check if it overrides message analysis
301
- if request.document_privacy_info and request.document_privacy_info.get('use_local'):
302
- # Document is sensitive, force colossus
303
- selected_provider = {
304
- "provider": "colossus",
305
- "reason": f"Document privacy protection ({request.document_privacy_info.get('reason', 'sensitive document')})",
306
- "selected_provider": "colossus",
307
- "detected_categories": request.document_privacy_info.get('detected_categories', []),
308
- "confidence": "high",
309
- "auto_detected": True,
310
- "privacy_level": request.document_privacy_info.get('privacy_level', 'high'),
311
- "document_triggered": True
312
- }
313
- logger.warning(f"🔒 DOCUMENT PRIVACY ENFORCED: {request.document_filename} → Colossus")
314
- else:
315
- # Normal privacy detection on full message (includes document if present)
316
- selected_provider = await _determine_provider(
317
- user_message=full_message,
318
- provider_preference=request.provider or request.privacy_mode
319
- )
320
 
321
  logger.info(f"🔒 Provider selection: {selected_provider['provider']} (reason: {selected_provider['reason']})")
322
 
@@ -389,16 +264,6 @@ async def multi_agent_chat(
389
 
390
  logger.info(f"✅ Single Agent Delegation: jane_alesi → {primary_agent}")
391
 
392
- # 💰 Save cost to database
393
- await _save_cost_to_db(
394
- agent_id=primary_agent,
395
- provider=selected_provider['provider'],
396
- processing_time=processing_time,
397
- success=True,
398
- input_tokens=len(request.user_message.split()) * 2, # Estimate tokens
399
- output_tokens=len(response_text.split()) * 2 if response_text else 150
400
- )
401
-
402
  return MultiAgentChatResponse(
403
  success=True,
404
  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'}.",
@@ -541,98 +406,3 @@ async def get_agent_workload(
541
  except Exception as e:
542
  logger.error(f"❌ Agent Workload Error for {agent_id}: {e}")
543
  raise HTTPException(status_code=500, detail=f"Workload check failed: {str(e)}")
544
-
545
-
546
- # ============================================================================
547
- # Chat History Persistence Endpoints
548
- # ============================================================================
549
-
550
- @multi_agent_router.get("/history")
551
- async def get_chat_history(
552
- session_id: str = "default",
553
- limit: int = 5
554
- ):
555
- """
556
- Get the last N messages from multi-agent chat history
557
- """
558
- try:
559
- from database.connection import db_manager
560
- from database.models import DBMultiAgentMessage
561
- from sqlalchemy import desc
562
-
563
- async with db_manager.get_async_session() as session:
564
- from sqlalchemy import select
565
-
566
- stmt = (
567
- select(DBMultiAgentMessage)
568
- .where(DBMultiAgentMessage.session_id == session_id)
569
- .order_by(desc(DBMultiAgentMessage.created_at))
570
- .limit(limit)
571
- )
572
- result = await session.execute(stmt)
573
- messages = result.scalars().all()
574
-
575
- # Return in chronological order (oldest first)
576
- return {
577
- "messages": [msg.to_dict() for msg in reversed(messages)],
578
- "count": len(messages),
579
- "session_id": session_id
580
- }
581
-
582
- except Exception as e:
583
- logger.error(f"❌ Chat History Error: {e}")
584
- return {"messages": [], "count": 0, "session_id": session_id, "error": str(e)}
585
-
586
-
587
- @multi_agent_router.post("/history/message")
588
- async def save_chat_message(message: dict):
589
- """
590
- Save a single message to the multi-agent chat history
591
- """
592
- try:
593
- from database.connection import db_manager
594
- from database.models import DBMultiAgentMessage
595
-
596
- async with db_manager.get_async_session() as session:
597
- db_message = DBMultiAgentMessage(
598
- session_id=message.get("session_id", "default"),
599
- content=message.get("content", ""),
600
- agent_id=message.get("agent", "unknown"),
601
- agent_name=message.get("agentName", "Unknown"),
602
- message_type=message.get("type", "agent"),
603
- role=message.get("role"),
604
- provider=message.get("provider"),
605
- processing_time=message.get("processingTime"),
606
- cost_usd=float(message["cost"].replace("$", "")) if message.get("cost") else None
607
- )
608
- session.add(db_message)
609
- await session.commit()
610
- await session.refresh(db_message)
611
-
612
- return {"success": True, "message_id": db_message.id}
613
-
614
- except Exception as e:
615
- logger.error(f"❌ Save Message Error: {e}")
616
- return {"success": False, "error": str(e)}
617
-
618
-
619
- @multi_agent_router.delete("/history")
620
- async def clear_chat_history(session_id: str = "default"):
621
- """
622
- Clear chat history for a session
623
- """
624
- try:
625
- from database.connection import db_manager
626
- from database.models import DBMultiAgentMessage
627
- from sqlalchemy import delete
628
-
629
- async with db_manager.get_async_session() as session:
630
- stmt = delete(DBMultiAgentMessage).where(DBMultiAgentMessage.session_id == session_id)
631
- await session.execute(stmt)
632
- await session.commit()
633
-
634
- return {"success": True, "session_id": session_id}
635
-
636
- except Exception as e:
637
- logger.error(f"❌ Clear History Error: {e}")
638
- return {"success": False, "error": str(e)}
 
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)
 
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
  """
 
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)
 
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
 
 
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'}.",
 
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/config/settings.py CHANGED
@@ -191,7 +191,7 @@ class SecuritySettings(BaseSettings):
191
 
192
  # CORS settings - Updated for network access + HuggingFace Spaces
193
  allowed_origins: str = Field(
194
- default="http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:8080,http://localhost:3000,http://100.64.0.45:5173,http://100.64.0.45:5174,http://100.64.0.45:5175,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
 
@@ -339,9 +339,10 @@ class SaapSettings(BaseSettings):
339
  """Get database URL with environment-specific adjustments"""
340
  if self.environment == 'testing':
341
  return "sqlite:///./test_saap.db"
342
- # 🔧 FIX: Use DATABASE_URL from .env for development too (PostgreSQL support)
343
- # Only override if DATABASE_URL is not set - otherwise use configured URL
344
- return self.database.database_url
 
345
 
346
  def is_production(self) -> bool:
347
  """Check if running in production"""
 
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
 
 
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"""
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()
backend/database/connection.py CHANGED
@@ -114,13 +114,8 @@ class DatabaseManager:
114
  database_url = settings.get_database_url()
115
 
116
  # Create sync engine for migrations and admin tasks
117
- # Fix: Use psycopg3 driver explicitly for PostgreSQL sync connections
118
- sync_url = database_url
119
- if database_url.startswith("postgresql://"):
120
- sync_url = database_url.replace("postgresql://", "postgresql+psycopg://")
121
-
122
  sync_kwargs = self._get_sync_engine_kwargs()
123
- self.engine = create_engine(sync_url, **sync_kwargs)
124
 
125
  # Setup SQL logging for sync engine
126
  self._setup_sql_logging(self.engine)
 
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)
backend/database/models.py CHANGED
@@ -311,98 +311,6 @@ class DBSystemLog(Base):
311
  Index('idx_log_logger_created', 'logger_name', 'created_at'),
312
  )
313
 
314
- class DBLLMCost(Base):
315
- """
316
- Database model for LLM cost tracking
317
- Stores all LLM API costs for evaluation and analysis
318
- """
319
- __tablename__ = "llm_costs"
320
-
321
- # Primary Key (🔧 FIXED: Match actual DB column name 'id')
322
- id = Column(Integer, primary_key=True, index=True, autoincrement=True)
323
-
324
- # Foreign Key to Agent
325
- agent_id = Column(String(100), ForeignKey("agents.id"), nullable=False, index=True)
326
-
327
- # LLM Details
328
- model_name = Column(String(200), nullable=False, index=True)
329
- provider = Column(String(100), nullable=False, index=True) # colossus, openrouter
330
-
331
- # Token Usage
332
- input_tokens = Column(Integer, nullable=False, default=0)
333
- output_tokens = Column(Integer, nullable=False, default=0)
334
-
335
- # Cost
336
- cost_usd = Column(Float, nullable=False, default=0.0)
337
-
338
- # Performance Metrics (🔧 FIXED: Match actual DB column names)
339
- response_time = Column(Float, nullable=True) # DB has 'response_time', not 'response_time_seconds'
340
- success = Column(Boolean, default=True) # DB has 'success', not 'request_success'
341
- error_message = Column(Text, nullable=True) # DB has 'error_message' instead of 'privacy_level'
342
-
343
- # Timestamp
344
- created_at = Column(TIMESTAMP(timezone=True), default=func.now(), nullable=False, index=True)
345
-
346
- # Indexes for cost analytics
347
- __table_args__ = (
348
- Index('idx_cost_agent_created', 'agent_id', 'created_at'),
349
- Index('idx_cost_provider_created', 'provider', 'created_at'),
350
- Index('idx_cost_model_created', 'model_name', 'created_at'),
351
- Index('idx_cost_date', 'created_at'),
352
- )
353
-
354
-
355
- class DBMultiAgentMessage(Base):
356
- """
357
- Database model for Multi-Agent Chat message history
358
- Stores all multi-agent conversations for persistence across sessions
359
- """
360
- __tablename__ = "multi_agent_messages"
361
-
362
- # Primary Key
363
- id = Column(Integer, primary_key=True, index=True, autoincrement=True)
364
-
365
- # Session identifier (to group messages by session)
366
- session_id = Column(String(100), nullable=False, index=True, default="default")
367
-
368
- # Message Content
369
- content = Column(Text, nullable=False)
370
- agent_id = Column(String(100), nullable=False, index=True)
371
- agent_name = Column(String(200), nullable=False)
372
- message_type = Column(String(50), nullable=False, default="agent") # user, coordinator, specialist, delegation, error
373
-
374
- # Additional metadata
375
- role = Column(String(100), nullable=True) # Master Coordinator, Specialist, etc.
376
- provider = Column(String(50), nullable=True) # colossus, openrouter
377
- processing_time = Column(Float, nullable=True)
378
- cost_usd = Column(Float, nullable=True)
379
-
380
- # Timestamp
381
- created_at = Column(TIMESTAMP(timezone=True), default=func.now(), nullable=False, index=True)
382
-
383
- # Indexes for performance
384
- __table_args__ = (
385
- Index('idx_multi_agent_session_created', 'session_id', 'created_at'),
386
- Index('idx_multi_agent_agent_created', 'agent_id', 'created_at'),
387
- )
388
-
389
- def to_dict(self):
390
- """Convert to dictionary for API response"""
391
- return {
392
- "id": self.id,
393
- "session_id": self.session_id,
394
- "content": self.content,
395
- "agent": self.agent_id,
396
- "agentName": self.agent_name,
397
- "type": self.message_type,
398
- "role": self.role,
399
- "provider": self.provider,
400
- "processingTime": self.processing_time,
401
- "cost": f"${self.cost_usd:.6f}" if self.cost_usd else None,
402
- "timestamp": self.created_at.isoformat() if self.created_at else None
403
- }
404
-
405
-
406
  class DBHealthCheck(Base):
407
  """
408
  Database model for system health monitoring
 
311
  Index('idx_log_logger_created', 'logger_name', 'created_at'),
312
  )
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  class DBHealthCheck(Base):
315
  """
316
  Database model for system health monitoring
backend/database_service.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SAAP Database Service - Production Ready
3
+ High-level database operations for agent management and chat persistence
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Optional, Dict, Any
8
+ from datetime import datetime, timedelta
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+ from sqlalchemy import text, select, update, delete, and_, desc
11
+ from sqlalchemy.exc import SQLAlchemyError
12
+
13
+ from database.connection import db_manager
14
+ from database.models import DBAgent, DBChatMessage, DBAgentSession, DBSystemLog, DBHealthCheck
15
+ from models.agent import SaapAgent
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class DatabaseService:
20
+ """
21
+ Database service layer for SAAP production deployment
22
+ Provides high-level database operations with error handling and logging
23
+ """
24
+
25
+ def __init__(self):
26
+ self.is_initialized = False
27
+ self.db_manager = db_manager
28
+
29
+ async def initialize(self):
30
+ """Initialize database service"""
31
+ if not self.db_manager.is_initialized:
32
+ await self.db_manager.initialize()
33
+ self.is_initialized = True
34
+ logger.info("✅ Database Service initialized")
35
+
36
+ # =====================================================
37
+ # AGENT MANAGEMENT OPERATIONS
38
+ # =====================================================
39
+
40
+ async def save_agent(self, agent: SaapAgent) -> bool:
41
+ """Save or update agent in database"""
42
+ try:
43
+ async with self.db_manager.get_async_session() as session:
44
+ # Check if agent exists
45
+ existing_agent = await session.get(DBAgent, agent.id)
46
+
47
+ if existing_agent:
48
+ # Update existing agent
49
+ existing_agent.name = agent.name
50
+ existing_agent.agent_type = agent.type.value
51
+ existing_agent.color = agent.color
52
+ existing_agent.avatar = agent.avatar
53
+ existing_agent.description = agent.description
54
+ existing_agent.llm_config = agent.llm_config.model_dump()
55
+ existing_agent.capabilities = agent.capabilities
56
+ existing_agent.personality = agent.personality.model_dump() if agent.personality else None
57
+ existing_agent.status = agent.status.value
58
+ existing_agent.last_active = agent.metrics.last_active if agent.metrics else None
59
+ existing_agent.metrics = agent.metrics.model_dump() if agent.metrics else None
60
+ existing_agent.updated_at = datetime.utcnow()
61
+ existing_agent.tags = agent.tags
62
+
63
+ logger.info(f"📝 Updated agent in database: {agent.name}")
64
+ else:
65
+ # Create new agent
66
+ db_agent = DBAgent.from_saap_agent(agent)
67
+ session.add(db_agent)
68
+ logger.info(f"💾 Saved new agent to database: {agent.name}")
69
+
70
+ await session.commit()
71
+ return True
72
+
73
+ except SQLAlchemyError as e:
74
+ logger.error(f"❌ Database error saving agent {agent.id}: {e}")
75
+ return False
76
+ except Exception as e:
77
+ logger.error(f"❌ Unexpected error saving agent {agent.id}: {e}")
78
+ return False
79
+
80
+ async def load_agent(self, agent_id: str) -> Optional[SaapAgent]:
81
+ """Load single agent from database"""
82
+ try:
83
+ async with self.db_manager.get_async_session() as session:
84
+ db_agent = await session.get(DBAgent, agent_id)
85
+
86
+ if db_agent:
87
+ return db_agent.to_saap_agent()
88
+ else:
89
+ logger.warning(f"⚠️ Agent not found in database: {agent_id}")
90
+ return None
91
+
92
+ except Exception as e:
93
+ logger.error(f"❌ Error loading agent {agent_id}: {e}")
94
+ return None
95
+
96
+ async def load_all_agents(self) -> List[SaapAgent]:
97
+ """Load all agents from database"""
98
+ try:
99
+ async with self.db_manager.get_async_session() as session:
100
+ result = await session.execute(
101
+ select(DBAgent).order_by(DBAgent.created_at)
102
+ )
103
+ db_agents = result.scalars().all()
104
+
105
+ saap_agents = []
106
+ for db_agent in db_agents:
107
+ try:
108
+ saap_agent = db_agent.to_saap_agent()
109
+ saap_agents.append(saap_agent)
110
+ except Exception as e:
111
+ logger.error(f"❌ Error converting DB agent {db_agent.id}: {e}")
112
+
113
+ logger.info(f"📋 Loaded {len(saap_agents)} agents from database")
114
+ return saap_agents
115
+
116
+ except Exception as e:
117
+ logger.error(f"❌ Error loading agents: {e}")
118
+ return []
119
+
120
+ async def delete_agent(self, agent_id: str) -> bool:
121
+ """Delete agent and all related data"""
122
+ try:
123
+ async with self.db_manager.get_async_session() as session:
124
+ # Delete agent (cascading will delete related chat_messages and sessions)
125
+ result = await session.execute(
126
+ delete(DBAgent).where(DBAgent.id == agent_id)
127
+ )
128
+
129
+ if result.rowcount > 0:
130
+ await session.commit()
131
+ logger.info(f"🗑️ Deleted agent from database: {agent_id}")
132
+ return True
133
+ else:
134
+ logger.warning(f"⚠️ Agent not found for deletion: {agent_id}")
135
+ return False
136
+
137
+ except Exception as e:
138
+ logger.error(f"❌ Error deleting agent {agent_id}: {e}")
139
+ return False
140
+
141
+ async def get_agent_statistics(self) -> Dict[str, Any]:
142
+ """Get comprehensive agent statistics"""
143
+ try:
144
+ async with self.db_manager.get_async_session() as session:
145
+ # Total agents by status
146
+ status_result = await session.execute(text(
147
+ "SELECT status, COUNT(*) as count FROM agents GROUP BY status"
148
+ ))
149
+ status_breakdown = {row.status: row.count for row in status_result}
150
+
151
+ # Total agents
152
+ total_result = await session.execute(text("SELECT COUNT(*) FROM agents"))
153
+ total_agents = total_result.scalar()
154
+
155
+ # Active agents
156
+ active_result = await session.execute(text(
157
+ "SELECT COUNT(*) FROM agents WHERE status = 'active'"
158
+ ))
159
+ active_agents = active_result.scalar()
160
+
161
+ # Agent types
162
+ type_result = await session.execute(text(
163
+ "SELECT agent_type, COUNT(*) as count FROM agents GROUP BY agent_type"
164
+ ))
165
+ type_breakdown = {row.agent_type: row.count for row in type_result}
166
+
167
+ # Recent activity
168
+ recent_result = await session.execute(text(
169
+ "SELECT COUNT(*) FROM agents WHERE last_active > :cutoff"
170
+ ), {"cutoff": datetime.utcnow() - timedelta(hours=24)})
171
+ recent_active = recent_result.scalar()
172
+
173
+ return {
174
+ "total_agents": total_agents,
175
+ "active_agents": active_agents,
176
+ "recent_active_24h": recent_active,
177
+ "status_breakdown": status_breakdown,
178
+ "type_breakdown": type_breakdown,
179
+ "timestamp": datetime.utcnow().isoformat()
180
+ }
181
+
182
+ except Exception as e:
183
+ logger.error(f"❌ Error getting agent statistics: {e}")
184
+ return {"error": str(e)}
185
+
186
+ # =====================================================
187
+ # CHAT MESSAGE OPERATIONS
188
+ # =====================================================
189
+
190
+ async def save_chat_message(self, agent_id: str, user_message: str,
191
+ agent_response: str, response_time: float,
192
+ metadata: Optional[Dict] = None) -> bool:
193
+ """Save chat message to database"""
194
+ try:
195
+ async with self.db_manager.get_async_session() as session:
196
+ chat_message = DBChatMessage(
197
+ agent_id=agent_id,
198
+ user_message=user_message,
199
+ agent_response=agent_response,
200
+ response_time=response_time,
201
+ metadata=metadata or {}
202
+ )
203
+
204
+ session.add(chat_message)
205
+ await session.commit()
206
+
207
+ logger.debug(f"💬 Saved chat message for agent {agent_id}")
208
+ return True
209
+
210
+ except Exception as e:
211
+ logger.error(f"❌ Error saving chat message: {e}")
212
+ return False
213
+
214
+ async def get_chat_history(self, agent_id: str, limit: int = 100) -> List[Dict]:
215
+ """Get chat history for an agent"""
216
+ try:
217
+ async with self.db_manager.get_async_session() as session:
218
+ result = await session.execute(
219
+ select(DBChatMessage)
220
+ .where(DBChatMessage.agent_id == agent_id)
221
+ .order_by(desc(DBChatMessage.created_at))
222
+ .limit(limit)
223
+ )
224
+
225
+ messages = result.scalars().all()
226
+
227
+ return [{
228
+ "id": msg.id,
229
+ "user_message": msg.user_message,
230
+ "agent_response": msg.agent_response,
231
+ "response_time": msg.response_time,
232
+ "created_at": msg.created_at.isoformat(),
233
+ "metadata": msg.metadata
234
+ } for msg in messages]
235
+
236
+ except Exception as e:
237
+ logger.error(f"❌ Error getting chat history: {e}")
238
+ return []
239
+
240
+ async def get_chat_statistics(self, agent_id: Optional[str] = None) -> Dict[str, Any]:
241
+ """Get chat statistics"""
242
+ try:
243
+ async with self.db_manager.get_async_session() as session:
244
+ base_query = select(DBChatMessage)
245
+ if agent_id:
246
+ base_query = base_query.where(DBChatMessage.agent_id == agent_id)
247
+
248
+ # Total messages
249
+ count_result = await session.execute(
250
+ select([text("COUNT(*)")]).select_from(base_query.alias())
251
+ )
252
+ total_messages = count_result.scalar()
253
+
254
+ # Average response time
255
+ avg_result = await session.execute(
256
+ select([text("AVG(response_time)")]).select_from(base_query.alias())
257
+ )
258
+ avg_response_time = avg_result.scalar() or 0.0
259
+
260
+ # Recent activity (24h)
261
+ recent_query = base_query.where(
262
+ DBChatMessage.created_at > datetime.utcnow() - timedelta(hours=24)
263
+ )
264
+ recent_result = await session.execute(
265
+ select([text("COUNT(*)")]).select_from(recent_query.alias())
266
+ )
267
+ recent_messages = recent_result.scalar()
268
+
269
+ return {
270
+ "total_messages": total_messages,
271
+ "average_response_time": float(avg_response_time),
272
+ "recent_messages_24h": recent_messages,
273
+ "agent_id": agent_id,
274
+ "timestamp": datetime.utcnow().isoformat()
275
+ }
276
+
277
+ except Exception as e:
278
+ logger.error(f"❌ Error getting chat statistics: {e}")
279
+ return {"error": str(e)}
280
+
281
+ # =====================================================
282
+ # AGENT SESSION MANAGEMENT
283
+ # =====================================================
284
+
285
+ async def start_agent_session(self, agent_id: str, metadata: Optional[Dict] = None) -> int:
286
+ """Start new agent session"""
287
+ try:
288
+ async with self.db_manager.get_async_session() as session:
289
+ agent_session = DBAgentSession(
290
+ agent_id=agent_id,
291
+ status="active",
292
+ metadata=metadata or {}
293
+ )
294
+
295
+ session.add(agent_session)
296
+ await session.commit()
297
+ await session.refresh(agent_session)
298
+
299
+ logger.info(f"🚀 Started session {agent_session.id} for agent {agent_id}")
300
+ return agent_session.id
301
+
302
+ except Exception as e:
303
+ logger.error(f"❌ Error starting agent session: {e}")
304
+ return 0
305
+
306
+ async def end_agent_session(self, session_id: int, end_reason: str = "graceful") -> bool:
307
+ """End agent session"""
308
+ try:
309
+ async with self.db_manager.get_async_session() as session:
310
+ agent_session = await session.get(DBAgentSession, session_id)
311
+
312
+ if agent_session:
313
+ agent_session.session_end = datetime.utcnow()
314
+ agent_session.status = "completed"
315
+ agent_session.end_reason = end_reason
316
+ agent_session.calculate_duration()
317
+
318
+ await session.commit()
319
+ logger.info(f"🔚 Ended session {session_id} ({end_reason})")
320
+ return True
321
+ else:
322
+ logger.warning(f"⚠️ Agent session not found: {session_id}")
323
+ return False
324
+
325
+ except Exception as e:
326
+ logger.error(f"❌ Error ending agent session: {e}")
327
+ return False
328
+
329
+ # =====================================================
330
+ # SYSTEM MONITORING
331
+ # =====================================================
332
+
333
+ async def log_system_event(self, level: str, logger_name: str, message: str,
334
+ agent_id: Optional[str] = None, extra_data: Optional[Dict] = None):
335
+ """Log system event to database"""
336
+ try:
337
+ async with self.db_manager.get_async_session() as session:
338
+ system_log = DBSystemLog(
339
+ level=level,
340
+ logger_name=logger_name,
341
+ message=message,
342
+ agent_id=agent_id,
343
+ extra_data=extra_data or {}
344
+ )
345
+
346
+ session.add(system_log)
347
+ await session.commit()
348
+
349
+ except Exception as e:
350
+ logger.error(f"❌ Error logging system event: {e}")
351
+
352
+ async def health_check(self) -> Dict[str, Any]:
353
+ """Database health check"""
354
+ return await self.db_manager.health_check()
355
+
356
+ async def cleanup_old_data(self, days: int = 30):
357
+ """Clean up old data"""
358
+ await self.db_manager.cleanup_old_data(days)
359
+
360
+ # Global database service instance
361
+ db_service = DatabaseService()
362
+
363
+ if __name__ == "__main__":
364
+ import asyncio
365
+
366
+ async def test_db_service():
367
+ """Test database service operations"""
368
+ await db_service.initialize()
369
+
370
+ # Test health check
371
+ health = await db_service.health_check()
372
+ print(f"🔍 Database Health: {health}")
373
+
374
+ # Test agent statistics
375
+ stats = await db_service.get_agent_statistics()
376
+ print(f"📊 Agent Stats: {stats}")
377
+
378
+ # Test chat statistics
379
+ chat_stats = await db_service.get_chat_statistics()
380
+ print(f"💬 Chat Stats: {chat_stats}")
381
+
382
+ asyncio.run(test_db_service())
backend/main.py CHANGED
@@ -21,6 +21,7 @@ from dotenv import load_dotenv
21
  from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Depends
22
  from fastapi.middleware.cors import CORSMiddleware
23
  from fastapi.responses import JSONResponse
 
24
  from contextlib import asynccontextmanager
25
  from typing import List, Dict, Optional
26
  import asyncio
@@ -42,54 +43,6 @@ load_dotenv()
42
  logging.basicConfig(level=logging.INFO)
43
  logger = logging.getLogger(__name__)
44
 
45
-
46
- # 💰 COST TRACKING HELPER - Save to PostgreSQL
47
- async def _save_llm_cost(
48
- agent_id: str,
49
- provider: str,
50
- processing_time: float,
51
- success: bool = True,
52
- error_message: str = None,
53
- input_tokens: int = 100,
54
- output_tokens: int = 150,
55
- model_name: str = "gpt-4o-mini",
56
- cost_usd: float = None
57
- ):
58
- """
59
- 💰 Save LLM cost record to PostgreSQL llm_costs table
60
- """
61
- try:
62
- from database.connection import db_manager
63
- from database.models import DBLLMCost
64
-
65
- # Calculate cost if not provided
66
- if cost_usd is None:
67
- if provider == "openrouter":
68
- cost_usd = (input_tokens / 1000 * 0.00015) + (output_tokens / 1000 * 0.0006)
69
- else:
70
- cost_usd = 0.0
71
-
72
- async with db_manager.get_async_session() as session:
73
- cost_record = DBLLMCost(
74
- agent_id=agent_id,
75
- model_name=model_name,
76
- provider=provider,
77
- input_tokens=input_tokens,
78
- output_tokens=output_tokens,
79
- cost_usd=cost_usd,
80
- response_time=processing_time,
81
- success=success,
82
- error_message=error_message
83
- )
84
- session.add(cost_record)
85
- await session.commit()
86
- logger.info(f"💰 Cost saved to DB: {agent_id} via {provider} - ${cost_usd:.6f}")
87
- return True
88
- except Exception as e:
89
- logger.error(f"❌ Failed to save cost to DB: {e}")
90
- return False
91
-
92
-
93
  # 🚀 FIXED: Try Hybrid imports with proper error handling
94
  HYBRID_MODE = False
95
  OPENROUTER_ENDPOINTS = False
@@ -260,36 +213,6 @@ if OPENROUTER_ENDPOINTS and hybrid_router and HYBRID_MODE:
260
  else:
261
  logger.info("⚠️ OpenRouter endpoints not available")
262
 
263
- # 📄 Include Document Upload endpoints
264
- try:
265
- from api.document_upload import router as document_router
266
- app.include_router(document_router)
267
- logger.info("✅ Document Upload endpoints registered")
268
- except ImportError as e:
269
- logger.warning(f"⚠️ Document Upload endpoints import failed: {e}")
270
- except Exception as e:
271
- logger.warning(f"⚠️ Document Upload endpoints registration failed: {e}")
272
-
273
- # 💰 Include Cost Tracking endpoints (PostgreSQL)
274
- try:
275
- from api.cost_tracking import router as cost_router
276
- app.include_router(cost_router)
277
- logger.info("✅ Cost Tracking endpoints registered (PostgreSQL)")
278
- except ImportError as e:
279
- logger.warning(f"⚠️ Cost Tracking endpoints import failed: {e}")
280
- except Exception as e:
281
- logger.warning(f"⚠️ Cost Tracking endpoints registration failed: {e}")
282
-
283
- # 📊 Include System Metrics endpoints (Real-time Monitoring)
284
- try:
285
- from api.system_metrics import router as metrics_router
286
- app.include_router(metrics_router)
287
- logger.info("✅ System Metrics endpoints registered (Real-time Monitoring)")
288
- except ImportError as e:
289
- logger.warning(f"⚠️ System Metrics endpoints import failed: {e}")
290
- except Exception as e:
291
- logger.warning(f"⚠️ System Metrics endpoints registration failed: {e}")
292
-
293
  # Dependency injection
294
  def get_agent_manager() -> AgentManagerService:
295
  """Get agent manager instance"""
@@ -301,6 +224,20 @@ def get_websocket_manager() -> WebSocketManager:
301
  """Get WebSocket manager instance"""
302
  return saap_app.websocket_manager
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  # =====================================================
305
  # ENHANCED ROOT ENDPOINT WITH HYBRID INFO (API)
306
  # =====================================================
@@ -1104,17 +1041,6 @@ Respond with ONLY the agent name:
1104
  "timestamp": datetime.utcnow().isoformat()
1105
  }
1106
 
1107
- # 💰 Save cost for Jane direct response
1108
- await _save_llm_cost(
1109
- agent_id="jane_alesi",
1110
- provider=jane_direct_response.get("provider", "openrouter"),
1111
- processing_time=jane_direct_response.get("response_time", 0),
1112
- success=True,
1113
- cost_usd=jane_direct_response.get("cost_usd", 0),
1114
- input_tokens=len(user_message.split()) * 2,
1115
- output_tokens=len(jane_direct_response.get("content", "").split()) * 2
1116
- )
1117
-
1118
  return {
1119
  "success": True,
1120
  "coordinator": "jane_alesi",
@@ -1234,17 +1160,6 @@ Provide a coordinated final response that ensures quality and completeness."""
1234
 
1235
  logger.info(f"✅ Multi-Agent Chat successful: {'Delegated to ' + final_response.get('specialist_agent', '') if final_response.get('delegation_used') else 'Direct response from Jane'}")
1236
 
1237
- # 💰 Save cost to database
1238
- await _save_llm_cost(
1239
- agent_id=final_response.get("specialist_agent", "jane_alesi"),
1240
- provider=final_response["response"].get("provider", "openrouter"),
1241
- processing_time=final_response["response"].get("response_time", 0),
1242
- success=True,
1243
- cost_usd=final_response["response"].get("cost_usd", 0),
1244
- input_tokens=len(user_message.split()) * 2,
1245
- output_tokens=len(final_response["response"].get("content", "").split()) * 2
1246
- )
1247
-
1248
  return final_response
1249
 
1250
  except Exception as e:
@@ -1718,111 +1633,7 @@ async def startup_message():
1718
  else:
1719
  logger.warning("⚠️ OpenRouter endpoints not available")
1720
 
1721
- # ============================================================================
1722
- # Multi-Agent Chat History Persistence Endpoints
1723
- # ============================================================================
1724
-
1725
- @app.get("/api/v1/multi-agent/history")
1726
- async def get_chat_history(session_id: str = "default", limit: int = 5):
1727
- """Get the last N messages from multi-agent chat history"""
1728
- try:
1729
- from database.models import DBMultiAgentMessage
1730
- from database.connection import db_manager
1731
- from sqlalchemy import desc, select
1732
-
1733
- async with db_manager.get_async_session() as session:
1734
- stmt = (
1735
- select(DBMultiAgentMessage)
1736
- .where(DBMultiAgentMessage.session_id == session_id)
1737
- .order_by(desc(DBMultiAgentMessage.created_at))
1738
- .limit(limit)
1739
- )
1740
- result = await session.execute(stmt)
1741
- messages = result.scalars().all()
1742
-
1743
- return {
1744
- "messages": [msg.to_dict() for msg in reversed(messages)],
1745
- "count": len(messages),
1746
- "session_id": session_id
1747
- }
1748
- except Exception as e:
1749
- logger.error(f"❌ Chat History Error: {e}")
1750
- return {"messages": [], "count": 0, "session_id": session_id, "error": str(e)}
1751
-
1752
-
1753
- @app.post("/api/v1/multi-agent/history/message")
1754
- async def save_chat_message(message: dict):
1755
- """Save a single message to the multi-agent chat history"""
1756
- try:
1757
- from database.models import DBMultiAgentMessage
1758
- from database.connection import db_manager
1759
-
1760
- async with db_manager.get_async_session() as session:
1761
- cost_str = message.get("cost")
1762
- cost_float = None
1763
- if cost_str and isinstance(cost_str, str):
1764
- cost_float = float(cost_str.replace("$", ""))
1765
- elif cost_str:
1766
- cost_float = float(cost_str)
1767
-
1768
- db_message = DBMultiAgentMessage(
1769
- session_id=message.get("session_id", "default"),
1770
- content=message.get("content", ""),
1771
- agent_id=message.get("agent", "unknown"),
1772
- agent_name=message.get("agentName", "Unknown"),
1773
- message_type=message.get("type", "agent"),
1774
- role=message.get("role"),
1775
- provider=message.get("provider"),
1776
- processing_time=message.get("processingTime"),
1777
- cost_usd=cost_float
1778
- )
1779
- session.add(db_message)
1780
- await session.commit()
1781
- await session.refresh(db_message)
1782
-
1783
- return {"success": True, "message_id": db_message.id}
1784
- except Exception as e:
1785
- logger.error(f"❌ Save Message Error: {e}")
1786
- return {"success": False, "error": str(e)}
1787
-
1788
-
1789
- @app.delete("/api/v1/multi-agent/history")
1790
- async def clear_chat_history(session_id: str = "default"):
1791
- """Clear chat history for a session"""
1792
- try:
1793
- from database.models import DBMultiAgentMessage
1794
- from database.connection import db_manager
1795
- from sqlalchemy import delete
1796
-
1797
- async with db_manager.get_async_session() as session:
1798
- stmt = delete(DBMultiAgentMessage).where(DBMultiAgentMessage.session_id == session_id)
1799
- await session.execute(stmt)
1800
- await session.commit()
1801
-
1802
- return {"success": True, "session_id": session_id}
1803
- except Exception as e:
1804
- logger.error(f"❌ Clear History Error: {e}")
1805
- return {"success": False, "error": str(e)}
1806
-
1807
-
1808
  # ==========================================
1809
  # NOTE: No if __name__ == "__main__" block needed
1810
  # Supervisor/Uvicorn will start the app directly
1811
  # ==========================================
1812
-
1813
- # =====================================================
1814
- # STATIC FILES - Serve Vue.js Frontend (SPA Mode)
1815
- # ⚠️ CRITICAL: Must be AFTER all API routes!
1816
- # =====================================================
1817
-
1818
- # Import custom SPA Static Files handler
1819
- from spa_static_files import SPAStaticFiles
1820
-
1821
- # Mount static files for frontend (must be AFTER all API routes)
1822
- # SPAStaticFiles preserves API routes while serving Vue.js SPA
1823
- frontend_dist = "/app/frontend/dist"
1824
- if os.path.exists(frontend_dist):
1825
- app.mount("/", SPAStaticFiles(directory=frontend_dist, html=True), name="static")
1826
- logger.info(f"✅ SPA Static files mounted: {frontend_dist}")
1827
- else:
1828
- logger.warning(f"⚠️ Frontend dist directory not found: {frontend_dist}")
 
21
  from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Depends
22
  from fastapi.middleware.cors import CORSMiddleware
23
  from fastapi.responses import JSONResponse
24
+ from fastapi.staticfiles import StaticFiles
25
  from contextlib import asynccontextmanager
26
  from typing import List, Dict, Optional
27
  import asyncio
 
43
  logging.basicConfig(level=logging.INFO)
44
  logger = logging.getLogger(__name__)
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  # 🚀 FIXED: Try Hybrid imports with proper error handling
47
  HYBRID_MODE = False
48
  OPENROUTER_ENDPOINTS = False
 
213
  else:
214
  logger.info("⚠️ OpenRouter endpoints not available")
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  # Dependency injection
217
  def get_agent_manager() -> AgentManagerService:
218
  """Get agent manager instance"""
 
224
  """Get WebSocket manager instance"""
225
  return saap_app.websocket_manager
226
 
227
+ # =====================================================
228
+ # STATIC FILES - Serve Vue.js Frontend
229
+ # =====================================================
230
+
231
+ # Mount static files for frontend (must be AFTER all API routes)
232
+ # This serves the built Vue.js app
233
+ import os
234
+ frontend_dist = "/app/frontend/dist"
235
+ if os.path.exists(frontend_dist):
236
+ app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="static")
237
+ logger.info(f"✅ Static files mounted: {frontend_dist}")
238
+ else:
239
+ logger.warning(f"⚠️ Frontend dist directory not found: {frontend_dist}")
240
+
241
  # =====================================================
242
  # ENHANCED ROOT ENDPOINT WITH HYBRID INFO (API)
243
  # =====================================================
 
1041
  "timestamp": datetime.utcnow().isoformat()
1042
  }
1043
 
 
 
 
 
 
 
 
 
 
 
 
1044
  return {
1045
  "success": True,
1046
  "coordinator": "jane_alesi",
 
1160
 
1161
  logger.info(f"✅ Multi-Agent Chat successful: {'Delegated to ' + final_response.get('specialist_agent', '') if final_response.get('delegation_used') else 'Direct response from Jane'}")
1162
 
 
 
 
 
 
 
 
 
 
 
 
1163
  return final_response
1164
 
1165
  except Exception as e:
 
1633
  else:
1634
  logger.warning("⚠️ OpenRouter endpoints not available")
1635
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1636
  # ==========================================
1637
  # NOTE: No if __name__ == "__main__" block needed
1638
  # Supervisor/Uvicorn will start the app directly
1639
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/multi_agent_coordinator.py ADDED
@@ -0,0 +1,606 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Multi-Agent Coordinator Service for SAAP Platform
4
+ Enables autonomous agent-to-agent communication and task delegation
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ import time
11
+ import uuid
12
+ from datetime import datetime
13
+ from typing import Dict, List, Optional, Any, Tuple
14
+ from enum import Enum
15
+ from dataclasses import dataclass
16
+
17
+ import redis.asyncio as aioredis
18
+ from pydantic import BaseModel
19
+
20
+ # Configure logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ class TaskStatus(str, Enum):
25
+ CREATED = "created"
26
+ ASSIGNED = "assigned"
27
+ IN_PROGRESS = "in_progress"
28
+ COMPLETED = "completed"
29
+ FAILED = "failed"
30
+
31
+ class TaskPriority(str, Enum):
32
+ LOW = "low"
33
+ NORMAL = "normal"
34
+ HIGH = "high"
35
+ URGENT = "urgent"
36
+
37
+ @dataclass
38
+ class AgentCapability:
39
+ """Agent capability definition for intelligent task matching"""
40
+ name: str
41
+ description: str
42
+ keywords: List[str]
43
+ complexity_level: int # 1-10 scale
44
+
45
+ class TaskRequest(BaseModel):
46
+ task_id: str
47
+ task_type: str
48
+ description: str
49
+ input_data: Dict[str, Any]
50
+ priority: TaskPriority = TaskPriority.NORMAL
51
+ status: TaskStatus = TaskStatus.CREATED
52
+ assigned_agent: Optional[str] = None
53
+ parent_task_id: Optional[str] = None
54
+ max_execution_time: int = 300 # seconds
55
+
56
+ class TaskResult(BaseModel):
57
+ task_id: str
58
+ agent_id: str
59
+ status: TaskStatus
60
+ result: Dict[str, Any]
61
+ execution_time: float
62
+ timestamp: datetime
63
+ error_message: Optional[str] = None
64
+
65
+ class MultiAgentCoordinator:
66
+ """
67
+ Multi-Agent Coordinator for autonomous task delegation and workflow orchestration
68
+ Jane Alesi acts as the master coordinator with intelligent agent selection
69
+ """
70
+
71
+ def __init__(self, redis_host: str = "localhost", redis_port: int = 6379):
72
+ self.redis_host = redis_host
73
+ self.redis_port = redis_port
74
+ self.redis_client = None
75
+
76
+ # Agent capabilities database
77
+ self.agent_capabilities = {
78
+ "jane_alesi": [
79
+ AgentCapability("coordination", "Master coordination and workflow management",
80
+ ["coordinate", "manage", "orchestrate", "plan"], 9),
81
+ AgentCapability("architecture", "System architecture and design decisions",
82
+ ["architecture", "design", "system", "structure"], 10),
83
+ AgentCapability("integration", "Multi-agent integration and communication",
84
+ ["integrate", "communication", "multi-agent"], 10)
85
+ ],
86
+ "john_alesi": [
87
+ AgentCapability("development", "Software development and coding",
88
+ ["code", "develop", "program", "software", "implementation"], 9),
89
+ AgentCapability("debugging", "Code debugging and troubleshooting",
90
+ ["debug", "fix", "error", "troubleshoot"], 8),
91
+ AgentCapability("optimization", "Performance optimization and refactoring",
92
+ ["optimize", "performance", "refactor"], 7),
93
+ AgentCapability("testing", "Unit testing and code quality assurance",
94
+ ["test", "quality", "validation"], 8)
95
+ ],
96
+ "lara_alesi": [
97
+ AgentCapability("medical_analysis", "Medical data analysis and diagnosis",
98
+ ["medical", "health", "diagnosis", "clinical"], 10),
99
+ AgentCapability("data_analysis", "Statistical data analysis and interpretation",
100
+ ["analysis", "statistics", "data", "interpret"], 9),
101
+ AgentCapability("research", "Medical research and literature review",
102
+ ["research", "study", "literature", "evidence"], 8)
103
+ ],
104
+ "justus_alesi": [
105
+ AgentCapability("legal_analysis", "Legal compliance and regulatory analysis",
106
+ ["legal", "compliance", "regulation", "law"], 10),
107
+ AgentCapability("documentation", "Legal documentation and contract review",
108
+ ["document", "contract", "review", "legal"], 9),
109
+ AgentCapability("risk_assessment", "Legal risk assessment and mitigation",
110
+ ["risk", "assessment", "legal", "mitigation"], 8)
111
+ ],
112
+ "theo_alesi": [
113
+ AgentCapability("financial_analysis", "Financial analysis and budgeting",
114
+ ["finance", "budget", "cost", "investment"], 10),
115
+ AgentCapability("market_analysis", "Market analysis and business intelligence",
116
+ ["market", "business", "intelligence", "analysis"], 9),
117
+ AgentCapability("reporting", "Financial reporting and KPI tracking",
118
+ ["report", "kpi", "tracking", "metrics"], 8)
119
+ ],
120
+ "leon_alesi": [
121
+ AgentCapability("system_administration", "System administration and deployment",
122
+ ["system", "admin", "deploy", "infrastructure"], 10),
123
+ AgentCapability("monitoring", "System monitoring and performance tracking",
124
+ ["monitor", "performance", "system", "tracking"], 9),
125
+ AgentCapability("security", "System security and access control",
126
+ ["security", "access", "control", "protect"], 9)
127
+ ],
128
+ "luna_alesi": [
129
+ AgentCapability("coaching", "Team coaching and development",
130
+ ["coach", "team", "development", "training"], 10),
131
+ AgentCapability("process_improvement", "Process optimization and workflow improvement",
132
+ ["process", "improvement", "workflow", "optimize"], 8),
133
+ AgentCapability("communication", "Team communication and collaboration",
134
+ ["communication", "collaboration", "team"], 9)
135
+ ]
136
+ }
137
+
138
+ # Active tasks tracking
139
+ self.active_tasks: Dict[str, TaskRequest] = {}
140
+ self.completed_tasks: Dict[str, TaskResult] = {}
141
+
142
+ # Agent manager reference (will be injected)
143
+ self.agent_manager = None
144
+
145
+ async def initialize(self):
146
+ """Initialize Redis connection and coordinator"""
147
+ try:
148
+ self.redis_client = aioredis.from_url(f"redis://{self.redis_host}:{self.redis_port}")
149
+ await self.redis_client.ping()
150
+ logger.info(f"✅ Multi-Agent Coordinator initialized with Redis at {self.redis_host}:{self.redis_port}")
151
+ return True
152
+ except Exception as e:
153
+ logger.error(f"❌ Failed to initialize Multi-Agent Coordinator: {e}")
154
+ return False
155
+
156
+ def set_agent_manager(self, agent_manager):
157
+ """Inject agent manager dependency"""
158
+ self.agent_manager = agent_manager
159
+
160
+ async def analyze_intent(self, user_message: str) -> Tuple[str, List[str], str]:
161
+ """
162
+ Analyze user intent and determine if multi-agent coordination is needed
163
+ Returns: (task_type, required_capabilities, primary_agent)
164
+ """
165
+ message_lower = user_message.lower()
166
+
167
+ # Intent analysis patterns - 🎯 ENHANCED FOR FINANCIAL
168
+ intent_patterns = {
169
+ "development": ["entwicke", "code", "programmier", "implementier", "software", "app"],
170
+ "medical": ["medizinisch", "gesundheit", "diagnose", "patient", "clinical"],
171
+ "legal": ["rechtlich", "legal", "compliance", "vertrag", "regulation"],
172
+ "financial": ["finanzanwendung", "finanz", "finanziell", "budget", "kosten", "investment", "market", "banking", "payment"], # 🔧 FIXED
173
+ "system": ["system", "deploy", "server", "infrastructure", "admin"],
174
+ "coordination": ["koordinier", "manage", "plan", "orchestrat", "workflow"],
175
+ "analysis": ["analysier", "untersuche", "bewerte", "statistik"]
176
+ }
177
+
178
+ # Multi-agent patterns (require coordination)
179
+ multi_agent_patterns = [
180
+ "full stack", "complete solution", "end-to-end", "comprehensive",
181
+ "multi", "various", "different aspects", "holistic approach"
182
+ ]
183
+
184
+ detected_intents = []
185
+ for intent, keywords in intent_patterns.items():
186
+ if any(keyword in message_lower for keyword in keywords):
187
+ detected_intents.append(intent)
188
+
189
+ # Check if multi-agent coordination is needed
190
+ needs_coordination = any(pattern in message_lower for pattern in multi_agent_patterns) or len(detected_intents) > 1
191
+
192
+ if needs_coordination or "coordination" in detected_intents:
193
+ return "multi_agent_task", detected_intents, "jane_alesi"
194
+ elif "development" in detected_intents:
195
+ return "development_task", ["development"], "john_alesi"
196
+ elif "medical" in detected_intents:
197
+ return "medical_task", ["medical_analysis"], "lara_alesi"
198
+ elif "legal" in detected_intents:
199
+ return "legal_task", ["legal_analysis"], "justus_alesi"
200
+ elif "financial" in detected_intents:
201
+ return "financial_task", ["financial_analysis"], "theo_alesi" # 🔧 THEO HANDLES FINANCE
202
+ elif "system" in detected_intents:
203
+ return "system_task", ["system_administration"], "leon_alesi"
204
+ else:
205
+ return "general_task", ["coordination"], "jane_alesi"
206
+
207
+ def select_agents_for_capabilities(self, required_capabilities: List[str], exclude_agent: str = None) -> List[str]:
208
+ """
209
+ Select best agents for required capabilities
210
+ """
211
+ agent_scores = {}
212
+
213
+ for agent_id, capabilities in self.agent_capabilities.items():
214
+ if exclude_agent and agent_id == exclude_agent:
215
+ continue
216
+
217
+ total_score = 0
218
+ matched_capabilities = 0
219
+
220
+ for required_cap in required_capabilities:
221
+ best_match_score = 0
222
+ for capability in capabilities:
223
+ # Check keyword matches
224
+ keyword_matches = sum(1 for keyword in capability.keywords
225
+ if keyword in required_cap.lower())
226
+ if keyword_matches > 0:
227
+ match_score = keyword_matches * capability.complexity_level
228
+ best_match_score = max(best_match_score, match_score)
229
+
230
+ if best_match_score > 0:
231
+ total_score += best_match_score
232
+ matched_capabilities += 1
233
+
234
+ if matched_capabilities > 0:
235
+ # Average score weighted by coverage
236
+ agent_scores[agent_id] = (total_score / len(required_capabilities)) * (matched_capabilities / len(required_capabilities))
237
+
238
+ # Return top agents sorted by score
239
+ sorted_agents = sorted(agent_scores.items(), key=lambda x: x[1], reverse=True)
240
+ return [agent_id for agent_id, score in sorted_agents[:3]] # Top 3 agents
241
+
242
+ async def create_task(self, task_type: str, description: str, input_data: Dict[str, Any],
243
+ priority: TaskPriority = TaskPriority.NORMAL,
244
+ parent_task_id: str = None) -> str:
245
+ """Create a new task in the coordination system"""
246
+ task_id = str(uuid.uuid4())
247
+
248
+ task = TaskRequest(
249
+ task_id=task_id,
250
+ task_type=task_type,
251
+ description=description,
252
+ input_data=input_data,
253
+ priority=priority,
254
+ status=TaskStatus.CREATED,
255
+ parent_task_id=parent_task_id
256
+ )
257
+
258
+ self.active_tasks[task_id] = task
259
+
260
+ # Store in Redis for persistence
261
+ if self.redis_client:
262
+ try:
263
+ await self.redis_client.hset("saap:tasks", task_id, task.json())
264
+ except Exception as e:
265
+ logger.warning(f"⚠️ Failed to store task in Redis: {e}")
266
+
267
+ logger.info(f"📋 Created task {task_id}: {task_type} - {description}")
268
+ return task_id
269
+
270
+ async def delegate_task(self, task_id: str, agent_id: str) -> bool:
271
+ """Delegate a task to a specific agent"""
272
+ if task_id not in self.active_tasks:
273
+ logger.error(f"❌ Task {task_id} not found")
274
+ return False
275
+
276
+ task = self.active_tasks[task_id]
277
+ task.assigned_agent = agent_id
278
+ task.status = TaskStatus.ASSIGNED
279
+
280
+ # Update Redis
281
+ if self.redis_client:
282
+ try:
283
+ await self.redis_client.hset("saap:tasks", task_id, task.json())
284
+ except Exception as e:
285
+ logger.warning(f"⚠️ Failed to update task in Redis: {e}")
286
+
287
+ logger.info(f"👤 Delegated task {task_id} to {agent_id}")
288
+ return True
289
+
290
+ async def execute_task(self, task_id: str) -> TaskResult:
291
+ """Execute a delegated task through the assigned agent"""
292
+ if task_id not in self.active_tasks:
293
+ raise ValueError(f"Task {task_id} not found")
294
+
295
+ task = self.active_tasks[task_id]
296
+ if not task.assigned_agent:
297
+ raise ValueError(f"Task {task_id} has no assigned agent")
298
+
299
+ start_time = time.time()
300
+ task.status = TaskStatus.IN_PROGRESS
301
+
302
+ try:
303
+ # Get agent and execute task
304
+ if not self.agent_manager:
305
+ raise ValueError("Agent manager not available")
306
+
307
+ # 🔧 FIX: Don't await synchronous methods
308
+ agent = self.agent_manager.get_agent(task.assigned_agent)
309
+ if not agent:
310
+ raise ValueError(f"Agent {task.assigned_agent} not found")
311
+
312
+ # Create task-specific prompt
313
+ task_prompt = self._create_task_prompt(task)
314
+
315
+ # 🔧 FIX: Check if send_message is async or sync
316
+ if hasattr(self.agent_manager, 'send_message'):
317
+ # Try to determine if it's async
318
+ send_method = getattr(self.agent_manager, 'send_message')
319
+ if asyncio.iscoroutinefunction(send_method):
320
+ response = await self.agent_manager.send_message(task.assigned_agent, task_prompt)
321
+ else:
322
+ response = self.agent_manager.send_message(task.assigned_agent, task_prompt)
323
+ else:
324
+ # Fallback: Use a generic message
325
+ response = f"Als Master Coordinator habe ich deine Anfrage analysiert und das beste Ergebnis koordiniert."
326
+
327
+ execution_time = time.time() - start_time
328
+
329
+ result = TaskResult(
330
+ task_id=task_id,
331
+ agent_id=task.assigned_agent,
332
+ status=TaskStatus.COMPLETED,
333
+ result={"response": response, "execution_time": execution_time},
334
+ execution_time=execution_time,
335
+ timestamp=datetime.now()
336
+ )
337
+
338
+ # Move to completed tasks
339
+ self.completed_tasks[task_id] = result
340
+ del self.active_tasks[task_id]
341
+
342
+ # Update Redis
343
+ if self.redis_client:
344
+ try:
345
+ await self.redis_client.hset("saap:completed_tasks", task_id, result.json())
346
+ await self.redis_client.hdel("saap:tasks", task_id)
347
+ except Exception as e:
348
+ logger.warning(f"⚠️ Failed to update Redis: {e}")
349
+
350
+ logger.info(f"✅ Completed task {task_id} in {execution_time:.2f}s")
351
+ return result
352
+
353
+ except Exception as e:
354
+ execution_time = time.time() - start_time
355
+ error_result = TaskResult(
356
+ task_id=task_id,
357
+ agent_id=task.assigned_agent,
358
+ status=TaskStatus.FAILED,
359
+ result={"error": str(e)},
360
+ execution_time=execution_time,
361
+ timestamp=datetime.now(),
362
+ error_message=str(e)
363
+ )
364
+
365
+ self.completed_tasks[task_id] = error_result
366
+ del self.active_tasks[task_id]
367
+
368
+ logger.error(f"❌ Task {task_id} failed: {e}")
369
+ return error_result
370
+
371
+ def _create_task_prompt(self, task: TaskRequest) -> str:
372
+ """Create agent-specific prompt for task execution"""
373
+ prompt = f"""
374
+ Task ID: {task.task_id}
375
+ Task Type: {task.task_type}
376
+ Priority: {task.priority}
377
+
378
+ Description: {task.description}
379
+
380
+ Input Data: {json.dumps(task.input_data, indent=2)}
381
+
382
+ Please process this task according to your role and capabilities.
383
+ Provide a detailed response with actionable results.
384
+ """
385
+ return prompt
386
+
387
+ async def coordinate_multi_agent_task(self, user_message: str, user_context: Dict[str, Any] = None) -> Dict[str, Any]:
388
+ """
389
+ Main coordination method for multi-agent tasks
390
+ This is the entry point for complex multi-agent workflows
391
+ """
392
+ start_time = time.time()
393
+
394
+ try:
395
+ # Step 1: Analyze intent
396
+ task_type, required_capabilities, primary_agent = await self.analyze_intent(user_message)
397
+
398
+ logger.info(f"🎯 Intent Analysis: {task_type} → {primary_agent} (capabilities: {required_capabilities})")
399
+
400
+ # Step 2: Determine if multi-agent coordination is needed
401
+ if task_type == "multi_agent_task" or len(required_capabilities) > 1:
402
+ return await self._handle_multi_agent_workflow(user_message, required_capabilities, user_context)
403
+ else:
404
+ return await self._handle_single_agent_task(user_message, primary_agent, user_context)
405
+
406
+ except Exception as e:
407
+ logger.error(f"❌ Coordination error: {e}")
408
+ return {
409
+ "success": False,
410
+ "error": str(e),
411
+ "execution_time": time.time() - start_time
412
+ }
413
+
414
+ async def _handle_single_agent_task(self, message: str, agent_id: str, context: Dict[str, Any]) -> Dict[str, Any]:
415
+ """Handle simple single-agent task"""
416
+ start_time = time.time()
417
+
418
+ try:
419
+ # Create and execute task
420
+ task_id = await self.create_task(
421
+ task_type="single_agent",
422
+ description=message,
423
+ input_data={"message": message, "context": context or {}}
424
+ )
425
+
426
+ await self.delegate_task(task_id, agent_id)
427
+ result = await self.execute_task(task_id)
428
+
429
+ return {
430
+ "success": result.status == TaskStatus.COMPLETED,
431
+ "task_id": task_id,
432
+ "coordinator": agent_id,
433
+ "coordinator_response": result.result.get("response", "Als Master Coordinator habe ich deine Anfrage analysiert und das beste Ergebnis koordiniert."),
434
+ "specialist_response": result.result.get("response", ""),
435
+ "execution_time": time.time() - start_time,
436
+ "workflow_type": "single_agent",
437
+ "coordination_chain": [agent_id],
438
+ "processing_time": result.execution_time,
439
+ "timestamp": result.timestamp.isoformat()
440
+ }
441
+ except Exception as e:
442
+ logger.error(f"❌ Single agent task failed: {e}")
443
+ return {
444
+ "success": False,
445
+ "coordinator": agent_id,
446
+ "coordinator_response": "Als Master Coordinator habe ich deine Anfrage analysiert und das beste Ergebnis koordiniert.",
447
+ "specialist_response": "",
448
+ "error": str(e),
449
+ "execution_time": time.time() - start_time,
450
+ "workflow_type": "single_agent",
451
+ "coordination_chain": [agent_id],
452
+ "processing_time": 0,
453
+ "timestamp": datetime.now().isoformat()
454
+ }
455
+
456
+ async def _handle_multi_agent_workflow(self, message: str, capabilities: List[str], context: Dict[str, Any]) -> Dict[str, Any]:
457
+ """Handle complex multi-agent workflow with Jane as coordinator"""
458
+ start_time = time.time()
459
+ workflow_steps = []
460
+
461
+ try:
462
+ # Step 1: Jane analyzes and creates coordination plan
463
+ coordination_task_id = await self.create_task(
464
+ task_type="coordination_analysis",
465
+ description=f"Analyze the following request and create a multi-agent coordination plan: {message}",
466
+ input_data={
467
+ "original_message": message,
468
+ "detected_capabilities": capabilities,
469
+ "context": context or {}
470
+ },
471
+ priority=TaskPriority.HIGH
472
+ )
473
+
474
+ await self.delegate_task(coordination_task_id, "jane_alesi")
475
+ coordination_result = await self.execute_task(coordination_task_id)
476
+
477
+ workflow_steps.append({
478
+ "step": "coordination_analysis",
479
+ "agent": "jane_alesi",
480
+ "result": coordination_result.result,
481
+ "execution_time": coordination_result.execution_time
482
+ })
483
+
484
+ # Step 2: Select and coordinate specialist agents
485
+ selected_agents = self.select_agents_for_capabilities(capabilities, exclude_agent="jane_alesi")
486
+ specialist_results = []
487
+
488
+ for agent_id in selected_agents[:2]: # Limit to 2 specialists for now
489
+ specialist_task_id = await self.create_task(
490
+ task_type="specialist_analysis",
491
+ description=f"Provide specialist analysis for: {message}",
492
+ input_data={
493
+ "original_message": message,
494
+ "coordination_plan": coordination_result.result,
495
+ "context": context or {}
496
+ },
497
+ parent_task_id=coordination_task_id
498
+ )
499
+
500
+ await self.delegate_task(specialist_task_id, agent_id)
501
+ specialist_result = await self.execute_task(specialist_task_id)
502
+
503
+ specialist_results.append(specialist_result)
504
+ workflow_steps.append({
505
+ "step": "specialist_analysis",
506
+ "agent": agent_id,
507
+ "result": specialist_result.result,
508
+ "execution_time": specialist_result.execution_time
509
+ })
510
+
511
+ # Step 3: Jane synthesizes all results
512
+ synthesis_task_id = await self.create_task(
513
+ task_type="result_synthesis",
514
+ description="Synthesize specialist results into comprehensive response",
515
+ input_data={
516
+ "original_message": message,
517
+ "coordination_result": coordination_result.result,
518
+ "specialist_results": [r.result for r in specialist_results],
519
+ "context": context or {}
520
+ },
521
+ priority=TaskPriority.HIGH,
522
+ parent_task_id=coordination_task_id
523
+ )
524
+
525
+ await self.delegate_task(synthesis_task_id, "jane_alesi")
526
+ synthesis_result = await self.execute_task(synthesis_task_id)
527
+
528
+ workflow_steps.append({
529
+ "step": "result_synthesis",
530
+ "agent": "jane_alesi",
531
+ "result": synthesis_result.result,
532
+ "execution_time": synthesis_result.execution_time
533
+ })
534
+
535
+ total_execution_time = time.time() - start_time
536
+
537
+ return {
538
+ "success": True,
539
+ "workflow_type": "multi_agent",
540
+ "coordinator": "jane_alesi",
541
+ "specialists": selected_agents[:2],
542
+ "workflow_steps": workflow_steps,
543
+ "coordinator_response": "Als Master Coordinator habe ich deine Anfrage analysiert und das beste Ergebnis koordiniert.",
544
+ "specialist_response": synthesis_result.result.get("response", ""),
545
+ "final_response": synthesis_result.result.get("response", ""),
546
+ "coordination_chain": ["jane_alesi"] + selected_agents[:2],
547
+ "processing_time": total_execution_time,
548
+ "timestamp": datetime.now().isoformat(),
549
+ "task_count": len(workflow_steps)
550
+ }
551
+
552
+ except Exception as e:
553
+ logger.error(f"❌ Multi-agent workflow failed: {e}")
554
+ return {
555
+ "success": False,
556
+ "workflow_type": "multi_agent",
557
+ "coordinator": "jane_alesi",
558
+ "coordinator_response": "Als Master Coordinator habe ich deine Anfrage analysiert und das beste Ergebnis koordiniert.",
559
+ "specialist_response": "",
560
+ "error": str(e),
561
+ "coordination_chain": ["jane_alesi"],
562
+ "processing_time": time.time() - start_time,
563
+ "timestamp": datetime.now().isoformat()
564
+ }
565
+
566
+ async def get_agent_workload(self, agent_id: str) -> Dict[str, Any]:
567
+ """Get current workload statistics for an agent"""
568
+ active_count = sum(1 for task in self.active_tasks.values()
569
+ if task.assigned_agent == agent_id)
570
+ completed_count = sum(1 for result in self.completed_tasks.values()
571
+ if result.agent_id == agent_id)
572
+
573
+ return {
574
+ "agent_id": agent_id,
575
+ "active_tasks": active_count,
576
+ "completed_tasks": completed_count,
577
+ "capabilities": [cap.name for cap in self.agent_capabilities.get(agent_id, [])]
578
+ }
579
+
580
+ async def get_coordination_stats(self) -> Dict[str, Any]:
581
+ """Get overall coordination statistics"""
582
+ return {
583
+ "active_tasks": len(self.active_tasks),
584
+ "completed_tasks": len(self.completed_tasks),
585
+ "available_agents": len(self.agent_capabilities),
586
+ "agent_workloads": {
587
+ agent_id: await self.get_agent_workload(agent_id)
588
+ for agent_id in self.agent_capabilities.keys()
589
+ }
590
+ }
591
+
592
+ async def cleanup(self):
593
+ """Cleanup Redis connections"""
594
+ if self.redis_client:
595
+ await self.redis_client.close()
596
+
597
+ # Global coordinator instance
598
+ coordinator_instance = None
599
+
600
+ async def get_coordinator() -> MultiAgentCoordinator:
601
+ """Get global coordinator instance"""
602
+ global coordinator_instance
603
+ if coordinator_instance is None:
604
+ coordinator_instance = MultiAgentCoordinator()
605
+ await coordinator_instance.initialize()
606
+ return coordinator_instance
backend/openrouter_integration.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OpenRouter Integration for existing SAAP AgentManagerService
3
+ Simple integration that extends existing functionality without breaking anything
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ import time
9
+ from datetime import datetime
10
+ from typing import Dict, List, Optional, Any
11
+ import aiohttp
12
+ import json
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class OpenRouterIntegration:
17
+ """
18
+ Simple OpenRouter integration for existing SAAP system
19
+ Designed to work alongside existing colossus integration
20
+ """
21
+
22
+ def __init__(self, api_key: str = "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"):
23
+ self.api_key = api_key
24
+ self.base_url = "https://openrouter.ai/api/v1"
25
+ self.session: Optional[aiohttp.ClientSession] = None
26
+
27
+ # Simple agent model mapping
28
+ self.agent_models = {
29
+ "jane_alesi": {
30
+ "model": "openai/gpt-4o-mini",
31
+ "max_tokens": 800,
32
+ "temperature": 0.7,
33
+ "cost_per_1m_input": 0.15,
34
+ "cost_per_1m_output": 0.60
35
+ },
36
+ "john_alesi": {
37
+ "model": "anthropic/claude-3-5-sonnet-20241022",
38
+ "max_tokens": 1200,
39
+ "temperature": 0.5,
40
+ "cost_per_1m_input": 3.00,
41
+ "cost_per_1m_output": 15.00
42
+ },
43
+ "lara_alesi": {
44
+ "model": "openai/gpt-4o-mini",
45
+ "max_tokens": 1000,
46
+ "temperature": 0.3,
47
+ "cost_per_1m_input": 0.15,
48
+ "cost_per_1m_output": 0.60
49
+ }
50
+ }
51
+
52
+ self.daily_cost = 0.0
53
+ self.requests_count = 0
54
+
55
+ logger.info(f"🌐 OpenRouter integration initialized for {len(self.agent_models)} agents")
56
+
57
+ async def __aenter__(self):
58
+ """Initialize session"""
59
+ self.session = aiohttp.ClientSession(
60
+ headers={
61
+ "Authorization": f"Bearer {self.api_key}",
62
+ "Content-Type": "application/json",
63
+ "HTTP-Referer": "https://saap.satware.ai",
64
+ "X-Title": "SAAP Agent Platform"
65
+ },
66
+ timeout=aiohttp.ClientTimeout(total=30)
67
+ )
68
+ return self
69
+
70
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
71
+ """Close session"""
72
+ if self.session:
73
+ await self.session.close()
74
+
75
+ async def send_message(self, agent_id: str, message: str, system_prompt: str = "") -> Dict[str, Any]:
76
+ """
77
+ Send message to OpenRouter for specific agent
78
+ Returns response in same format as colossus for compatibility
79
+ """
80
+ if not self.session:
81
+ return {
82
+ "error": "OpenRouter session not initialized",
83
+ "provider": "openrouter"
84
+ }
85
+
86
+ # Get agent config or use default
87
+ config = self.agent_models.get(agent_id, {
88
+ "model": "meta-llama/llama-3.2-3b-instruct:free",
89
+ "max_tokens": 600,
90
+ "temperature": 0.7,
91
+ "cost_per_1m_input": 0.0,
92
+ "cost_per_1m_output": 0.0
93
+ })
94
+
95
+ # Prepare messages
96
+ messages = []
97
+ if system_prompt:
98
+ messages.append({"role": "system", "content": system_prompt})
99
+ messages.append({"role": "user", "content": message})
100
+
101
+ start_time = time.time()
102
+
103
+ try:
104
+ payload = {
105
+ "model": config["model"],
106
+ "messages": messages,
107
+ "max_tokens": config["max_tokens"],
108
+ "temperature": config["temperature"]
109
+ }
110
+
111
+ async with self.session.post(f"{self.base_url}/chat/completions", json=payload) as response:
112
+ response_time = time.time() - start_time
113
+
114
+ if response.status == 200:
115
+ data = await response.json()
116
+
117
+ # Extract content
118
+ content = ""
119
+ if "choices" in data and len(data["choices"]) > 0:
120
+ choice = data["choices"][0]
121
+ if "message" in choice and "content" in choice["message"]:
122
+ content = choice["message"]["content"]
123
+
124
+ # Calculate cost
125
+ usage = data.get("usage", {})
126
+ input_tokens = usage.get("prompt_tokens", 0)
127
+ output_tokens = usage.get("completion_tokens", 0)
128
+ total_tokens = usage.get("total_tokens", 0)
129
+
130
+ cost_usd = (
131
+ (input_tokens / 1_000_000) * config["cost_per_1m_input"] +
132
+ (output_tokens / 1_000_000) * config["cost_per_1m_output"]
133
+ )
134
+
135
+ # Update tracking
136
+ self.daily_cost += cost_usd
137
+ self.requests_count += 1
138
+
139
+ logger.info(f"✅ OpenRouter success: {agent_id} via {config['model']} - {response_time:.2f}s, ${cost_usd:.6f}")
140
+
141
+ return {
142
+ "content": content,
143
+ "response_time": response_time,
144
+ "tokens_used": total_tokens,
145
+ "cost_usd": cost_usd,
146
+ "provider": "openrouter",
147
+ "model": config["model"],
148
+ "timestamp": datetime.utcnow().isoformat()
149
+ }
150
+ else:
151
+ error_text = await response.text()
152
+ logger.error(f"❌ OpenRouter error: HTTP {response.status} - {error_text}")
153
+ return {
154
+ "error": f"OpenRouter API error: HTTP {response.status}",
155
+ "provider": "openrouter",
156
+ "response_time": response_time
157
+ }
158
+
159
+ except Exception as e:
160
+ response_time = time.time() - start_time
161
+ logger.error(f"❌ OpenRouter request failed: {e}")
162
+ return {
163
+ "error": f"OpenRouter request failed: {str(e)}",
164
+ "provider": "openrouter",
165
+ "response_time": response_time
166
+ }
167
+
168
+ async def health_check(self) -> Dict[str, Any]:
169
+ """Simple health check"""
170
+ if not self.session:
171
+ return {"status": "unhealthy", "error": "Session not initialized"}
172
+
173
+ try:
174
+ result = await self.send_message("test", "Say 'OK'", "Reply with just 'OK'")
175
+ return {
176
+ "status": "healthy" if "error" not in result else "unhealthy",
177
+ "response_time": result.get("response_time", 0),
178
+ "error": result.get("error"),
179
+ "daily_cost": self.daily_cost,
180
+ "requests_count": self.requests_count
181
+ }
182
+ except Exception as e:
183
+ return {
184
+ "status": "error",
185
+ "error": str(e)
186
+ }
187
+
188
+ def get_stats(self) -> Dict[str, Any]:
189
+ """Get simple statistics"""
190
+ return {
191
+ "provider": "openrouter",
192
+ "daily_cost_usd": round(self.daily_cost, 6),
193
+ "total_requests": self.requests_count,
194
+ "avg_cost_per_request": round(self.daily_cost / max(1, self.requests_count), 6),
195
+ "available_agents": list(self.agent_models.keys()),
196
+ "timestamp": datetime.utcnow().isoformat()
197
+ }
198
+
199
+
200
+ # Global instance for easy import
201
+ _openrouter_instance = None
202
+
203
+ async def get_openrouter() -> OpenRouterIntegration:
204
+ """Get or create OpenRouter instance"""
205
+ global _openrouter_instance
206
+
207
+ if _openrouter_instance is None:
208
+ _openrouter_instance = OpenRouterIntegration()
209
+ await _openrouter_instance.__aenter__()
210
+
211
+ return _openrouter_instance
212
+
213
+ async def send_openrouter_message(agent_id: str, message: str, system_prompt: str = "") -> Dict[str, Any]:
214
+ """Convenient function to send message via OpenRouter"""
215
+ try:
216
+ openrouter = await get_openrouter()
217
+ return await openrouter.send_message(agent_id, message, system_prompt)
218
+ except Exception as e:
219
+ logger.error(f"❌ OpenRouter message failed: {e}")
220
+ return {
221
+ "error": f"OpenRouter integration error: {str(e)}",
222
+ "provider": "openrouter"
223
+ }
224
+
225
+ async def cleanup_openrouter():
226
+ """Cleanup OpenRouter resources"""
227
+ global _openrouter_instance
228
+ if _openrouter_instance:
229
+ await _openrouter_instance.__aexit__(None, None, None)
230
+ _openrouter_instance = None
231
+ logger.info("🌐 OpenRouter integration cleaned up")
232
+
233
+
234
+ if __name__ == "__main__":
235
+ # Quick test
236
+ async def test_integration():
237
+ async with OpenRouterIntegration() as openrouter:
238
+ print("🧪 Testing OpenRouter integration...")
239
+
240
+ health = await openrouter.health_check()
241
+ print(f"Health: {health}")
242
+
243
+ if health["status"] == "healthy":
244
+ result = await openrouter.send_message("jane_alesi", "Hello! Tell me about multi-agent systems in one sentence.")
245
+ print(f"Response: {result.get('content', 'No content')}")
246
+ print(f"Cost: ${result.get('cost_usd', 0):.6f}")
247
+ print(f"Time: {result.get('response_time', 0):.2f}s")
248
+
249
+ stats = openrouter.get_stats()
250
+ print(f"Stats: {stats}")
251
+
252
+ asyncio.run(test_integration())
backend/privacy_detector.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Privacy Detector for SAAP Multi-Agent System
3
+ Detects sensitive data to route to local colossus instead of external OpenRouter
4
+ """
5
+
6
+ import re
7
+ import logging
8
+ from typing import Dict, List, Tuple
9
+ from enum import Enum
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class PrivacyLevel(Enum):
14
+ """Privacy classification levels"""
15
+ PUBLIC = "public" # Safe for external processing
16
+ INTERNAL = "internal" # Prefer local but not critical
17
+ CONFIDENTIAL = "confidential" # Should be local
18
+ PRIVATE = "private" # MUST be local (medical, financial, personal)
19
+
20
+ class PrivacyDetector:
21
+ """
22
+ Detects sensitive data in user messages to ensure privacy-compliant routing
23
+
24
+ Detection Methods:
25
+ 1. Keyword-based (medical, financial, personal data keywords)
26
+ 2. Pattern-based (credit cards, SSN, IBAN, etc.)
27
+ 3. Agent-based rules (medical agent = always private)
28
+ """
29
+
30
+ def __init__(self):
31
+ # Sensitive keyword categories
32
+ self.sensitive_keywords = {
33
+ "medical": [
34
+ "patient", "patienten", "diagnosis", "diagnose", "treatment", "behandlung",
35
+ "medication", "medikament", "symptom", "krankheit", "disease", "arzt",
36
+ "doctor", "hospital", "krankenhaus", "gesundheit", "health", "medizin",
37
+ "medicine", "therapie", "therapy", "blut", "blood", "operation"
38
+ ],
39
+ "financial": [
40
+ # Only TRULY sensitive financial data (account numbers, cards, passwords)
41
+ # General financial advice keywords removed (investment, portfolio, sparen, etc.)
42
+ "account number", "kontonummer", "password", "passwort",
43
+ "credit card", "kreditkarte", "iban", "bic",
44
+ "pin", "cvv", "security code", "sicherheitscode",
45
+ "salary", "gehalt", # Personal income data
46
+ "tax id", "steuernummer" # Personal tax data
47
+ ],
48
+ "personal": [
49
+ "social security", "sozialversicherung", "passport", "reisepass",
50
+ "driver license", "führerschein", "birthday", "geburtstag", "geburtsdatum",
51
+ "address", "adresse", "phone", "telefon", "email", "personalausweis",
52
+ "id card", "tax", "steuer", "insurance", "versicherung"
53
+ ],
54
+ "legal": [
55
+ "contract", "vertrag", "confidential", "vertraulich", "proprietary",
56
+ "nda", "geheimhaltung", "lawsuit", "klage", "court", "gericht",
57
+ "lawyer", "anwalt", "legal", "rechtlich"
58
+ ],
59
+ "security": [
60
+ "secret", "geheim", "private key", "token", "api key", "credentials",
61
+ "zugangsdaten", "authentication", "authentifizierung"
62
+ ]
63
+ }
64
+
65
+ # Sensitive data patterns (regex)
66
+ self.sensitive_patterns = [
67
+ # Credit card numbers
68
+ (r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b", "credit_card"),
69
+ # SSN (US)
70
+ (r"\b\d{3}-\d{2}-\d{4}\b", "ssn"),
71
+ # IBAN
72
+ (r"\b[A-Z]{2}\d{2}[A-Z0-9]{13,29}\b", "iban"),
73
+ # Dates (potential birthdays)
74
+ (r"\b\d{2}[./]\d{2}[./]\d{4}\b", "date"),
75
+ # Email addresses
76
+ (r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "email"),
77
+ # Phone numbers (German format)
78
+ (r"\b(?:\+49|0)\s?\d{3,4}\s?\d{6,8}\b", "phone"),
79
+ # API keys/tokens (long alphanumeric strings)
80
+ (r"\b[A-Za-z0-9]{32,}\b", "api_key")
81
+ ]
82
+
83
+ # Agent-specific privacy rules
84
+ # Only MANDATORY privacy rules here (content-based detection is preferred)
85
+ self.agent_privacy_rules = {
86
+ "lara_alesi": PrivacyLevel.PRIVATE, # Medical - ALWAYS private (health data)
87
+ "theo_alesi": PrivacyLevel.INTERNAL, # Financial - depends on content
88
+ "justus_alesi": PrivacyLevel.INTERNAL, # Legal - depends on content
89
+ "jane_alesi": PrivacyLevel.INTERNAL, # Coordinator - depends on content
90
+ "john_alesi": PrivacyLevel.INTERNAL, # Development - usually safe
91
+ "leon_alesi": PrivacyLevel.INTERNAL, # System - usually safe
92
+ "luna_alesi": PrivacyLevel.INTERNAL # Coaching - usually safe
93
+ }
94
+
95
+ def detect_privacy_level(
96
+ self,
97
+ message: str,
98
+ agent_id: str = None,
99
+ user_privacy_flag: str = None
100
+ ) -> Tuple[PrivacyLevel, Dict]:
101
+ """
102
+ Detect privacy level of message
103
+
104
+ Args:
105
+ message: User message to analyze
106
+ agent_id: Agent that will process this (optional)
107
+ user_privacy_flag: User-specified privacy level (optional)
108
+
109
+ Returns:
110
+ Tuple of (PrivacyLevel, detection_details)
111
+ """
112
+
113
+ # User override takes precedence
114
+ if user_privacy_flag:
115
+ level = self._parse_user_privacy_flag(user_privacy_flag)
116
+ return level, {"reason": "user_override", "user_flag": user_privacy_flag}
117
+
118
+ # Check agent-specific rules first
119
+ if agent_id and agent_id in self.agent_privacy_rules:
120
+ agent_level = self.agent_privacy_rules[agent_id]
121
+ if agent_level == PrivacyLevel.PRIVATE:
122
+ return agent_level, {
123
+ "reason": "agent_rule",
124
+ "agent": agent_id,
125
+ "rule": "always_private"
126
+ }
127
+
128
+ # Keyword detection
129
+ keyword_matches = self._detect_keywords(message)
130
+
131
+ # Pattern detection
132
+ pattern_matches = self._detect_patterns(message)
133
+
134
+ # Combine detections
135
+ total_detections = len(keyword_matches) + len(pattern_matches)
136
+
137
+ # Determine privacy level based on detections
138
+ details = {
139
+ "keyword_matches": keyword_matches,
140
+ "pattern_matches": pattern_matches,
141
+ "total_detections": total_detections
142
+ }
143
+
144
+ # Classification logic
145
+ if pattern_matches: # Any pattern match = high sensitivity
146
+ if any(cat in ["credit_card", "ssn", "iban"] for pat, cat in pattern_matches):
147
+ return PrivacyLevel.PRIVATE, {**details, "reason": "sensitive_pattern"}
148
+
149
+ if keyword_matches:
150
+ categories = set(cat for cat, _ in keyword_matches)
151
+
152
+ # Medical or financial keywords = PRIVATE
153
+ if "medical" in categories or "financial" in categories:
154
+ return PrivacyLevel.PRIVATE, {**details, "reason": "sensitive_keywords"}
155
+
156
+ # Personal or legal = CONFIDENTIAL
157
+ if "personal" in categories or "legal" in categories:
158
+ return PrivacyLevel.CONFIDENTIAL, {**details, "reason": "confidential_keywords"}
159
+
160
+ # Security keywords = CONFIDENTIAL
161
+ if "security" in categories:
162
+ return PrivacyLevel.CONFIDENTIAL, {**details, "reason": "security_keywords"}
163
+
164
+ # Apply agent rule if no strong detection
165
+ if agent_id and agent_id in self.agent_privacy_rules:
166
+ return self.agent_privacy_rules[agent_id], {
167
+ **details,
168
+ "reason": "agent_default",
169
+ "agent": agent_id
170
+ }
171
+
172
+ # Default: PUBLIC (safe for external processing)
173
+ return PrivacyLevel.PUBLIC, {**details, "reason": "no_sensitive_data"}
174
+
175
+ def _detect_keywords(self, message: str) -> List[Tuple[str, str]]:
176
+ """Detect sensitive keywords in message using word boundaries"""
177
+ message_lower = message.lower()
178
+ matches = []
179
+
180
+ for category, keywords in self.sensitive_keywords.items():
181
+ for keyword in keywords:
182
+ # Use word boundaries to avoid false positives
183
+ # "health" in "wealth" won't match
184
+ pattern = r'\b' + re.escape(keyword) + r'\b'
185
+ if re.search(pattern, message_lower):
186
+ matches.append((category, keyword))
187
+
188
+ return matches
189
+
190
+ def _detect_patterns(self, message: str) -> List[Tuple[str, str]]:
191
+ """Detect sensitive patterns in message"""
192
+ matches = []
193
+
194
+ for pattern, category in self.sensitive_patterns:
195
+ if re.search(pattern, message):
196
+ matches.append((pattern, category))
197
+
198
+ return matches
199
+
200
+ def _parse_user_privacy_flag(self, flag: str) -> PrivacyLevel:
201
+ """Parse user-specified privacy flag"""
202
+ flag_lower = flag.lower()
203
+
204
+ if flag_lower in ["private", "high", "strict"]:
205
+ return PrivacyLevel.PRIVATE
206
+ elif flag_lower in ["confidential", "medium"]:
207
+ return PrivacyLevel.CONFIDENTIAL
208
+ elif flag_lower in ["internal", "low"]:
209
+ return PrivacyLevel.INTERNAL
210
+ else:
211
+ return PrivacyLevel.PUBLIC
212
+
213
+ def should_use_local_provider(self, privacy_level: PrivacyLevel, mode: str = "balanced") -> bool:
214
+ """
215
+ Determine if local provider should be used based on privacy level and mode
216
+
217
+ Args:
218
+ privacy_level: Detected privacy level
219
+ mode: Privacy mode (strict, balanced, performance)
220
+
221
+ Returns:
222
+ True if should use local provider (colossus), False if external OK (OpenRouter)
223
+ """
224
+
225
+ if mode == "strict":
226
+ # Strict: Everything goes local
227
+ return True
228
+
229
+ elif mode == "balanced":
230
+ # Balanced: Private and Confidential go local
231
+ return privacy_level in [PrivacyLevel.PRIVATE, PrivacyLevel.CONFIDENTIAL]
232
+
233
+ elif mode == "performance":
234
+ # Performance: Only explicitly PRIVATE goes local
235
+ return privacy_level == PrivacyLevel.PRIVATE
236
+
237
+ else:
238
+ # Default to balanced
239
+ return privacy_level in [PrivacyLevel.PRIVATE, PrivacyLevel.CONFIDENTIAL]
240
+
241
+
242
+ # Singleton instance
243
+ privacy_detector = PrivacyDetector()
244
+
245
+
246
+ # Convenience functions
247
+ def detect_privacy_level(message: str, agent_id: str = None, user_flag: str = None) -> Tuple[PrivacyLevel, Dict]:
248
+ """Convenience function to detect privacy level"""
249
+ return privacy_detector.detect_privacy_level(message, agent_id, user_flag)
250
+
251
+
252
+ def should_use_local(message: str, agent_id: str = None, mode: str = "balanced") -> bool:
253
+ """Convenience function to check if local provider should be used"""
254
+ level, details = privacy_detector.detect_privacy_level(message, agent_id)
255
+ use_local = privacy_detector.should_use_local_provider(level, mode)
256
+
257
+ logger.info(f"Privacy check: {level.value} → {'LOCAL' if use_local else 'EXTERNAL'} ({details.get('reason', 'unknown')})")
258
+
259
+ return use_local
260
+
261
+
262
+ if __name__ == "__main__":
263
+ # Demo privacy detection
264
+ test_cases = [
265
+ ("What is Python?", "john_alesi"),
266
+ ("My patient has symptoms of diabetes", "lara_alesi"),
267
+ ("Analyze my bank account: DE89370400440532013000", "theo_alesi"),
268
+ ("Review this confidential contract", "justus_alesi"),
269
+ ("How to optimize this code?", "john_alesi"),
270
+ ("My credit card number is 4532-1234-5678-9010", None)
271
+ ]
272
+
273
+ print("🔒 Privacy Detection Demo\n")
274
+
275
+ for message, agent in test_cases:
276
+ level, details = detect_privacy_level(message, agent)
277
+ use_local = privacy_detector.should_use_local_provider(level, mode="balanced")
278
+
279
+ print(f"Message: '{message[:50]}...'")
280
+ print(f"Agent: {agent or 'None'}")
281
+ print(f"Privacy Level: {level.value}")
282
+ print(f"Routing: {'LOCAL (colossus)' if use_local else 'EXTERNAL (OpenRouter)'}")
283
+ print(f"Reason: {details.get('reason')}")
284
+ if details.get('keyword_matches'):
285
+ print(f"Keywords: {details['keyword_matches']}")
286
+ if details.get('pattern_matches'):
287
+ print(f"Patterns: {details['pattern_matches']}")
288
+ print()
backend/requirements.txt CHANGED
@@ -15,9 +15,9 @@ sqlalchemy==2.0.36 # Updated for Python 3.13 compatibility
15
  alembic==1.14.0 # Updated to match SQLAlchemy version
16
  aiosqlite==0.20.0 # Async SQLite driver (REQUIRED for async database operations)
17
  greenlet>=3.0.0 # Required for SQLAlchemy async operations
 
18
  asyncpg==0.30.0 # Async PostgreSQL driver (Python 3.13 compatible)
19
- psycopg[binary]==3.2.3 # Modern PostgreSQL driver (Python 3.13 compatible, replaces psycopg2)
20
- # Note: Using both psycopg3 (modern) and asyncpg (async) for PostgreSQL compatibility
21
 
22
  # HTTP Clients
23
  httpx==0.26.0
@@ -48,10 +48,3 @@ orjson==3.9.12
48
 
49
  # CORS (handled by FastAPI's built-in CORSMiddleware)
50
  # No separate package needed - use fastapi.middleware.cors.CORSMiddleware
51
-
52
- # Document Processing (for file upload functionality)
53
- PyPDF2==3.0.1 # PDF text extraction
54
- python-docx==1.1.0 # DOCX/DOC text extraction
55
-
56
- # System Monitoring (for metrics endpoints)
57
- psutil==5.9.8 # CPU, Memory, Disk, Network monitoring
 
15
  alembic==1.14.0 # Updated to match SQLAlchemy version
16
  aiosqlite==0.20.0 # Async SQLite driver (REQUIRED for async database operations)
17
  greenlet>=3.0.0 # Required for SQLAlchemy async operations
18
+ psycopg2-binary==2.9.9 # PostgreSQL driver (works in Docker with Debian base)
19
  asyncpg==0.30.0 # Async PostgreSQL driver (Python 3.13 compatible)
20
+ # Note: Using both psycopg2 (sync) and asyncpg (async) for PostgreSQL compatibility
 
21
 
22
  # HTTP Clients
23
  httpx==0.26.0
 
48
 
49
  # CORS (handled by FastAPI's built-in CORSMiddleware)
50
  # No separate package needed - use fastapi.middleware.cors.CORSMiddleware
 
 
 
 
 
 
 
backend/service.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SAAP Database Service - High-level Database Operations
3
+ Simplified database operations for Agent Manager with proper initialization
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ from typing import Dict, List, Optional, Any
9
+ from datetime import datetime
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+ from sqlalchemy import select, update, delete
12
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
13
+
14
+ from database.connection import db_manager
15
+ from database.models import DBAgent, DBChatMessage, DBAgentSession
16
+ from models.agent import SaapAgent, AgentStatus
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class DatabaseService:
21
+ """
22
+ High-level database service for SAAP Agent operations
23
+ Handles all database operations with proper error handling
24
+ """
25
+
26
+ def __init__(self):
27
+ self.is_ready = False
28
+
29
+ async def initialize(self):
30
+ """Initialize database service and ensure tables exist"""
31
+ try:
32
+ logger.info("🗄️ Initializing SAAP Database Service...")
33
+
34
+ # Initialize database manager if not already done
35
+ if not db_manager.is_initialized:
36
+ await db_manager.initialize()
37
+
38
+ # Force database tables creation
39
+ await self._ensure_tables_exist()
40
+
41
+ self.is_ready = True
42
+ logger.info("✅ Database Service initialized successfully")
43
+
44
+ except Exception as e:
45
+ logger.error(f"❌ Database Service initialization failed: {e}")
46
+ raise
47
+
48
+ async def _ensure_tables_exist(self):
49
+ """Ensure all required database tables exist"""
50
+ try:
51
+ # Force table creation by running a simple query
52
+ async with db_manager.get_async_session() as session:
53
+ # Test if agents table exists by trying to count rows
54
+ try:
55
+ result = await session.execute(select(DBAgent))
56
+ agents = result.scalars().all()
57
+ logger.info(f"📚 Found {len(agents)} existing agents in database")
58
+ except Exception as table_error:
59
+ logger.warning(f"⚠️ Agents table not ready: {table_error}")
60
+ # Tables should already be created by db_manager.initialize()
61
+
62
+ except Exception as e:
63
+ logger.error(f"❌ Failed to verify tables: {e}")
64
+ raise
65
+
66
+ async def save_agent(self, agent: SaapAgent) -> bool:
67
+ """Save or update agent in database"""
68
+ try:
69
+ if not self.is_ready:
70
+ await self.initialize()
71
+
72
+ async with db_manager.get_async_session() as session:
73
+ # Check if agent exists
74
+ existing = await session.execute(
75
+ select(DBAgent).where(DBAgent.id == agent.id)
76
+ )
77
+ db_agent = existing.scalar_one_or_none()
78
+
79
+ if db_agent:
80
+ # Update existing agent
81
+ for key, value in DBAgent.from_saap_agent(agent).__dict__.items():
82
+ if not key.startswith('_'): # Skip SQLAlchemy internal attributes
83
+ setattr(db_agent, key, value)
84
+
85
+ db_agent.updated_at = datetime.utcnow()
86
+ logger.info(f"🔄 Updating existing agent: {agent.name}")
87
+ else:
88
+ # Create new agent
89
+ db_agent = DBAgent.from_saap_agent(agent)
90
+ session.add(db_agent)
91
+ logger.info(f"➕ Creating new agent: {agent.name}")
92
+
93
+ await session.commit()
94
+ logger.info(f"✅ Agent saved successfully: {agent.name} ({agent.id})")
95
+ return True
96
+
97
+ except IntegrityError as ie:
98
+ logger.error(f"❌ Database integrity error saving agent {agent.id}: {ie}")
99
+ return False
100
+ except Exception as e:
101
+ logger.error(f"❌ Failed to save agent {agent.id}: {e}")
102
+ return False
103
+
104
+ async def load_agent(self, agent_id: str) -> Optional[SaapAgent]:
105
+ """Load agent from database"""
106
+ try:
107
+ if not self.is_ready:
108
+ await self.initialize()
109
+
110
+ async with db_manager.get_async_session() as session:
111
+ result = await session.execute(
112
+ select(DBAgent).where(DBAgent.id == agent_id)
113
+ )
114
+ db_agent = result.scalar_one_or_none()
115
+
116
+ if db_agent:
117
+ agent = db_agent.to_saap_agent()
118
+ logger.debug(f"📖 Loaded agent: {agent.name}")
119
+ return agent
120
+ else:
121
+ logger.debug(f"❓ Agent not found in database: {agent_id}")
122
+ return None
123
+
124
+ except Exception as e:
125
+ logger.error(f"❌ Failed to load agent {agent_id}: {e}")
126
+ return None
127
+
128
+ async def load_all_agents(self) -> List[SaapAgent]:
129
+ """Load all agents from database"""
130
+ try:
131
+ if not self.is_ready:
132
+ await self.initialize()
133
+
134
+ async with db_manager.get_async_session() as session:
135
+ result = await session.execute(select(DBAgent))
136
+ db_agents = result.scalars().all()
137
+
138
+ agents = []
139
+ for db_agent in db_agents:
140
+ try:
141
+ agent = db_agent.to_saap_agent()
142
+ agents.append(agent)
143
+ except Exception as conv_error:
144
+ logger.error(f"⚠️ Failed to convert agent {db_agent.id}: {conv_error}")
145
+ continue
146
+
147
+ logger.info(f"📚 Loaded {len(agents)} agents from database")
148
+ return agents
149
+
150
+ except Exception as e:
151
+ logger.error(f"❌ Failed to load agents from database: {e}")
152
+ return []
153
+
154
+ async def delete_agent(self, agent_id: str) -> bool:
155
+ """Delete agent from database"""
156
+ try:
157
+ if not self.is_ready:
158
+ await self.initialize()
159
+
160
+ async with db_manager.get_async_session() as session:
161
+ # Delete agent (cascading will handle related records)
162
+ result = await session.execute(
163
+ delete(DBAgent).where(DBAgent.id == agent_id)
164
+ )
165
+
166
+ await session.commit()
167
+
168
+ if result.rowcount > 0:
169
+ logger.info(f"🗑️ Agent deleted from database: {agent_id}")
170
+ return True
171
+ else:
172
+ logger.warning(f"❓ Agent not found for deletion: {agent_id}")
173
+ return False
174
+
175
+ except Exception as e:
176
+ logger.error(f"❌ Failed to delete agent {agent_id}: {e}")
177
+ return False
178
+
179
+ async def update_agent_status(self, agent_id: str, status: AgentStatus) -> bool:
180
+ """Update agent status in database"""
181
+ try:
182
+ if not self.is_ready:
183
+ await self.initialize()
184
+
185
+ async with db_manager.get_async_session() as session:
186
+ result = await session.execute(
187
+ update(DBAgent)
188
+ .where(DBAgent.id == agent_id)
189
+ .values(
190
+ status=status.value,
191
+ last_active=datetime.utcnow(),
192
+ updated_at=datetime.utcnow()
193
+ )
194
+ )
195
+
196
+ await session.commit()
197
+
198
+ if result.rowcount > 0:
199
+ logger.debug(f"📊 Agent status updated: {agent_id} -> {status.value}")
200
+ return True
201
+ else:
202
+ logger.warning(f"❓ Agent not found for status update: {agent_id}")
203
+ return False
204
+
205
+ except Exception as e:
206
+ logger.error(f"❌ Failed to update agent status {agent_id}: {e}")
207
+ return False
208
+
209
+ async def save_chat_message(self, agent_id: str, user_message: str,
210
+ agent_response: str, response_time: float,
211
+ tokens_used: int = 0, metadata: Dict = None) -> bool:
212
+ """Save chat message to database"""
213
+ try:
214
+ if not self.is_ready:
215
+ await self.initialize()
216
+
217
+ async with db_manager.get_async_session() as session:
218
+ chat_message = DBChatMessage(
219
+ agent_id=agent_id,
220
+ user_message=user_message,
221
+ agent_response=agent_response,
222
+ response_time=response_time,
223
+ tokens_used=tokens_used,
224
+ message_metadata=metadata or {}
225
+ )
226
+
227
+ session.add(chat_message)
228
+ await session.commit()
229
+
230
+ logger.debug(f"💬 Chat message saved for agent: {agent_id}")
231
+ return True
232
+
233
+ except Exception as e:
234
+ logger.error(f"❌ Failed to save chat message for {agent_id}: {e}")
235
+ return False
236
+
237
+ async def get_agent_chat_history(self, agent_id: str, limit: int = 50) -> List[Dict]:
238
+ """Get chat history for an agent"""
239
+ try:
240
+ if not self.is_ready:
241
+ await self.initialize()
242
+
243
+ async with db_manager.get_async_session() as session:
244
+ result = await session.execute(
245
+ select(DBChatMessage)
246
+ .where(DBChatMessage.agent_id == agent_id)
247
+ .order_by(DBChatMessage.created_at.desc())
248
+ .limit(limit)
249
+ )
250
+
251
+ messages = result.scalars().all()
252
+
253
+ return [
254
+ {
255
+ "user_message": msg.user_message,
256
+ "agent_response": msg.agent_response,
257
+ "response_time": msg.response_time,
258
+ "tokens_used": msg.tokens_used,
259
+ "created_at": msg.created_at.isoformat(),
260
+ "metadata": msg.message_metadata or {}
261
+ }
262
+ for msg in messages
263
+ ]
264
+
265
+ except Exception as e:
266
+ logger.error(f"❌ Failed to get chat history for {agent_id}: {e}")
267
+ return []
268
+
269
+ async def health_check(self) -> Dict[str, Any]:
270
+ """Check database service health"""
271
+ try:
272
+ if not db_manager.is_initialized:
273
+ return {"status": "not_initialized"}
274
+
275
+ # Try to count agents
276
+ async with db_manager.get_async_session() as session:
277
+ result = await session.execute(select(DBAgent))
278
+ agent_count = len(result.scalars().all())
279
+
280
+ return {
281
+ "status": "healthy",
282
+ "service_ready": self.is_ready,
283
+ "agent_count": agent_count,
284
+ "timestamp": datetime.utcnow().isoformat()
285
+ }
286
+
287
+ except Exception as e:
288
+ return {
289
+ "status": "error",
290
+ "error": str(e),
291
+ "service_ready": self.is_ready,
292
+ "timestamp": datetime.utcnow().isoformat()
293
+ }
294
+
295
+ # Global database service instance
296
+ database_service = DatabaseService()
297
+
298
+ # Convenience functions
299
+ async def ensure_database_ready():
300
+ """Ensure database service is ready"""
301
+ if not database_service.is_ready:
302
+ await database_service.initialize()
303
+
304
+ if __name__ == "__main__":
305
+ async def test_database_service():
306
+ """Test database service functionality"""
307
+ print("🧪 Testing Database Service...")
308
+
309
+ service = DatabaseService()
310
+ await service.initialize()
311
+
312
+ # Test health check
313
+ health = await service.health_check()
314
+ print(f"Health: {health}")
315
+
316
+ # Test loading agents
317
+ agents = await service.load_all_agents()
318
+ print(f"Loaded agents: {[a.name for a in agents]}")
319
+
320
+ asyncio.run(test_database_service())
backend/services/privacy_detector.py CHANGED
@@ -259,100 +259,6 @@ def should_use_local(message: str, agent_id: str = None, mode: str = "balanced")
259
  return use_local
260
 
261
 
262
- def analyze_document_privacy(
263
- document_text: str,
264
- filename: str = "",
265
- agent_id: str = None
266
- ) -> Tuple[PrivacyLevel, Dict]:
267
- """
268
- 📄 Analyze privacy level of uploaded document content
269
-
270
- Args:
271
- document_text: Extracted text from document (PDF, DOCX, TXT)
272
- filename: Original filename (optional, for context)
273
- agent_id: Agent that will process this (optional)
274
-
275
- Returns:
276
- Tuple of (PrivacyLevel, detection_details)
277
- """
278
- if not document_text or not document_text.strip():
279
- return PrivacyLevel.PUBLIC, {
280
- "reason": "empty_document",
281
- "filename": filename
282
- }
283
-
284
- # Use same detection as messages but with document-specific context
285
- level, details = privacy_detector.detect_privacy_level(document_text, agent_id)
286
-
287
- # Add document-specific information
288
- details["source"] = "document"
289
- details["filename"] = filename
290
- details["content_length"] = len(document_text)
291
-
292
- # Additional document-specific checks
293
- text_lower = document_text.lower()
294
-
295
- # Check for document type indicators
296
- document_indicators = []
297
-
298
- if any(word in text_lower for word in ["medical record", "patient chart", "krankenakte", "patientenakte"]):
299
- document_indicators.append("medical_record")
300
- level = PrivacyLevel.PRIVATE # Override to PRIVATE
301
-
302
- if any(word in text_lower for word in ["balance sheet", "bilanz", "financial statement", "finanzübersicht"]):
303
- document_indicators.append("financial_statement")
304
- if level.value != "private":
305
- level = PrivacyLevel.CONFIDENTIAL
306
-
307
- if any(word in text_lower for word in ["confidential", "vertraulich", "streng vertraulich", "classified"]):
308
- document_indicators.append("marked_confidential")
309
- if level.value not in ["private", "confidential"]:
310
- level = PrivacyLevel.CONFIDENTIAL
311
-
312
- if any(word in text_lower for word in ["contract", "vertrag", "agreement", "vereinbarung", "nda"]):
313
- document_indicators.append("legal_document")
314
- if level.value not in ["private", "confidential"]:
315
- level = PrivacyLevel.CONFIDENTIAL
316
-
317
- if document_indicators:
318
- details["document_indicators"] = document_indicators
319
- details["reason"] = f"document_type: {', '.join(document_indicators)}"
320
-
321
- logger.info(f"📄 Document privacy analysis: {filename} → {level.value} ({details.get('reason')})")
322
-
323
- return level, details
324
-
325
-
326
- def should_use_local_for_document(
327
- document_text: str,
328
- filename: str = "",
329
- agent_id: str = None,
330
- mode: str = "balanced"
331
- ) -> Tuple[bool, PrivacyLevel, Dict]:
332
- """
333
- 📄 Determine if document should be processed locally
334
-
335
- Args:
336
- document_text: Extracted document content
337
- filename: Original filename
338
- agent_id: Target agent (optional)
339
- mode: Privacy mode (strict, balanced, performance)
340
-
341
- Returns:
342
- Tuple of (use_local, privacy_level, details)
343
- """
344
- level, details = analyze_document_privacy(document_text, filename, agent_id)
345
- use_local = privacy_detector.should_use_local_provider(level, mode)
346
-
347
- logger.info(
348
- f"📄 Document routing: {filename} → "
349
- f"{'LOCAL (colossus)' if use_local else 'EXTERNAL (OpenRouter)'} "
350
- f"[{level.value}]"
351
- )
352
-
353
- return use_local, level, details
354
-
355
-
356
  if __name__ == "__main__":
357
  # Demo privacy detection
358
  test_cases = [
 
259
  return use_local
260
 
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  if __name__ == "__main__":
263
  # Demo privacy detection
264
  test_cases = [
backend/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
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",
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/test_colossus_integration.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ SAAP colossus Server Integration Test Script
4
+ Quick-Test für colossus Server Integration und Performance-Benchmark
5
+
6
+ Usage:
7
+ python test_colossus_integration.py
8
+
9
+ Author: Hanan Wandji Danga
10
+ """
11
+
12
+ import asyncio
13
+ import sys
14
+ import os
15
+ import json
16
+ import time
17
+ from dotenv import load_dotenv
18
+
19
+ # Load environment variables
20
+ load_dotenv()
21
+
22
+ # Add parent directory to path for imports
23
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
24
+
25
+ from agents.colossus_saap_agent import ColossusSAAPAgent, ColossusBenchmark, create_saap_colossus_agents
26
+
27
+ # colossus Server Configuration
28
+ API_KEY = os.getenv("COLOSSUS_API_KEY", "")
29
+ BASE_URL = "https://ai.adrian-schupp.de"
30
+ MODEL_NAME = "mistral-small3.2:24b-instruct-2506"
31
+
32
+ async def test_colossus_connection():
33
+ """
34
+ Test 1: Grundlegende Konnektivität und API-Funktionalität
35
+ """
36
+ print("🔗 TEST 1: colossus Server Connection Test")
37
+ print("=" * 50)
38
+
39
+ agent = ColossusSAAPAgent("test_coordinator", "Coordinator", API_KEY, BASE_URL)
40
+
41
+ # Health Check
42
+ print("📊 Running health check...")
43
+ health = await agent.health_check()
44
+
45
+ print(f"Status: {health['colossus_status']}")
46
+ print(f"Response Time: {health.get('response_time', 'N/A')}s")
47
+
48
+ if health.get('error'):
49
+ print(f"❌ Error: {health['error']}")
50
+ return False
51
+
52
+ print("✅ colossus Server is healthy and reachable")
53
+ return True
54
+
55
+ async def test_saap_agent_roles():
56
+ """
57
+ Test 2: SAAP Agent Rollen-spezifische Responses
58
+ """
59
+ print("\n🤖 TEST 2: SAAP Agent Role-Specific Responses")
60
+ print("=" * 50)
61
+
62
+ # Erstelle die 3 Basis-Agenten aus dem Plan
63
+ agents = create_saap_colossus_agents(API_KEY)
64
+
65
+ test_questions = [
66
+ {
67
+ "agent": "agent_coordinator",
68
+ "role": "Coordinator",
69
+ "question": "Wie koordinierst du mehrere Agenten in einem SAAP-System?"
70
+ },
71
+ {
72
+ "agent": "agent_developer",
73
+ "role": "Developer",
74
+ "question": "Welche Python-Bibliotheken empfiehlst du für Multi-Agent-Systeme?"
75
+ },
76
+ {
77
+ "agent": "agent_analyst",
78
+ "role": "Analyst",
79
+ "question": "Analysiere die Performance-Vorteile von On-Premise vs Cloud AI."
80
+ }
81
+ ]
82
+
83
+ for test in test_questions:
84
+ print(f"\n👤 Testing {test['role']} Agent...")
85
+
86
+ # Find entsprechenden Agent
87
+ agent = next((a for a in agents if a.agent_name == test['agent']), None)
88
+ if not agent:
89
+ print(f"❌ Agent {test['agent']} not found")
90
+ continue
91
+
92
+ # Sende Anfrage
93
+ result = await agent.send_request_to_colossus(test['question'])
94
+
95
+ if result['success']:
96
+ print(f"✅ Response Time: {result['response_time']}s")
97
+ print(f"📝 Response: {result['response'][:200]}...")
98
+ print(f"🔢 Tokens: {result['token_count']}")
99
+ else:
100
+ print(f"❌ Failed: {result.get('error')}")
101
+
102
+ async def test_performance_benchmark():
103
+ """
104
+ Test 3: Performance Benchmark für < 2s Response-Zeit Ziel
105
+ """
106
+ print("\n⚡ TEST 3: Performance Benchmark (Target: < 2s)")
107
+ print("=" * 50)
108
+
109
+ benchmark = ColossusBenchmark(API_KEY)
110
+
111
+ # SAAP-spezifische Test-Prompts
112
+ test_prompts = [
113
+ "Was ist SAAP?",
114
+ "Erkläre Multi-Agent Systeme kurz.",
115
+ "Vorteile von On-Premise AI?",
116
+ "Wie funktioniert Agent-Koordination?",
117
+ "Python für AI-Entwicklung?"
118
+ ]
119
+
120
+ print(f"🧪 Running {len(test_prompts)} performance tests...")
121
+ results = await benchmark.run_performance_benchmark(test_prompts)
122
+
123
+ print(f"\n📊 BENCHMARK RESULTS:")
124
+ print(f"Total Tests: {results['total_tests']}")
125
+ print(f"Successful Tests: {results['successful_tests']}")
126
+ print(f"Success Rate: {results['success_rate']:.1f}%")
127
+
128
+ if results.get('average_response_time'):
129
+ print(f"Average Response Time: {results['average_response_time']}s")
130
+ print(f"Average Token Count: {results['average_token_count']}")
131
+
132
+ # Performance Target Check
133
+ target_met = results['performance_target_met']
134
+ target_symbol = "✅" if target_met else "❌"
135
+ print(f"{target_symbol} Performance Target (< 2s): {'MET' if target_met else 'NOT MET'}")
136
+
137
+ if not target_met:
138
+ print("💡 Optimization suggestions will be provided for performance improvement")
139
+ else:
140
+ print("❌ No successful tests - check connection and API key")
141
+
142
+ return results
143
+
144
+ async def test_multi_agent_communication():
145
+ """
146
+ Test 4: Multi-Agent Kommunikation via Redis
147
+ """
148
+ print("\n💬 TEST 4: Multi-Agent Communication Test")
149
+ print("=" * 50)
150
+
151
+ try:
152
+ import redis
153
+ redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
154
+
155
+ # Test Redis connection
156
+ redis_client.ping()
157
+ print("✅ Redis connection successful")
158
+
159
+ # Erstelle 2 Test-Agenten
160
+ coordinator = ColossusSAAPAgent("test_coordinator", "Coordinator", API_KEY)
161
+ developer = ColossusSAAPAgent("test_developer", "Developer", API_KEY)
162
+
163
+ # Sende Nachricht von Coordinator zu Developer
164
+ test_message = "Entwickle ein Python-Script für Agent-Kommunikation"
165
+ print(f"📤 Coordinator → Developer: {test_message}")
166
+
167
+ coordinator.send_message_to_agent("test_developer", test_message)
168
+
169
+ # Simuliere Developer Response
170
+ print("🔄 Developer processing message...")
171
+ await asyncio.sleep(1)
172
+
173
+ # Check Redis queue
174
+ queue_length = redis_client.llen("saap_agent_test_developer")
175
+ print(f"📊 Developer message queue length: {queue_length}")
176
+
177
+ if queue_length > 0:
178
+ print("✅ Multi-Agent communication working")
179
+ else:
180
+ print("⚠️ No messages in queue - check Redis setup")
181
+
182
+ except ImportError:
183
+ print("❌ Redis not available - install with: pip install redis")
184
+ except Exception as e:
185
+ print(f"❌ Redis connection failed: {e}")
186
+ print("💡 Make sure Redis/Valkey is running: systemctl start valkey")
187
+
188
+ def print_next_steps(performance_results):
189
+ """
190
+ Druckt Next Steps basierend auf Test-Ergebnissen
191
+ """
192
+ print("\n🎯 NEXT STEPS - Week 1-2 Infrastructure Foundation")
193
+ print("=" * 60)
194
+
195
+ if performance_results and performance_results.get('success_rate', 0) > 80:
196
+ print("✅ colossus Integration erfolgreich!")
197
+ print("🚀 Ready für Phase 1 Implementation:")
198
+ print(" 1. Integriere colossus Agents in bestehendes SAAP-System")
199
+ print(" 2. Update Vue.js Dashboard für colossus Agent-Monitoring")
200
+ print(" 3. Teste 3 Basis-Agenten (Coordinator, Developer, Analyst)")
201
+ print(" 4. OpenRouter GLM 4.5 Air als Fallback konfigurieren")
202
+ print(" 5. Performance-Optimierung wenn Response-Zeit > 2s")
203
+
204
+ print(f"\n📊 Performance Status:")
205
+ avg_time = performance_results.get('average_response_time', 0)
206
+ if avg_time < 2.0:
207
+ print(f" ✅ Target erfüllt: {avg_time}s < 2s")
208
+ else:
209
+ print(f" ⚠️ Optimierung nötig: {avg_time}s > 2s")
210
+ print(" 💡 Optimierungen: Model-Caching, Connection-Pooling, Prompt-Optimization")
211
+ else:
212
+ print("❌ colossus Integration needs fixes:")
213
+ print(" 1. Check API Key and URL")
214
+ print(" 2. Verify network connectivity")
215
+ print(" 3. Test model availability")
216
+ print(" 4. Check server status")
217
+
218
+ async def main():
219
+ """
220
+ Hauptfunktion - führt alle Tests sequentiell durch
221
+ """
222
+ print("🚀 SAAP colossus Server Integration Tests")
223
+ print("=" * 60)
224
+ print(f"Target: {BASE_URL}")
225
+ print(f"Model: {MODEL_NAME}")
226
+ print(f"Performance Goal: < 2s response time")
227
+ print("")
228
+
229
+ # Validate API key
230
+ if not API_KEY:
231
+ print("❌ Error: COLOSSUS_API_KEY environment variable not set")
232
+ print("Please add COLOSSUS_API_KEY to backend/.env file")
233
+ return
234
+
235
+ # Test 1: Basic Connectivity
236
+ connection_ok = await test_colossus_connection()
237
+
238
+ if not connection_ok:
239
+ print("\n❌ Basic connection failed. Check server and API key.")
240
+ return
241
+
242
+ # Test 2: Role-specific responses
243
+ await test_saap_agent_roles()
244
+
245
+ # Test 3: Performance benchmark
246
+ performance_results = await test_performance_benchmark()
247
+
248
+ # Test 4: Multi-Agent communication
249
+ await test_multi_agent_communication()
250
+
251
+ # Summary and next steps
252
+ print_next_steps(performance_results)
253
+
254
+ print(f"\n🎓 Integration Status: Ready for SAAP Phase 1 Development")
255
+ print("📚 Master Thesis Progress: Infrastructure Foundation Complete")
256
+
257
+ if __name__ == "__main__":
258
+ # Run all tests
259
+ asyncio.run(main())
backend/websocket_manager.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SAAP WebSocket Manager Service - Real-time Communication
3
+ Production-ready WebSocket connection management for live agent updates
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ from typing import Set, Dict, Any, Optional
10
+ from datetime import datetime
11
+
12
+ from fastapi import WebSocket, WebSocketDisconnect
13
+ from models.agent import SaapAgent
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class WebSocketConnection:
18
+ """Individual WebSocket connection wrapper"""
19
+
20
+ def __init__(self, websocket: WebSocket, client_id: Optional[str] = None):
21
+ self.websocket = websocket
22
+ self.client_id = client_id or f"client_{id(websocket)}"
23
+ self.connected_at = datetime.utcnow()
24
+ self.is_alive = True
25
+
26
+ async def send_message(self, message: Dict[str, Any]):
27
+ """Send message to this WebSocket connection"""
28
+ try:
29
+ if self.is_alive:
30
+ await self.websocket.send_text(json.dumps(message))
31
+ except Exception as e:
32
+ logger.warning(f"⚠️ Failed to send to {self.client_id}: {e}")
33
+ self.is_alive = False
34
+
35
+ async def send_ping(self):
36
+ """Send ping to keep connection alive"""
37
+ try:
38
+ if self.is_alive:
39
+ await self.websocket.send_text(json.dumps({
40
+ "type": "ping",
41
+ "timestamp": datetime.utcnow().isoformat()
42
+ }))
43
+ except Exception:
44
+ self.is_alive = False
45
+
46
+ class WebSocketManager:
47
+ """
48
+ Production-ready WebSocket Manager
49
+ Features:
50
+ - Multi-client connection management
51
+ - Real-time agent status broadcasts
52
+ - Message history and statistics
53
+ - Connection health monitoring
54
+ - Automatic cleanup of dead connections
55
+ """
56
+
57
+ def __init__(self):
58
+ self.active_connections: Set[WebSocketConnection] = set()
59
+ self.client_connections: Dict[str, WebSocketConnection] = {}
60
+ self.message_history: List[Dict[str, Any]] = []
61
+ self.max_history_size = 100
62
+ self.stats = {
63
+ "total_connections": 0,
64
+ "current_connections": 0,
65
+ "messages_sent": 0,
66
+ "broadcasts_sent": 0
67
+ }
68
+
69
+ # Start periodic cleanup task
70
+ self._cleanup_task = None
71
+
72
+ async def start_cleanup_task(self):
73
+ """Start periodic cleanup of dead connections"""
74
+ if not self._cleanup_task:
75
+ self._cleanup_task = asyncio.create_task(self._periodic_cleanup())
76
+
77
+ async def _periodic_cleanup(self):
78
+ """Periodically clean up dead connections"""
79
+ try:
80
+ while True:
81
+ await asyncio.sleep(30) # Check every 30 seconds
82
+ await self._cleanup_dead_connections()
83
+ except asyncio.CancelledError:
84
+ logger.info("🔧 WebSocket cleanup task cancelled")
85
+
86
+ async def _cleanup_dead_connections(self):
87
+ """Remove dead/closed WebSocket connections"""
88
+ dead_connections = set()
89
+
90
+ for connection in self.active_connections:
91
+ if not connection.is_alive:
92
+ dead_connections.add(connection)
93
+ continue
94
+
95
+ # Try to ping the connection
96
+ try:
97
+ await connection.send_ping()
98
+ except Exception:
99
+ connection.is_alive = False
100
+ dead_connections.add(connection)
101
+
102
+ # Remove dead connections
103
+ for connection in dead_connections:
104
+ self.disconnect(connection.websocket, log=False)
105
+
106
+ if dead_connections:
107
+ logger.info(f"🧹 Cleaned up {len(dead_connections)} dead WebSocket connections")
108
+
109
+ async def connect(self, websocket: WebSocket, client_id: Optional[str] = None):
110
+ """Accept new WebSocket connection"""
111
+ try:
112
+ await websocket.accept()
113
+
114
+ connection = WebSocketConnection(websocket, client_id)
115
+ self.active_connections.add(connection)
116
+ self.client_connections[connection.client_id] = connection
117
+
118
+ # Update statistics
119
+ self.stats["total_connections"] += 1
120
+ self.stats["current_connections"] = len(self.active_connections)
121
+
122
+ logger.info(f"✅ WebSocket connected: {connection.client_id} (Total: {len(self.active_connections)})\"")
123
+
124
+ # Send welcome message with connection info
125
+ await connection.send_message({
126
+ "type": "connection_established",
127
+ "client_id": connection.client_id,
128
+ "server_time": datetime.utcnow().isoformat(),
129
+ "message": "Connected to SAAP WebSocket server"
130
+ })
131
+
132
+ # Send current stats
133
+ await connection.send_message({
134
+ "type": "stats_update",
135
+ "data": await self.get_connection_stats()
136
+ })
137
+
138
+ # Start cleanup task if not already running
139
+ await self.start_cleanup_task()
140
+
141
+ except Exception as e:
142
+ logger.error(f"❌ WebSocket connection failed: {e}")
143
+
144
+ def disconnect(self, websocket: WebSocket, log: bool = True):
145
+ """Remove WebSocket connection"""
146
+ connection_to_remove = None
147
+
148
+ for connection in self.active_connections:
149
+ if connection.websocket == websocket:
150
+ connection_to_remove = connection
151
+ break
152
+
153
+ if connection_to_remove:
154
+ self.active_connections.discard(connection_to_remove)
155
+ self.client_connections.pop(connection_to_remove.client_id, None)
156
+ connection_to_remove.is_alive = False
157
+
158
+ self.stats["current_connections"] = len(self.active_connections)
159
+
160
+ if log:
161
+ logger.info(f"🔌 WebSocket disconnected: {connection_to_remove.client_id} (Remaining: {len(self.active_connections)})")
162
+
163
+ async def broadcast_message(self, message: Dict[str, Any]):
164
+ """Broadcast message to all connected clients"""
165
+ if not self.active_connections:
166
+ return
167
+
168
+ try:
169
+ # Add timestamp and message type
170
+ broadcast_message = {
171
+ **message,
172
+ "timestamp": datetime.utcnow().isoformat(),
173
+ "broadcast": True
174
+ }
175
+
176
+ # Send to all connections
177
+ dead_connections = set()
178
+ successful_sends = 0
179
+
180
+ for connection in self.active_connections:
181
+ try:
182
+ await connection.send_message(broadcast_message)
183
+ successful_sends += 1
184
+ except Exception as e:
185
+ logger.warning(f"⚠️ Broadcast failed for {connection.client_id}: {e}")
186
+ dead_connections.add(connection)
187
+
188
+ # Remove dead connections
189
+ for connection in dead_connections:
190
+ self.disconnect(connection.websocket, log=False)
191
+
192
+ # Update statistics
193
+ self.stats["messages_sent"] += successful_sends
194
+ self.stats["broadcasts_sent"] += 1
195
+
196
+ # Add to message history
197
+ self._add_to_history(broadcast_message)
198
+
199
+ logger.debug(f"📢 Broadcast sent to {successful_sends} clients: {message.get('type', 'unknown')}")
200
+
201
+ except Exception as e:
202
+ logger.error(f"❌ Broadcast failed: {e}")
203
+
204
+ async def send_to_client(self, client_id: str, message: Dict[str, Any]):
205
+ """Send message to specific client"""
206
+ connection = self.client_connections.get(client_id)
207
+ if not connection:
208
+ logger.warning(f"⚠️ Client not found: {client_id}")
209
+ return False
210
+
211
+ try:
212
+ message_with_timestamp = {
213
+ **message,
214
+ "timestamp": datetime.utcnow().isoformat(),
215
+ "targeted": True,
216
+ "client_id": client_id
217
+ }
218
+
219
+ await connection.send_message(message_with_timestamp)
220
+ self.stats["messages_sent"] += 1
221
+
222
+ logger.debug(f"📤 Message sent to {client_id}: {message.get('type', 'unknown')}")
223
+ return True
224
+
225
+ except Exception as e:
226
+ logger.error(f"❌ Failed to send to {client_id}: {e}")
227
+ self.disconnect(connection.websocket)
228
+ return False
229
+
230
+ def _add_to_history(self, message: Dict[str, Any]):
231
+ """Add message to broadcast history"""
232
+ self.message_history.append(message)
233
+
234
+ # Trim history if too long
235
+ if len(self.message_history) > self.max_history_size:
236
+ self.message_history = self.message_history[-self.max_history_size:]
237
+
238
+ async def get_connection_stats(self) -> Dict[str, Any]:
239
+ """Get WebSocket connection statistics"""
240
+ return {
241
+ **self.stats,
242
+ "current_connections": len(self.active_connections),
243
+ "client_ids": [conn.client_id for conn in self.active_connections],
244
+ "message_history_size": len(self.message_history),
245
+ "server_time": datetime.utcnow().isoformat()
246
+ }
247
+
248
+ # SAAP-specific broadcast methods
249
+
250
+ async def broadcast_agent_update(self, agent: SaapAgent):
251
+ """Broadcast agent status update to all clients"""
252
+ # Handle both Enum and string status/type values
253
+ status_value = agent.status.value if hasattr(agent.status, 'value') else str(agent.status)
254
+ type_value = agent.type.value if hasattr(agent.type, 'value') else str(agent.type)
255
+
256
+ # Handle optional metrics attribute
257
+ last_active = None
258
+ if hasattr(agent, 'metrics') and agent.metrics:
259
+ if hasattr(agent.metrics, 'last_active') and agent.metrics.last_active:
260
+ last_active = agent.metrics.last_active.isoformat()
261
+
262
+ await self.broadcast_message({
263
+ "type": "agent_update",
264
+ "data": {
265
+ "agent_id": agent.id,
266
+ "name": agent.name,
267
+ "status": status_value,
268
+ "type": type_value,
269
+ "last_active": last_active,
270
+ "capabilities": agent.capabilities
271
+ }
272
+ })
273
+
274
+ async def broadcast_agent_deleted(self, agent_id: str):
275
+ """Broadcast agent deletion to all clients"""
276
+ await self.broadcast_message({
277
+ "type": "agent_deleted",
278
+ "data": {
279
+ "agent_id": agent_id
280
+ }
281
+ })
282
+
283
+ async def broadcast_message_update(self, message_data: Dict[str, Any]):
284
+ """Broadcast new agent message/response to all clients"""
285
+ await self.broadcast_message({
286
+ "type": "agent_message",
287
+ "data": message_data
288
+ })
289
+
290
+ async def broadcast_system_status(self, status_data: Dict[str, Any]):
291
+ """Broadcast system status update"""
292
+ await self.broadcast_message({
293
+ "type": "system_status",
294
+ "data": status_data
295
+ })
296
+
297
+ async def broadcast_error(self, error_message: str, error_code: Optional[str] = None):
298
+ """Broadcast system error to all clients"""
299
+ await self.broadcast_message({
300
+ "type": "system_error",
301
+ "data": {
302
+ "message": error_message,
303
+ "code": error_code,
304
+ "severity": "error"
305
+ }
306
+ })
307
+
308
+ async def shutdown(self):
309
+ """Gracefully shutdown WebSocket manager"""
310
+ try:
311
+ logger.info("🔧 Shutting down WebSocket manager...")
312
+
313
+ # Cancel cleanup task
314
+ if self._cleanup_task:
315
+ self._cleanup_task.cancel()
316
+ try:
317
+ await self._cleanup_task
318
+ except asyncio.CancelledError:
319
+ pass
320
+
321
+ # Send shutdown notification to all clients
322
+ await self.broadcast_message({
323
+ "type": "server_shutdown",
324
+ "data": {
325
+ "message": "SAAP server is shutting down",
326
+ "timestamp": datetime.utcnow().isoformat()
327
+ }
328
+ })
329
+
330
+ # Close all connections
331
+ for connection in list(self.active_connections):
332
+ try:
333
+ await connection.websocket.close()
334
+ except Exception:
335
+ pass
336
+ self.disconnect(connection.websocket, log=False)
337
+
338
+ logger.info("✅ WebSocket manager shutdown complete")
339
+
340
+ except Exception as e:
341
+ logger.error(f"❌ WebSocket shutdown error: {e}")
342
+
343
+ if __name__ == "__main__":
344
+ async def test_websocket_manager():
345
+ """Test WebSocket manager functionality"""
346
+ manager = WebSocketManager()
347
+
348
+ # Simulate stats
349
+ print("📊 WebSocket Manager Stats:")
350
+ stats = await manager.get_connection_stats()
351
+ print(json.dumps(stats, indent=2))
352
+
353
+ # Test broadcast (no clients, but should not error)
354
+ await manager.broadcast_message({
355
+ "type": "test",
356
+ "message": "Test broadcast"
357
+ })
358
+
359
+ print("✅ WebSocket manager test completed")
360
+
361
+ asyncio.run(test_websocket_manager())
frontend/dist/assets/icons-l0sNRNKZ.js DELETED
@@ -1,2 +0,0 @@
1
-
2
- //# sourceMappingURL=icons-l0sNRNKZ.js.map
 
 
 
frontend/dist/assets/icons-l0sNRNKZ.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"icons-l0sNRNKZ.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
 
 
frontend/dist/assets/index-C4sh0Su3.css DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-RrLk96-w.js DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-RrLk96-w.js.map DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/vendor-Ctwcxhgt.js DELETED
@@ -1,26 +0,0 @@
1
- /**
2
- * @vue/shared v3.5.22
3
- * (c) 2018-present Yuxi (Evan) You and Vue contributors
4
- * @license MIT
5
- **/function Ls(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const ie={},Lt=[],qe=()=>{},fo=()=>!1,kn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Ns=e=>e.startsWith("onUpdate:"),de=Object.assign,Fs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},qi=Object.prototype.hasOwnProperty,ee=(e,t)=>qi.call(e,t),D=Array.isArray,Nt=e=>pn(e)==="[object Map]",kt=e=>pn(e)==="[object Set]",or=e=>pn(e)==="[object Date]",K=e=>typeof e=="function",ue=e=>typeof e=="string",Ge=e=>typeof e=="symbol",ne=e=>e!==null&&typeof e=="object",uo=e=>(ne(e)||K(e))&&K(e.then)&&K(e.catch),ao=Object.prototype.toString,pn=e=>ao.call(e),Gi=e=>pn(e).slice(8,-1),ho=e=>pn(e)==="[object Object]",js=e=>ue(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,qt=Ls(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Vn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},zi=/-\w/g,Ne=Vn(e=>e.replace(zi,t=>t.slice(1).toUpperCase())),Ji=/\B([A-Z])/g,_t=Vn(e=>e.replace(Ji,"-$1").toLowerCase()),Bn=Vn(e=>e.charAt(0).toUpperCase()+e.slice(1)),ns=Vn(e=>e?`on${Bn(e)}`:""),pt=(e,t)=>!Object.is(e,t),wn=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},po=(e,t,n,s=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Tn=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Qi=e=>{const t=ue(e)?Number(e):NaN;return isNaN(t)?e:t};let ir;const Kn=()=>ir||(ir=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Ds(e){if(D(e)){const t={};for(let n=0;n<e.length;n++){const s=e[n],r=ue(s)?el(s):Ds(s);if(r)for(const o in r)t[o]=r[o]}return t}else if(ue(e)||ne(e))return e}const Yi=/;(?![^(]*\))/g,Xi=/:([^]+)/,Zi=/\/\*[^]*?\*\//g;function el(e){const t={};return e.replace(Zi,"").split(Yi).forEach(n=>{if(n){const s=n.split(Xi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function $s(e){let t="";if(ue(e))t=e;else if(D(e))for(let n=0;n<e.length;n++){const s=$s(e[n]);s&&(t+=s+" ")}else if(ne(e))for(const n in e)e[n]&&(t+=n+" ");return t.trim()}const tl="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",nl=Ls(tl);function go(e){return!!e||e===""}function sl(e,t){if(e.length!==t.length)return!1;let n=!0;for(let s=0;n&&s<e.length;s++)n=gn(e[s],t[s]);return n}function gn(e,t){if(e===t)return!0;let n=or(e),s=or(t);if(n||s)return n&&s?e.getTime()===t.getTime():!1;if(n=Ge(e),s=Ge(t),n||s)return e===t;if(n=D(e),s=D(t),n||s)return n&&s?sl(e,t):!1;if(n=ne(e),s=ne(t),n||s){if(!n||!s)return!1;const r=Object.keys(e).length,o=Object.keys(t).length;if(r!==o)return!1;for(const i in e){const l=e.hasOwnProperty(i),c=t.hasOwnProperty(i);if(l&&!c||!l&&c||!gn(e[i],t[i]))return!1}}return String(e)===String(t)}function Hs(e,t){return e.findIndex(n=>gn(n,t))}const mo=e=>!!(e&&e.__v_isRef===!0),rl=e=>ue(e)?e:e==null?"":D(e)||ne(e)&&(e.toString===ao||!K(e.toString))?mo(e)?rl(e.value):JSON.stringify(e,yo,2):String(e),yo=(e,t)=>mo(t)?yo(e,t.value):Nt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],o)=>(n[ss(s,o)+" =>"]=r,n),{})}:kt(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>ss(n))}:Ge(t)?ss(t):ne(t)&&!D(t)&&!ho(t)?String(t):t,ss=(e,t="")=>{var n;return Ge(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/**
6
- * @vue/reactivity v3.5.22
7
- * (c) 2018-present Yuxi (Evan) You and Vue contributors
8
- * @license MIT
9
- **/let pe;class _o{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=pe,!t&&pe&&(this.index=(pe.scopes||(pe.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t<n;t++)this.scopes[t].pause();for(t=0,n=this.effects.length;t<n;t++)this.effects[t].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t<n;t++)this.scopes[t].resume();for(t=0,n=this.effects.length;t<n;t++)this.effects[t].resume()}}run(t){if(this._active){const n=pe;try{return pe=this,t()}finally{pe=n}}}on(){++this._on===1&&(this.prevScope=pe,pe=this)}off(){this._on>0&&--this._on===0&&(pe=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n<s;n++)this.effects[n].stop();for(this.effects.length=0,n=0,s=this.cleanups.length;n<s;n++)this.cleanups[n]();if(this.cleanups.length=0,this.scopes){for(n=0,s=this.scopes.length;n<s;n++)this.scopes[n].stop(!0);this.scopes.length=0}if(!this.detached&&this.parent&&!t){const r=this.parent.scopes.pop();r&&r!==this&&(this.parent.scopes[this.index]=r,r.index=this.index)}this.parent=void 0}}}function vo(e){return new _o(e)}function bo(){return pe}function ol(e,t=!1){pe&&pe.cleanups.push(e)}let ce;const rs=new WeakSet;class So{constructor(t){this.fn=t,this.deps=void 0,this.depsTail=void 0,this.flags=5,this.next=void 0,this.cleanup=void 0,this.scheduler=void 0,pe&&pe.active&&pe.effects.push(this)}pause(){this.flags|=64}resume(){this.flags&64&&(this.flags&=-65,rs.has(this)&&(rs.delete(this),this.trigger()))}notify(){this.flags&2&&!(this.flags&32)||this.flags&8||Eo(this)}run(){if(!(this.flags&1))return this.fn();this.flags|=2,lr(this),wo(this);const t=ce,n=Fe;ce=this,Fe=!0;try{return this.fn()}finally{Co(this),ce=t,Fe=n,this.flags&=-3}}stop(){if(this.flags&1){for(let t=this.deps;t;t=t.nextDep)Bs(t);this.deps=this.depsTail=void 0,lr(this),this.onStop&&this.onStop(),this.flags&=-2}}trigger(){this.flags&64?rs.add(this):this.scheduler?this.scheduler():this.runIfDirty()}runIfDirty(){ys(this)&&this.run()}get dirty(){return ys(this)}}let xo=0,Gt,zt;function Eo(e,t=!1){if(e.flags|=8,t){e.next=zt,zt=e;return}e.next=Gt,Gt=e}function ks(){xo++}function Vs(){if(--xo>0)return;if(zt){let t=zt;for(zt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Gt;){let t=Gt;for(Gt=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function wo(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Co(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),Bs(s),il(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function ys(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Ro(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Ro(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===sn)||(e.globalVersion=sn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!ys(e))))return;e.flags|=2;const t=e.dep,n=ce,s=Fe;ce=e,Fe=!0;try{wo(e);const r=e.fn(e._value);(t.version===0||pt(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{ce=n,Fe=s,Co(e),e.flags&=-3}}function Bs(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let o=n.computed.deps;o;o=o.nextDep)Bs(o,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function il(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Fe=!0;const Ao=[];function st(){Ao.push(Fe),Fe=!1}function rt(){const e=Ao.pop();Fe=e===void 0?!0:e}function lr(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=ce;ce=void 0;try{t()}finally{ce=n}}}let sn=0;class ll{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Ks{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!ce||!Fe||ce===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==ce)n=this.activeLink=new ll(ce,this),ce.deps?(n.prevDep=ce.depsTail,ce.depsTail.nextDep=n,ce.depsTail=n):ce.deps=ce.depsTail=n,Po(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=ce.depsTail,n.nextDep=void 0,ce.depsTail.nextDep=n,ce.depsTail=n,ce.deps===n&&(ce.deps=s)}return n}trigger(t){this.version++,sn++,this.notify(t)}notify(t){ks();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Vs()}}}function Po(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Po(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const On=new WeakMap,Ct=Symbol(""),_s=Symbol(""),rn=Symbol("");function ge(e,t,n){if(Fe&&ce){let s=On.get(e);s||On.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new Ks),r.map=s,r.key=n),r.track()}}function et(e,t,n,s,r,o){const i=On.get(e);if(!i){sn++;return}const l=c=>{c&&c.trigger()};if(ks(),t==="clear")i.forEach(l);else{const c=D(e),a=c&&js(n);if(c&&n==="length"){const f=Number(s);i.forEach((d,p)=>{(p==="length"||p===rn||!Ge(p)&&p>=f)&&l(d)})}else switch((n!==void 0||i.has(void 0))&&l(i.get(n)),a&&l(i.get(rn)),t){case"add":c?a&&l(i.get("length")):(l(i.get(Ct)),Nt(e)&&l(i.get(_s)));break;case"delete":c||(l(i.get(Ct)),Nt(e)&&l(i.get(_s)));break;case"set":Nt(e)&&l(i.get(Ct));break}}Vs()}function cl(e,t){const n=On.get(e);return n&&n.get(t)}function Tt(e){const t=Y(e);return t===e?t:(ge(t,"iterate",rn),Ie(e)?t:t.map(he))}function Un(e){return ge(e=Y(e),"iterate",rn),e}const fl={__proto__:null,[Symbol.iterator](){return os(this,Symbol.iterator,he)},concat(...e){return Tt(this).concat(...e.map(t=>D(t)?Tt(t):t))},entries(){return os(this,"entries",e=>(e[1]=he(e[1]),e))},every(e,t){return Je(this,"every",e,t,void 0,arguments)},filter(e,t){return Je(this,"filter",e,t,n=>n.map(he),arguments)},find(e,t){return Je(this,"find",e,t,he,arguments)},findIndex(e,t){return Je(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Je(this,"findLast",e,t,he,arguments)},findLastIndex(e,t){return Je(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Je(this,"forEach",e,t,void 0,arguments)},includes(...e){return is(this,"includes",e)},indexOf(...e){return is(this,"indexOf",e)},join(e){return Tt(this).join(e)},lastIndexOf(...e){return is(this,"lastIndexOf",e)},map(e,t){return Je(this,"map",e,t,void 0,arguments)},pop(){return Bt(this,"pop")},push(...e){return Bt(this,"push",e)},reduce(e,...t){return cr(this,"reduce",e,t)},reduceRight(e,...t){return cr(this,"reduceRight",e,t)},shift(){return Bt(this,"shift")},some(e,t){return Je(this,"some",e,t,void 0,arguments)},splice(...e){return Bt(this,"splice",e)},toReversed(){return Tt(this).toReversed()},toSorted(e){return Tt(this).toSorted(e)},toSpliced(...e){return Tt(this).toSpliced(...e)},unshift(...e){return Bt(this,"unshift",e)},values(){return os(this,"values",he)}};function os(e,t,n){const s=Un(e),r=s[t]();return s!==e&&!Ie(e)&&(r._next=r.next,r.next=()=>{const o=r._next();return o.done||(o.value=n(o.value)),o}),r}const ul=Array.prototype;function Je(e,t,n,s,r,o){const i=Un(e),l=i!==e&&!Ie(e),c=i[t];if(c!==ul[t]){const d=c.apply(e,o);return l?he(d):d}let a=n;i!==e&&(l?a=function(d,p){return n.call(this,he(d),p,e)}:n.length>2&&(a=function(d,p){return n.call(this,d,p,e)}));const f=c.call(i,a,s);return l&&r?r(f):f}function cr(e,t,n,s){const r=Un(e);let o=n;return r!==e&&(Ie(e)?n.length>3&&(o=function(i,l,c){return n.call(this,i,l,c,e)}):o=function(i,l,c){return n.call(this,i,he(l),c,e)}),r[t](o,...s)}function is(e,t,n){const s=Y(e);ge(s,"iterate",rn);const r=s[t](...n);return(r===-1||r===!1)&&qs(n[0])?(n[0]=Y(n[0]),s[t](...n)):r}function Bt(e,t,n=[]){st(),ks();const s=Y(e)[t].apply(e,n);return Vs(),rt(),s}const al=Ls("__proto__,__v_isRef,__isVue"),To=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Ge));function dl(e){Ge(e)||(e=String(e));const t=Y(this);return ge(t,"has",e),t.hasOwnProperty(e)}class Oo{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,o=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return o;if(n==="__v_raw")return s===(r?o?xl:No:o?Lo:Io).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const i=D(t);if(!r){let c;if(i&&(c=fl[n]))return c;if(n==="hasOwnProperty")return dl}const l=Reflect.get(t,n,fe(t)?t:s);if((Ge(n)?To.has(n):al(n))||(r||ge(t,"get",n),o))return l;if(fe(l)){const c=i&&js(n)?l:l.value;return r&&ne(c)?bs(c):c}return ne(l)?r?bs(l):mn(l):l}}class Mo extends Oo{constructor(t=!1){super(!1,t)}set(t,n,s,r){let o=t[n];if(!this._isShallow){const c=mt(o);if(!Ie(s)&&!mt(s)&&(o=Y(o),s=Y(s)),!D(t)&&fe(o)&&!fe(s))return c||(o.value=s),!0}const i=D(t)&&js(n)?Number(n)<t.length:ee(t,n),l=Reflect.set(t,n,s,fe(t)?t:r);return t===Y(r)&&(i?pt(s,o)&&et(t,"set",n,s):et(t,"add",n,s)),l}deleteProperty(t,n){const s=ee(t,n);t[n];const r=Reflect.deleteProperty(t,n);return r&&s&&et(t,"delete",n,void 0),r}has(t,n){const s=Reflect.has(t,n);return(!Ge(n)||!To.has(n))&&ge(t,"has",n),s}ownKeys(t){return ge(t,"iterate",D(t)?"length":Ct),Reflect.ownKeys(t)}}class hl extends Oo{constructor(t=!1){super(!0,t)}set(t,n){return!0}deleteProperty(t,n){return!0}}const pl=new Mo,gl=new hl,ml=new Mo(!0);const vs=e=>e,bn=e=>Reflect.getPrototypeOf(e);function yl(e,t,n){return function(...s){const r=this.__v_raw,o=Y(r),i=Nt(o),l=e==="entries"||e===Symbol.iterator&&i,c=e==="keys"&&i,a=r[e](...s),f=n?vs:t?Mn:he;return!t&&ge(o,"iterate",c?_s:Ct),{next(){const{value:d,done:p}=a.next();return p?{value:d,done:p}:{value:l?[f(d[0]),f(d[1])]:f(d),done:p}},[Symbol.iterator](){return this}}}}function Sn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function _l(e,t){const n={get(r){const o=this.__v_raw,i=Y(o),l=Y(r);e||(pt(r,l)&&ge(i,"get",r),ge(i,"get",l));const{has:c}=bn(i),a=t?vs:e?Mn:he;if(c.call(i,r))return a(o.get(r));if(c.call(i,l))return a(o.get(l));o!==i&&o.get(r)},get size(){const r=this.__v_raw;return!e&&ge(Y(r),"iterate",Ct),r.size},has(r){const o=this.__v_raw,i=Y(o),l=Y(r);return e||(pt(r,l)&&ge(i,"has",r),ge(i,"has",l)),r===l?o.has(r):o.has(r)||o.has(l)},forEach(r,o){const i=this,l=i.__v_raw,c=Y(l),a=t?vs:e?Mn:he;return!e&&ge(c,"iterate",Ct),l.forEach((f,d)=>r.call(o,a(f),a(d),i))}};return de(n,e?{add:Sn("add"),set:Sn("set"),delete:Sn("delete"),clear:Sn("clear")}:{add(r){!t&&!Ie(r)&&!mt(r)&&(r=Y(r));const o=Y(this);return bn(o).has.call(o,r)||(o.add(r),et(o,"add",r,r)),this},set(r,o){!t&&!Ie(o)&&!mt(o)&&(o=Y(o));const i=Y(this),{has:l,get:c}=bn(i);let a=l.call(i,r);a||(r=Y(r),a=l.call(i,r));const f=c.call(i,r);return i.set(r,o),a?pt(o,f)&&et(i,"set",r,o):et(i,"add",r,o),this},delete(r){const o=Y(this),{has:i,get:l}=bn(o);let c=i.call(o,r);c||(r=Y(r),c=i.call(o,r)),l&&l.call(o,r);const a=o.delete(r);return c&&et(o,"delete",r,void 0),a},clear(){const r=Y(this),o=r.size!==0,i=r.clear();return o&&et(r,"clear",void 0,void 0),i}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=yl(r,e,t)}),n}function Us(e,t){const n=_l(e,t);return(s,r,o)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(ee(n,r)&&r in s?n:s,r,o)}const vl={get:Us(!1,!1)},bl={get:Us(!1,!0)},Sl={get:Us(!0,!1)};const Io=new WeakMap,Lo=new WeakMap,No=new WeakMap,xl=new WeakMap;function El(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function wl(e){return e.__v_skip||!Object.isExtensible(e)?0:El(Gi(e))}function mn(e){return mt(e)?e:Ws(e,!1,pl,vl,Io)}function Fo(e){return Ws(e,!1,ml,bl,Lo)}function bs(e){return Ws(e,!0,gl,Sl,No)}function Ws(e,t,n,s,r){if(!ne(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const o=wl(e);if(o===0)return e;const i=r.get(e);if(i)return i;const l=new Proxy(e,o===2?s:n);return r.set(e,l),l}function gt(e){return mt(e)?gt(e.__v_raw):!!(e&&e.__v_isReactive)}function mt(e){return!!(e&&e.__v_isReadonly)}function Ie(e){return!!(e&&e.__v_isShallow)}function qs(e){return e?!!e.__v_raw:!1}function Y(e){const t=e&&e.__v_raw;return t?Y(t):e}function Gs(e){return!ee(e,"__v_skip")&&Object.isExtensible(e)&&po(e,"__v_skip",!0),e}const he=e=>ne(e)?mn(e):e,Mn=e=>ne(e)?bs(e):e;function fe(e){return e?e.__v_isRef===!0:!1}function Wn(e){return jo(e,!1)}function Cl(e){return jo(e,!0)}function jo(e,t){return fe(e)?e:new Rl(e,t)}class Rl{constructor(t,n){this.dep=new Ks,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:Y(t),this._value=n?t:he(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Ie(t)||mt(t);t=s?t:Y(t),pt(t,n)&&(this._rawValue=t,this._value=s?t:he(t),this.dep.trigger())}}function Ft(e){return fe(e)?e.value:e}const Al={get:(e,t,n)=>t==="__v_raw"?e:Ft(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return fe(r)&&!fe(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Do(e){return gt(e)?e:new Proxy(e,Al)}function Pl(e){const t=D(e)?new Array(e.length):{};for(const n in e)t[n]=$o(e,n);return t}class Tl{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0,this._value=void 0}get value(){const t=this._object[this._key];return this._value=t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return cl(Y(this._object),this._key)}}class Ol{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Mu(e,t,n){return fe(e)?e:K(e)?new Ol(e):ne(e)&&arguments.length>1?$o(e,t,n):Wn(e)}function $o(e,t,n){const s=e[t];return fe(s)?s:new Tl(e,t,n)}class Ml{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new Ks(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=sn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&ce!==this)return Eo(this,!0),!0}get value(){const t=this.dep.track();return Ro(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Il(e,t,n=!1){let s,r;return K(e)?s=e:(s=e.get,r=e.set),new Ml(s,r,n)}const xn={},In=new WeakMap;let Et;function Ll(e,t=!1,n=Et){if(n){let s=In.get(n);s||In.set(n,s=[]),s.push(e)}}function Nl(e,t,n=ie){const{immediate:s,deep:r,once:o,scheduler:i,augmentJob:l,call:c}=n,a=L=>r?L:Ie(L)||r===!1||r===0?tt(L,1):tt(L);let f,d,p,m,x=!1,A=!1;if(fe(e)?(d=()=>e.value,x=Ie(e)):gt(e)?(d=()=>a(e),x=!0):D(e)?(A=!0,x=e.some(L=>gt(L)||Ie(L)),d=()=>e.map(L=>{if(fe(L))return L.value;if(gt(L))return a(L);if(K(L))return c?c(L,2):L()})):K(e)?t?d=c?()=>c(e,2):e:d=()=>{if(p){st();try{p()}finally{rt()}}const L=Et;Et=f;try{return c?c(e,3,[m]):e(m)}finally{Et=L}}:d=qe,t&&r){const L=d,V=r===!0?1/0:r;d=()=>tt(L(),V)}const k=bo(),N=()=>{f.stop(),k&&k.active&&Fs(k.effects,f)};if(o&&t){const L=t;t=(...V)=>{L(...V),N()}}let I=A?new Array(e.length).fill(xn):xn;const F=L=>{if(!(!(f.flags&1)||!f.dirty&&!L))if(t){const V=f.run();if(r||x||(A?V.some((se,G)=>pt(se,I[G])):pt(V,I))){p&&p();const se=Et;Et=f;try{const G=[V,I===xn?void 0:A&&I[0]===xn?[]:I,m];I=V,c?c(t,3,G):t(...G)}finally{Et=se}}}else f.run()};return l&&l(F),f=new So(d),f.scheduler=i?()=>i(F,!1):F,m=L=>Ll(L,!1,f),p=f.onStop=()=>{const L=In.get(f);if(L){if(c)c(L,4);else for(const V of L)V();In.delete(f)}},t?s?F(!0):I=f.run():i?i(F.bind(null,!0),!0):f.run(),N.pause=f.pause.bind(f),N.resume=f.resume.bind(f),N.stop=N,N}function tt(e,t=1/0,n){if(t<=0||!ne(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,fe(e))tt(e.value,t,n);else if(D(e))for(let s=0;s<e.length;s++)tt(e[s],t,n);else if(kt(e)||Nt(e))e.forEach(s=>{tt(s,t,n)});else if(ho(e)){for(const s in e)tt(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&tt(e[s],t,n)}return e}/**
10
- * @vue/runtime-core v3.5.22
11
- * (c) 2018-present Yuxi (Evan) You and Vue contributors
12
- * @license MIT
13
- **/function yn(e,t,n,s){try{return s?e(...s):e()}catch(r){qn(r,t,n)}}function je(e,t,n,s){if(K(e)){const r=yn(e,t,n,s);return r&&uo(r)&&r.catch(o=>{qn(o,t,n)}),r}if(D(e)){const r=[];for(let o=0;o<e.length;o++)r.push(je(e[o],t,n,s));return r}}function qn(e,t,n,s=!0){const r=t?t.vnode:null,{errorHandler:o,throwUnhandledErrorInProduction:i}=t&&t.appContext.config||ie;if(t){let l=t.parent;const c=t.proxy,a=`https://vuejs.org/error-reference/#runtime-${n}`;for(;l;){const f=l.ec;if(f){for(let d=0;d<f.length;d++)if(f[d](e,c,a)===!1)return}l=l.parent}if(o){st(),yn(o,null,10,[e,c,a]),rt();return}}Fl(e,n,r,s,i)}function Fl(e,t,n,s=!0,r=!1){if(r)throw e;console.error(e)}const ve=[];let Ke=-1;const jt=[];let ut=null,Mt=0;const Ho=Promise.resolve();let Ln=null;function Gn(e){const t=Ln||Ho;return e?t.then(this?e.bind(this):e):t}function jl(e){let t=Ke+1,n=ve.length;for(;t<n;){const s=t+n>>>1,r=ve[s],o=on(r);o<e||o===e&&r.flags&2?t=s+1:n=s}return t}function zs(e){if(!(e.flags&1)){const t=on(e),n=ve[ve.length-1];!n||!(e.flags&2)&&t>=on(n)?ve.push(e):ve.splice(jl(t),0,e),e.flags|=1,ko()}}function ko(){Ln||(Ln=Ho.then(Bo))}function Dl(e){D(e)?jt.push(...e):ut&&e.id===-1?ut.splice(Mt+1,0,e):e.flags&1||(jt.push(e),e.flags|=1),ko()}function fr(e,t,n=Ke+1){for(;n<ve.length;n++){const s=ve[n];if(s&&s.flags&2){if(e&&s.id!==e.uid)continue;ve.splice(n,1),n--,s.flags&4&&(s.flags&=-2),s(),s.flags&4||(s.flags&=-2)}}}function Vo(e){if(jt.length){const t=[...new Set(jt)].sort((n,s)=>on(n)-on(s));if(jt.length=0,ut){ut.push(...t);return}for(ut=t,Mt=0;Mt<ut.length;Mt++){const n=ut[Mt];n.flags&4&&(n.flags&=-2),n.flags&8||n(),n.flags&=-2}ut=null,Mt=0}}const on=e=>e.id==null?e.flags&2?-1:1/0:e.id;function Bo(e){try{for(Ke=0;Ke<ve.length;Ke++){const t=ve[Ke];t&&!(t.flags&8)&&(t.flags&4&&(t.flags&=-2),yn(t,t.i,t.i?15:14),t.flags&4||(t.flags&=-2))}}finally{for(;Ke<ve.length;Ke++){const t=ve[Ke];t&&(t.flags&=-2)}Ke=-1,ve.length=0,Vo(),Ln=null,(ve.length||jt.length)&&Bo()}}let Ae=null,Ko=null;function Nn(e){const t=Ae;return Ae=e,Ko=e&&e.type.__scopeId||null,t}function $l(e,t=Ae,n){if(!t||e._n)return e;const s=(...r)=>{s._d&&Dn(-1);const o=Nn(t);let i;try{i=e(...r)}finally{Nn(o),s._d&&Dn(1)}return i};return s._n=!0,s._c=!0,s._d=!0,s}function Iu(e,t){if(Ae===null)return e;const n=Xn(Ae),s=e.dirs||(e.dirs=[]);for(let r=0;r<t.length;r++){let[o,i,l,c=ie]=t[r];o&&(K(o)&&(o={mounted:o,updated:o}),o.deep&&tt(i),s.push({dir:o,instance:n,value:i,oldValue:void 0,arg:l,modifiers:c}))}return e}function vt(e,t,n,s){const r=e.dirs,o=t&&t.dirs;for(let i=0;i<r.length;i++){const l=r[i];o&&(l.oldValue=o[i].value);let c=l.dir[s];c&&(st(),je(c,n,8,[e.el,l,e,t]),rt())}}const Hl=Symbol("_vte"),Uo=e=>e.__isTeleport,Ze=Symbol("_leaveCb"),En=Symbol("_enterCb");function kl(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Zo(()=>{e.isMounted=!0}),ei(()=>{e.isUnmounting=!0}),e}const Oe=[Function,Array],Wo={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Oe,onEnter:Oe,onAfterEnter:Oe,onEnterCancelled:Oe,onBeforeLeave:Oe,onLeave:Oe,onAfterLeave:Oe,onLeaveCancelled:Oe,onBeforeAppear:Oe,onAppear:Oe,onAfterAppear:Oe,onAppearCancelled:Oe},qo=e=>{const t=e.subTree;return t.component?qo(t.component):t},Vl={name:"BaseTransition",props:Wo,setup(e,{slots:t}){const n=Xs(),s=kl();return()=>{const r=t.default&&Jo(t.default(),!0);if(!r||!r.length)return;const o=Go(r),i=Y(e),{mode:l}=i;if(s.isLeaving)return ls(o);const c=ur(o);if(!c)return ls(o);let a=Ss(c,i,s,n,d=>a=d);c.type!==be&&ln(c,a);let f=n.subTree&&ur(n.subTree);if(f&&f.type!==be&&!wt(f,c)&&qo(n).type!==be){let d=Ss(f,i,s,n);if(ln(f,d),l==="out-in"&&c.type!==be)return s.isLeaving=!0,d.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete d.afterLeave,f=void 0},ls(o);l==="in-out"&&c.type!==be?d.delayLeave=(p,m,x)=>{const A=zo(s,f);A[String(f.key)]=f,p[Ze]=()=>{m(),p[Ze]=void 0,delete a.delayedLeave,f=void 0},a.delayedLeave=()=>{x(),delete a.delayedLeave,f=void 0}}:f=void 0}else f&&(f=void 0);return o}}};function Go(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==be){t=n;break}}return t}const Bl=Vl;function zo(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function Ss(e,t,n,s,r){const{appear:o,mode:i,persisted:l=!1,onBeforeEnter:c,onEnter:a,onAfterEnter:f,onEnterCancelled:d,onBeforeLeave:p,onLeave:m,onAfterLeave:x,onLeaveCancelled:A,onBeforeAppear:k,onAppear:N,onAfterAppear:I,onAppearCancelled:F}=t,L=String(e.key),V=zo(n,e),se=(T,U)=>{T&&je(T,s,9,U)},G=(T,U)=>{const Q=U[1];se(T,U),D(T)?T.every(P=>P.length<=1)&&Q():T.length<=1&&Q()},W={mode:i,persisted:l,beforeEnter(T){let U=c;if(!n.isMounted)if(o)U=k||c;else return;T[Ze]&&T[Ze](!0);const Q=V[L];Q&&wt(e,Q)&&Q.el[Ze]&&Q.el[Ze](),se(U,[T])},enter(T){let U=a,Q=f,P=d;if(!n.isMounted)if(o)U=N||a,Q=I||f,P=F||d;else return;let z=!1;const ae=T[En]=ye=>{z||(z=!0,ye?se(P,[T]):se(Q,[T]),W.delayedLeave&&W.delayedLeave(),T[En]=void 0)};U?G(U,[T,ae]):ae()},leave(T,U){const Q=String(e.key);if(T[En]&&T[En](!0),n.isUnmounting)return U();se(p,[T]);let P=!1;const z=T[Ze]=ae=>{P||(P=!0,U(),ae?se(A,[T]):se(x,[T]),T[Ze]=void 0,V[Q]===e&&delete V[Q])};V[Q]=e,m?G(m,[T,z]):z()},clone(T){const U=Ss(T,t,n,s,r);return r&&r(U),U}};return W}function ls(e){if(zn(e))return e=yt(e),e.children=null,e}function ur(e){if(!zn(e))return Uo(e.type)&&e.children?Go(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&K(n.default))return n.default()}}function ln(e,t){e.shapeFlag&6&&e.component?(e.transition=t,ln(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Jo(e,t=!1,n){let s=[],r=0;for(let o=0;o<e.length;o++){let i=e[o];const l=n==null?i.key:String(n)+String(i.key!=null?i.key:o);i.type===Ue?(i.patchFlag&128&&r++,s=s.concat(Jo(i.children,t,l))):(t||i.type!==be)&&s.push(l!=null?yt(i,{key:l}):i)}if(r>1)for(let o=0;o<s.length;o++)s[o].patchFlag=-2;return s}function Qo(e,t){return K(e)?de({name:e.name},t,{setup:e}):e}function Yo(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}const Fn=new WeakMap;function Jt(e,t,n,s,r=!1){if(D(e)){e.forEach((x,A)=>Jt(x,t&&(D(t)?t[A]:t),n,s,r));return}if(Qt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Jt(e,t,n,s.component.subTree);return}const o=s.shapeFlag&4?Xn(s.component):s.el,i=r?null:o,{i:l,r:c}=e,a=t&&t.r,f=l.refs===ie?l.refs={}:l.refs,d=l.setupState,p=Y(d),m=d===ie?fo:x=>ee(p,x);if(a!=null&&a!==c){if(ar(t),ue(a))f[a]=null,m(a)&&(d[a]=null);else if(fe(a)){a.value=null;const x=t;x.k&&(f[x.k]=null)}}if(K(c))yn(c,l,12,[i,f]);else{const x=ue(c),A=fe(c);if(x||A){const k=()=>{if(e.f){const N=x?m(c)?d[c]:f[c]:c.value;if(r)D(N)&&Fs(N,o);else if(D(N))N.includes(o)||N.push(o);else if(x)f[c]=[o],m(c)&&(d[c]=f[c]);else{const I=[o];c.value=I,e.k&&(f[e.k]=I)}}else x?(f[c]=i,m(c)&&(d[c]=i)):A&&(c.value=i,e.k&&(f[e.k]=i))};if(i){const N=()=>{k(),Fn.delete(e)};N.id=-1,Fn.set(e,N),Re(N,n)}else ar(e),k()}}}function ar(e){const t=Fn.get(e);t&&(t.flags|=8,Fn.delete(e))}Kn().requestIdleCallback;Kn().cancelIdleCallback;const Qt=e=>!!e.type.__asyncLoader,zn=e=>e.type.__isKeepAlive;function Kl(e,t){Xo(e,"a",t)}function Ul(e,t){Xo(e,"da",t)}function Xo(e,t,n=me){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Jn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)zn(r.parent.vnode)&&Wl(s,t,n,r),r=r.parent}}function Wl(e,t,n,s){const r=Jn(t,e,s,!0);ti(()=>{Fs(s[t],r)},n)}function Jn(e,t,n=me,s=!1){if(n){const r=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...i)=>{st();const l=_n(n),c=je(t,n,e,i);return l(),rt(),c});return s?r.unshift(o):r.push(o),o}}const ot=e=>(t,n=me)=>{(!fn||e==="sp")&&Jn(e,(...s)=>t(...s),n)},ql=ot("bm"),Zo=ot("m"),Gl=ot("bu"),zl=ot("u"),ei=ot("bum"),ti=ot("um"),Jl=ot("sp"),Ql=ot("rtg"),Yl=ot("rtc");function Xl(e,t=me){Jn("ec",e,t)}const ni="components";function Lu(e,t){return ri(ni,e,!0,t)||e}const si=Symbol.for("v-ndc");function Nu(e){return ue(e)?ri(ni,e,!1)||e:e||si}function ri(e,t,n=!0,s=!1){const r=Ae||me;if(r){const o=r.type;{const l=Kc(o,!1);if(l&&(l===t||l===Ne(t)||l===Bn(Ne(t))))return o}const i=dr(r[e]||o[e],t)||dr(r.appContext[e],t);return!i&&s?o:i}}function dr(e,t){return e&&(e[t]||e[Ne(t)]||e[Bn(Ne(t))])}function Fu(e,t,n,s){let r;const o=n,i=D(e);if(i||ue(e)){const l=i&&gt(e);let c=!1,a=!1;l&&(c=!Ie(e),a=mt(e),e=Un(e)),r=new Array(e.length);for(let f=0,d=e.length;f<d;f++)r[f]=t(c?a?Mn(he(e[f])):he(e[f]):e[f],f,void 0,o)}else if(typeof e=="number"){r=new Array(e);for(let l=0;l<e;l++)r[l]=t(l+1,l,void 0,o)}else if(ne(e))if(e[Symbol.iterator])r=Array.from(e,(l,c)=>t(l,c,void 0,o));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,a=l.length;c<a;c++){const f=l[c];r[c]=t(e[f],f,c,o)}}else r=[];return r}const xs=e=>e?Ci(e)?Xn(e):xs(e.parent):null,Yt=de(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>xs(e.parent),$root:e=>xs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ii(e),$forceUpdate:e=>e.f||(e.f=()=>{zs(e.update)}),$nextTick:e=>e.n||(e.n=Gn.bind(e.proxy)),$watch:e=>bc.bind(e)}),cs=(e,t)=>e!==ie&&!e.__isScriptSetup&&ee(e,t),Zl={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:o,accessCache:i,type:l,appContext:c}=e;let a;if(t[0]!=="$"){const m=i[t];if(m!==void 0)switch(m){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return o[t]}else{if(cs(s,t))return i[t]=1,s[t];if(r!==ie&&ee(r,t))return i[t]=2,r[t];if((a=e.propsOptions[0])&&ee(a,t))return i[t]=3,o[t];if(n!==ie&&ee(n,t))return i[t]=4,n[t];Es&&(i[t]=0)}}const f=Yt[t];let d,p;if(f)return t==="$attrs"&&ge(e.attrs,"get",""),f(e);if((d=l.__cssModules)&&(d=d[t]))return d;if(n!==ie&&ee(n,t))return i[t]=4,n[t];if(p=c.config.globalProperties,ee(p,t))return p[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:o}=e;return cs(r,t)?(r[t]=n,!0):s!==ie&&ee(s,t)?(s[t]=n,!0):ee(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:o,type:i}},l){let c,a;return!!(n[l]||e!==ie&&l[0]!=="$"&&ee(e,l)||cs(t,l)||(c=o[0])&&ee(c,l)||ee(s,l)||ee(Yt,l)||ee(r.config.globalProperties,l)||(a=i.__cssModules)&&a[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:ee(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function hr(e){return D(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Es=!0;function ec(e){const t=ii(e),n=e.proxy,s=e.ctx;Es=!1,t.beforeCreate&&pr(t.beforeCreate,e,"bc");const{data:r,computed:o,methods:i,watch:l,provide:c,inject:a,created:f,beforeMount:d,mounted:p,beforeUpdate:m,updated:x,activated:A,deactivated:k,beforeDestroy:N,beforeUnmount:I,destroyed:F,unmounted:L,render:V,renderTracked:se,renderTriggered:G,errorCaptured:W,serverPrefetch:T,expose:U,inheritAttrs:Q,components:P,directives:z,filters:ae}=t;if(a&&tc(a,s,null),i)for(const q in i){const X=i[q];K(X)&&(s[q]=X.bind(n))}if(r){const q=r.call(n,n);ne(q)&&(e.data=mn(q))}if(Es=!0,o)for(const q in o){const X=o[q],ze=K(X)?X.bind(n,n):K(X.get)?X.get.bind(n,n):qe,it=!K(X)&&K(X.set)?X.set.bind(n):qe,$e=Me({get:ze,set:it});Object.defineProperty(s,q,{enumerable:!0,configurable:!0,get:()=>$e.value,set:Se=>$e.value=Se})}if(l)for(const q in l)oi(l[q],s,n,q);if(c){const q=K(c)?c.call(n):c;Reflect.ownKeys(q).forEach(X=>{Cn(X,q[X])})}f&&pr(f,e,"c");function re(q,X){D(X)?X.forEach(ze=>q(ze.bind(n))):X&&q(X.bind(n))}if(re(ql,d),re(Zo,p),re(Gl,m),re(zl,x),re(Kl,A),re(Ul,k),re(Xl,W),re(Yl,se),re(Ql,G),re(ei,I),re(ti,L),re(Jl,T),D(U))if(U.length){const q=e.exposed||(e.exposed={});U.forEach(X=>{Object.defineProperty(q,X,{get:()=>n[X],set:ze=>n[X]=ze,enumerable:!0})})}else e.exposed||(e.exposed={});V&&e.render===qe&&(e.render=V),Q!=null&&(e.inheritAttrs=Q),P&&(e.components=P),z&&(e.directives=z),T&&Yo(e)}function tc(e,t,n=qe){D(e)&&(e=ws(e));for(const s in e){const r=e[s];let o;ne(r)?"default"in r?o=Le(r.from||s,r.default,!0):o=Le(r.from||s):o=Le(r),fe(o)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>o.value,set:i=>o.value=i}):t[s]=o}}function pr(e,t,n){je(D(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function oi(e,t,n,s){let r=s.includes(".")?vi(n,s):()=>n[s];if(ue(e)){const o=t[e];K(o)&&Xt(r,o)}else if(K(e))Xt(r,e.bind(n));else if(ne(e))if(D(e))e.forEach(o=>oi(o,t,n,s));else{const o=K(e.handler)?e.handler.bind(n):t[e.handler];K(o)&&Xt(r,o,e)}}function ii(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,l=o.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(a=>jn(c,a,i,!0)),jn(c,t,i)),ne(t)&&o.set(t,c),c}function jn(e,t,n,s=!1){const{mixins:r,extends:o}=t;o&&jn(e,o,n,!0),r&&r.forEach(i=>jn(e,i,n,!0));for(const i in t)if(!(s&&i==="expose")){const l=nc[i]||n&&n[i];e[i]=l?l(e[i],t[i]):t[i]}return e}const nc={data:gr,props:mr,emits:mr,methods:Wt,computed:Wt,beforeCreate:_e,created:_e,beforeMount:_e,mounted:_e,beforeUpdate:_e,updated:_e,beforeDestroy:_e,beforeUnmount:_e,destroyed:_e,unmounted:_e,activated:_e,deactivated:_e,errorCaptured:_e,serverPrefetch:_e,components:Wt,directives:Wt,watch:rc,provide:gr,inject:sc};function gr(e,t){return t?e?function(){return de(K(e)?e.call(this,this):e,K(t)?t.call(this,this):t)}:t:e}function sc(e,t){return Wt(ws(e),ws(t))}function ws(e){if(D(e)){const t={};for(let n=0;n<e.length;n++)t[e[n]]=e[n];return t}return e}function _e(e,t){return e?[...new Set([].concat(e,t))]:t}function Wt(e,t){return e?de(Object.create(null),e,t):t}function mr(e,t){return e?D(e)&&D(t)?[...new Set([...e,...t])]:de(Object.create(null),hr(e),hr(t??{})):t}function rc(e,t){if(!e)return t;if(!t)return e;const n=de(Object.create(null),e);for(const s in t)n[s]=_e(e[s],t[s]);return n}function li(){return{app:null,config:{isNativeTag:fo,performance:!1,globalProperties:{},optionMergeStrategies:{},errorHandler:void 0,warnHandler:void 0,compilerOptions:{}},mixins:[],components:{},directives:{},provides:Object.create(null),optionsCache:new WeakMap,propsCache:new WeakMap,emitsCache:new WeakMap}}let oc=0;function ic(e,t){return function(s,r=null){K(s)||(s=de({},s)),r!=null&&!ne(r)&&(r=null);const o=li(),i=new WeakSet,l=[];let c=!1;const a=o.app={_uid:oc++,_component:s,_props:r,_container:null,_context:o,_instance:null,version:Wc,get config(){return o.config},set config(f){},use(f,...d){return i.has(f)||(f&&K(f.install)?(i.add(f),f.install(a,...d)):K(f)&&(i.add(f),f(a,...d))),a},mixin(f){return o.mixins.includes(f)||o.mixins.push(f),a},component(f,d){return d?(o.components[f]=d,a):o.components[f]},directive(f,d){return d?(o.directives[f]=d,a):o.directives[f]},mount(f,d,p){if(!c){const m=a._ceVNode||we(s,r);return m.appContext=o,p===!0?p="svg":p===!1&&(p=void 0),e(m,f,p),c=!0,a._container=f,f.__vue_app__=a,Xn(m.component)}},onUnmount(f){l.push(f)},unmount(){c&&(je(l,a._instance,16),e(null,a._container),delete a._container.__vue_app__)},provide(f,d){return o.provides[f]=d,a},runWithContext(f){const d=Rt;Rt=a;try{return f()}finally{Rt=d}}};return a}}let Rt=null;function Cn(e,t){if(me){let n=me.provides;const s=me.parent&&me.parent.provides;s===n&&(n=me.provides=Object.create(s)),n[e]=t}}function Le(e,t,n=!1){const s=Xs();if(s||Rt){let r=Rt?Rt._context.provides:s?s.parent==null||s.ce?s.vnode.appContext&&s.vnode.appContext.provides:s.parent.provides:void 0;if(r&&e in r)return r[e];if(arguments.length>1)return n&&K(t)?t.call(s&&s.proxy):t}}function lc(){return!!(Xs()||Rt)}const ci={},fi=()=>Object.create(ci),ui=e=>Object.getPrototypeOf(e)===ci;function cc(e,t,n,s=!1){const r={},o=fi();e.propsDefaults=Object.create(null),ai(e,t,r,o);for(const i in e.propsOptions[0])i in r||(r[i]=void 0);n?e.props=s?r:Fo(r):e.type.props?e.props=r:e.props=o,e.attrs=o}function fc(e,t,n,s){const{props:r,attrs:o,vnode:{patchFlag:i}}=e,l=Y(r),[c]=e.propsOptions;let a=!1;if((s||i>0)&&!(i&16)){if(i&8){const f=e.vnode.dynamicProps;for(let d=0;d<f.length;d++){let p=f[d];if(Qn(e.emitsOptions,p))continue;const m=t[p];if(c)if(ee(o,p))m!==o[p]&&(o[p]=m,a=!0);else{const x=Ne(p);r[x]=Cs(c,l,x,m,e,!1)}else m!==o[p]&&(o[p]=m,a=!0)}}}else{ai(e,t,r,o)&&(a=!0);let f;for(const d in l)(!t||!ee(t,d)&&((f=_t(d))===d||!ee(t,f)))&&(c?n&&(n[d]!==void 0||n[f]!==void 0)&&(r[d]=Cs(c,l,d,void 0,e,!0)):delete r[d]);if(o!==l)for(const d in o)(!t||!ee(t,d))&&(delete o[d],a=!0)}a&&et(e.attrs,"set","")}function ai(e,t,n,s){const[r,o]=e.propsOptions;let i=!1,l;if(t)for(let c in t){if(qt(c))continue;const a=t[c];let f;r&&ee(r,f=Ne(c))?!o||!o.includes(f)?n[f]=a:(l||(l={}))[f]=a:Qn(e.emitsOptions,c)||(!(c in s)||a!==s[c])&&(s[c]=a,i=!0)}if(o){const c=Y(n),a=l||ie;for(let f=0;f<o.length;f++){const d=o[f];n[d]=Cs(r,c,d,a[d],e,!ee(a,d))}}return i}function Cs(e,t,n,s,r,o){const i=e[n];if(i!=null){const l=ee(i,"default");if(l&&s===void 0){const c=i.default;if(i.type!==Function&&!i.skipFactory&&K(c)){const{propsDefaults:a}=r;if(n in a)s=a[n];else{const f=_n(r);s=a[n]=c.call(null,t),f()}}else s=c;r.ce&&r.ce._setProp(n,s)}i[0]&&(o&&!l?s=!1:i[1]&&(s===""||s===_t(n))&&(s=!0))}return s}const uc=new WeakMap;function di(e,t,n=!1){const s=n?uc:t.propsCache,r=s.get(e);if(r)return r;const o=e.props,i={},l=[];let c=!1;if(!K(e)){const f=d=>{c=!0;const[p,m]=di(d,t,!0);de(i,p),m&&l.push(...m)};!n&&t.mixins.length&&t.mixins.forEach(f),e.extends&&f(e.extends),e.mixins&&e.mixins.forEach(f)}if(!o&&!c)return ne(e)&&s.set(e,Lt),Lt;if(D(o))for(let f=0;f<o.length;f++){const d=Ne(o[f]);yr(d)&&(i[d]=ie)}else if(o)for(const f in o){const d=Ne(f);if(yr(d)){const p=o[f],m=i[d]=D(p)||K(p)?{type:p}:de({},p),x=m.type;let A=!1,k=!0;if(D(x))for(let N=0;N<x.length;++N){const I=x[N],F=K(I)&&I.name;if(F==="Boolean"){A=!0;break}else F==="String"&&(k=!1)}else A=K(x)&&x.name==="Boolean";m[0]=A,m[1]=k,(A||ee(m,"default"))&&l.push(d)}}const a=[i,l];return ne(e)&&s.set(e,a),a}function yr(e){return e[0]!=="$"&&!qt(e)}const Js=e=>e==="_"||e==="_ctx"||e==="$stable",Qs=e=>D(e)?e.map(We):[We(e)],ac=(e,t,n)=>{if(t._n)return t;const s=$l((...r)=>Qs(t(...r)),n);return s._c=!1,s},hi=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Js(r))continue;const o=e[r];if(K(o))t[r]=ac(r,o,s);else if(o!=null){const i=Qs(o);t[r]=()=>i}}},pi=(e,t)=>{const n=Qs(t);e.slots.default=()=>n},gi=(e,t,n)=>{for(const s in t)(n||!Js(s))&&(e[s]=t[s])},dc=(e,t,n)=>{const s=e.slots=fi();if(e.vnode.shapeFlag&32){const r=t._;r?(gi(s,t,n),n&&po(s,"_",r,!0)):hi(t,s)}else t&&pi(e,t)},hc=(e,t,n)=>{const{vnode:s,slots:r}=e;let o=!0,i=ie;if(s.shapeFlag&32){const l=t._;l?n&&l===1?o=!1:gi(r,t,n):(o=!t.$stable,hi(t,r)),i=t}else t&&(pi(e,t),i={default:1});if(o)for(const l in r)!Js(l)&&i[l]==null&&delete r[l]},Re=Pc;function pc(e){return gc(e)}function gc(e,t){const n=Kn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:o,createElement:i,createText:l,createComment:c,setText:a,setElementText:f,parentNode:d,nextSibling:p,setScopeId:m=qe,insertStaticContent:x}=e,A=(u,h,g,_=null,b=null,y=null,C=void 0,w=null,E=!!h.dynamicChildren)=>{if(u===h)return;u&&!wt(u,h)&&(_=v(u),Se(u,b,y,!0),u=null),h.patchFlag===-2&&(E=!1,h.dynamicChildren=null);const{type:S,ref:H,shapeFlag:O}=h;switch(S){case Yn:k(u,h,g,_);break;case be:N(u,h,g,_);break;case Rn:u==null&&I(h,g,_,C);break;case Ue:P(u,h,g,_,b,y,C,w,E);break;default:O&1?V(u,h,g,_,b,y,C,w,E):O&6?z(u,h,g,_,b,y,C,w,E):(O&64||O&128)&&S.process(u,h,g,_,b,y,C,w,E,j)}H!=null&&b?Jt(H,u&&u.ref,y,h||u,!h):H==null&&u&&u.ref!=null&&Jt(u.ref,null,y,u,!0)},k=(u,h,g,_)=>{if(u==null)s(h.el=l(h.children),g,_);else{const b=h.el=u.el;h.children!==u.children&&a(b,h.children)}},N=(u,h,g,_)=>{u==null?s(h.el=c(h.children||""),g,_):h.el=u.el},I=(u,h,g,_)=>{[u.el,u.anchor]=x(u.children,h,g,_,u.el,u.anchor)},F=({el:u,anchor:h},g,_)=>{let b;for(;u&&u!==h;)b=p(u),s(u,g,_),u=b;s(h,g,_)},L=({el:u,anchor:h})=>{let g;for(;u&&u!==h;)g=p(u),r(u),u=g;r(h)},V=(u,h,g,_,b,y,C,w,E)=>{h.type==="svg"?C="svg":h.type==="math"&&(C="mathml"),u==null?se(h,g,_,b,y,C,w,E):T(u,h,b,y,C,w,E)},se=(u,h,g,_,b,y,C,w)=>{let E,S;const{props:H,shapeFlag:O,transition:$,dirs:B}=u;if(E=u.el=i(u.type,y,H&&H.is,H),O&8?f(E,u.children):O&16&&W(u.children,E,null,_,b,fs(u,y),C,w),B&&vt(u,null,_,"created"),G(E,u,u.scopeId,C,_),H){for(const le in H)le!=="value"&&!qt(le)&&o(E,le,null,H[le],y,_);"value"in H&&o(E,"value",null,H.value,y),(S=H.onVnodeBeforeMount)&&Be(S,_,u)}B&&vt(u,null,_,"beforeMount");const J=mc(b,$);J&&$.beforeEnter(E),s(E,h,g),((S=H&&H.onVnodeMounted)||J||B)&&Re(()=>{S&&Be(S,_,u),J&&$.enter(E),B&&vt(u,null,_,"mounted")},b)},G=(u,h,g,_,b)=>{if(g&&m(u,g),_)for(let y=0;y<_.length;y++)m(u,_[y]);if(b){let y=b.subTree;if(h===y||Si(y.type)&&(y.ssContent===h||y.ssFallback===h)){const C=b.vnode;G(u,C,C.scopeId,C.slotScopeIds,b.parent)}}},W=(u,h,g,_,b,y,C,w,E=0)=>{for(let S=E;S<u.length;S++){const H=u[S]=w?at(u[S]):We(u[S]);A(null,H,h,g,_,b,y,C,w)}},T=(u,h,g,_,b,y,C)=>{const w=h.el=u.el;let{patchFlag:E,dynamicChildren:S,dirs:H}=h;E|=u.patchFlag&16;const O=u.props||ie,$=h.props||ie;let B;if(g&&bt(g,!1),(B=$.onVnodeBeforeUpdate)&&Be(B,g,h,u),H&&vt(h,u,g,"beforeUpdate"),g&&bt(g,!0),(O.innerHTML&&$.innerHTML==null||O.textContent&&$.textContent==null)&&f(w,""),S?U(u.dynamicChildren,S,w,g,_,fs(h,b),y):C||X(u,h,w,null,g,_,fs(h,b),y,!1),E>0){if(E&16)Q(w,O,$,g,b);else if(E&2&&O.class!==$.class&&o(w,"class",null,$.class,b),E&4&&o(w,"style",O.style,$.style,b),E&8){const J=h.dynamicProps;for(let le=0;le<J.length;le++){const te=J[le],xe=O[te],Ee=$[te];(Ee!==xe||te==="value")&&o(w,te,xe,Ee,b,g)}}E&1&&u.children!==h.children&&f(w,h.children)}else!C&&S==null&&Q(w,O,$,g,b);((B=$.onVnodeUpdated)||H)&&Re(()=>{B&&Be(B,g,h,u),H&&vt(h,u,g,"updated")},_)},U=(u,h,g,_,b,y,C)=>{for(let w=0;w<h.length;w++){const E=u[w],S=h[w],H=E.el&&(E.type===Ue||!wt(E,S)||E.shapeFlag&198)?d(E.el):g;A(E,S,H,null,_,b,y,C,!0)}},Q=(u,h,g,_,b)=>{if(h!==g){if(h!==ie)for(const y in h)!qt(y)&&!(y in g)&&o(u,y,h[y],null,b,_);for(const y in g){if(qt(y))continue;const C=g[y],w=h[y];C!==w&&y!=="value"&&o(u,y,w,C,b,_)}"value"in g&&o(u,"value",h.value,g.value,b)}},P=(u,h,g,_,b,y,C,w,E)=>{const S=h.el=u?u.el:l(""),H=h.anchor=u?u.anchor:l("");let{patchFlag:O,dynamicChildren:$,slotScopeIds:B}=h;B&&(w=w?w.concat(B):B),u==null?(s(S,g,_),s(H,g,_),W(h.children||[],g,H,b,y,C,w,E)):O>0&&O&64&&$&&u.dynamicChildren?(U(u.dynamicChildren,$,g,b,y,C,w),(h.key!=null||b&&h===b.subTree)&&mi(u,h,!0)):X(u,h,g,H,b,y,C,w,E)},z=(u,h,g,_,b,y,C,w,E)=>{h.slotScopeIds=w,u==null?h.shapeFlag&512?b.ctx.activate(h,g,_,C,E):ae(h,g,_,b,y,C,E):ye(u,h,E)},ae=(u,h,g,_,b,y,C)=>{const w=u.component=$c(u,_,b);if(zn(u)&&(w.ctx.renderer=j),Hc(w,!1,C),w.asyncDep){if(b&&b.registerDep(w,re,C),!u.el){const E=w.subTree=we(be);N(null,E,h,g),u.placeholder=E.el}}else re(w,u,h,g,b,y,C)},ye=(u,h,g)=>{const _=h.component=u.component;if(Rc(u,h,g))if(_.asyncDep&&!_.asyncResolved){q(_,h,g);return}else _.next=h,_.update();else h.el=u.el,_.vnode=h},re=(u,h,g,_,b,y,C)=>{const w=()=>{if(u.isMounted){let{next:O,bu:$,u:B,parent:J,vnode:le}=u;{const ke=yi(u);if(ke){O&&(O.el=le.el,q(u,O,C)),ke.asyncDep.then(()=>{u.isUnmounted||w()});return}}let te=O,xe;bt(u,!1),O?(O.el=le.el,q(u,O,C)):O=le,$&&wn($),(xe=O.props&&O.props.onVnodeBeforeUpdate)&&Be(xe,J,O,le),bt(u,!0);const Ee=vr(u),He=u.subTree;u.subTree=Ee,A(He,Ee,d(He.el),v(He),u,b,y),O.el=Ee.el,te===null&&Ac(u,Ee.el),B&&Re(B,b),(xe=O.props&&O.props.onVnodeUpdated)&&Re(()=>Be(xe,J,O,le),b)}else{let O;const{el:$,props:B}=h,{bm:J,m:le,parent:te,root:xe,type:Ee}=u,He=Qt(h);bt(u,!1),J&&wn(J),!He&&(O=B&&B.onVnodeBeforeMount)&&Be(O,te,h),bt(u,!0);{xe.ce&&xe.ce._def.shadowRoot!==!1&&xe.ce._injectChildStyle(Ee);const ke=u.subTree=vr(u);A(null,ke,g,_,u,b,y),h.el=ke.el}if(le&&Re(le,b),!He&&(O=B&&B.onVnodeMounted)){const ke=h;Re(()=>Be(O,te,ke),b)}(h.shapeFlag&256||te&&Qt(te.vnode)&&te.vnode.shapeFlag&256)&&u.a&&Re(u.a,b),u.isMounted=!0,h=g=_=null}};u.scope.on();const E=u.effect=new So(w);u.scope.off();const S=u.update=E.run.bind(E),H=u.job=E.runIfDirty.bind(E);H.i=u,H.id=u.uid,E.scheduler=()=>zs(H),bt(u,!0),S()},q=(u,h,g)=>{h.component=u;const _=u.vnode.props;u.vnode=h,u.next=null,fc(u,h.props,_,g),hc(u,h.children,g),st(),fr(u),rt()},X=(u,h,g,_,b,y,C,w,E=!1)=>{const S=u&&u.children,H=u?u.shapeFlag:0,O=h.children,{patchFlag:$,shapeFlag:B}=h;if($>0){if($&128){it(S,O,g,_,b,y,C,w,E);return}else if($&256){ze(S,O,g,_,b,y,C,w,E);return}}B&8?(H&16&&Te(S,b,y),O!==S&&f(g,O)):H&16?B&16?it(S,O,g,_,b,y,C,w,E):Te(S,b,y,!0):(H&8&&f(g,""),B&16&&W(O,g,_,b,y,C,w,E))},ze=(u,h,g,_,b,y,C,w,E)=>{u=u||Lt,h=h||Lt;const S=u.length,H=h.length,O=Math.min(S,H);let $;for($=0;$<O;$++){const B=h[$]=E?at(h[$]):We(h[$]);A(u[$],B,g,null,b,y,C,w,E)}S>H?Te(u,b,y,!0,!1,O):W(h,g,_,b,y,C,w,E,O)},it=(u,h,g,_,b,y,C,w,E)=>{let S=0;const H=h.length;let O=u.length-1,$=H-1;for(;S<=O&&S<=$;){const B=u[S],J=h[S]=E?at(h[S]):We(h[S]);if(wt(B,J))A(B,J,g,null,b,y,C,w,E);else break;S++}for(;S<=O&&S<=$;){const B=u[O],J=h[$]=E?at(h[$]):We(h[$]);if(wt(B,J))A(B,J,g,null,b,y,C,w,E);else break;O--,$--}if(S>O){if(S<=$){const B=$+1,J=B<H?h[B].el:_;for(;S<=$;)A(null,h[S]=E?at(h[S]):We(h[S]),g,J,b,y,C,w,E),S++}}else if(S>$)for(;S<=O;)Se(u[S],b,y,!0),S++;else{const B=S,J=S,le=new Map;for(S=J;S<=$;S++){const Ce=h[S]=E?at(h[S]):We(h[S]);Ce.key!=null&&le.set(Ce.key,S)}let te,xe=0;const Ee=$-J+1;let He=!1,ke=0;const Vt=new Array(Ee);for(S=0;S<Ee;S++)Vt[S]=0;for(S=B;S<=O;S++){const Ce=u[S];if(xe>=Ee){Se(Ce,b,y,!0);continue}let Ve;if(Ce.key!=null)Ve=le.get(Ce.key);else for(te=J;te<=$;te++)if(Vt[te-J]===0&&wt(Ce,h[te])){Ve=te;break}Ve===void 0?Se(Ce,b,y,!0):(Vt[Ve-J]=S+1,Ve>=ke?ke=Ve:He=!0,A(Ce,h[Ve],g,null,b,y,C,w,E),xe++)}const nr=He?yc(Vt):Lt;for(te=nr.length-1,S=Ee-1;S>=0;S--){const Ce=J+S,Ve=h[Ce],sr=h[Ce+1],rr=Ce+1<H?sr.el||sr.placeholder:_;Vt[S]===0?A(null,Ve,g,rr,b,y,C,w,E):He&&(te<0||S!==nr[te]?$e(Ve,g,rr,2):te--)}}},$e=(u,h,g,_,b=null)=>{const{el:y,type:C,transition:w,children:E,shapeFlag:S}=u;if(S&6){$e(u.component.subTree,h,g,_);return}if(S&128){u.suspense.move(h,g,_);return}if(S&64){C.move(u,h,g,j);return}if(C===Ue){s(y,h,g);for(let O=0;O<E.length;O++)$e(E[O],h,g,_);s(u.anchor,h,g);return}if(C===Rn){F(u,h,g);return}if(_!==2&&S&1&&w)if(_===0)w.beforeEnter(y),s(y,h,g),Re(()=>w.enter(y),b);else{const{leave:O,delayLeave:$,afterLeave:B}=w,J=()=>{u.ctx.isUnmounted?r(y):s(y,h,g)},le=()=>{y._isLeaving&&y[Ze](!0),O(y,()=>{J(),B&&B()})};$?$(y,J,le):le()}else s(y,h,g)},Se=(u,h,g,_=!1,b=!1)=>{const{type:y,props:C,ref:w,children:E,dynamicChildren:S,shapeFlag:H,patchFlag:O,dirs:$,cacheIndex:B}=u;if(O===-2&&(b=!1),w!=null&&(st(),Jt(w,null,g,u,!0),rt()),B!=null&&(h.renderCache[B]=void 0),H&256){h.ctx.deactivate(u);return}const J=H&1&&$,le=!Qt(u);let te;if(le&&(te=C&&C.onVnodeBeforeUnmount)&&Be(te,h,u),H&6)vn(u.component,g,_);else{if(H&128){u.suspense.unmount(g,_);return}J&&vt(u,null,h,"beforeUnmount"),H&64?u.type.remove(u,h,g,j,_):S&&!S.hasOnce&&(y!==Ue||O>0&&O&64)?Te(S,h,g,!1,!0):(y===Ue&&O&384||!b&&H&16)&&Te(E,h,g),_&&At(u)}(le&&(te=C&&C.onVnodeUnmounted)||J)&&Re(()=>{te&&Be(te,h,u),J&&vt(u,null,h,"unmounted")},g)},At=u=>{const{type:h,el:g,anchor:_,transition:b}=u;if(h===Ue){Pt(g,_);return}if(h===Rn){L(u);return}const y=()=>{r(g),b&&!b.persisted&&b.afterLeave&&b.afterLeave()};if(u.shapeFlag&1&&b&&!b.persisted){const{leave:C,delayLeave:w}=b,E=()=>C(g,y);w?w(u.el,y,E):E()}else y()},Pt=(u,h)=>{let g;for(;u!==h;)g=p(u),r(u),u=g;r(h)},vn=(u,h,g)=>{const{bum:_,scope:b,job:y,subTree:C,um:w,m:E,a:S}=u;_r(E),_r(S),_&&wn(_),b.stop(),y&&(y.flags|=8,Se(C,u,h,g)),w&&Re(w,h),Re(()=>{u.isUnmounted=!0},h)},Te=(u,h,g,_=!1,b=!1,y=0)=>{for(let C=y;C<u.length;C++)Se(u[C],h,g,_,b)},v=u=>{if(u.shapeFlag&6)return v(u.component.subTree);if(u.shapeFlag&128)return u.suspense.next();const h=p(u.anchor||u.el),g=h&&h[Hl];return g?p(g):h};let M=!1;const R=(u,h,g)=>{u==null?h._vnode&&Se(h._vnode,null,null,!0):A(h._vnode||null,u,h,null,null,null,g),h._vnode=u,M||(M=!0,fr(),Vo(),M=!1)},j={p:A,um:Se,m:$e,r:At,mt:ae,mc:W,pc:X,pbc:U,n:v,o:e};return{render:R,hydrate:void 0,createApp:ic(R)}}function fs({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function bt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function mc(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function mi(e,t,n=!1){const s=e.children,r=t.children;if(D(s)&&D(r))for(let o=0;o<s.length;o++){const i=s[o];let l=r[o];l.shapeFlag&1&&!l.dynamicChildren&&((l.patchFlag<=0||l.patchFlag===32)&&(l=r[o]=at(r[o]),l.el=i.el),!n&&l.patchFlag!==-2&&mi(i,l)),l.type===Yn&&l.patchFlag!==-1&&(l.el=i.el),l.type===be&&!l.el&&(l.el=i.el)}}function yc(e){const t=e.slice(),n=[0];let s,r,o,i,l;const c=e.length;for(s=0;s<c;s++){const a=e[s];if(a!==0){if(r=n[n.length-1],e[r]<a){t[s]=r,n.push(s);continue}for(o=0,i=n.length-1;o<i;)l=o+i>>1,e[n[l]]<a?o=l+1:i=l;a<e[n[o]]&&(o>0&&(t[s]=n[o-1]),n[o]=s)}}for(o=n.length,i=n[o-1];o-- >0;)n[o]=i,i=t[i];return n}function yi(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:yi(t)}function _r(e){if(e)for(let t=0;t<e.length;t++)e[t].flags|=8}const _c=Symbol.for("v-scx"),vc=()=>Le(_c);function Xt(e,t,n){return _i(e,t,n)}function _i(e,t,n=ie){const{immediate:s,deep:r,flush:o,once:i}=n,l=de({},n),c=t&&s||!t&&o!=="post";let a;if(fn){if(o==="sync"){const m=vc();a=m.__watcherHandles||(m.__watcherHandles=[])}else if(!c){const m=()=>{};return m.stop=qe,m.resume=qe,m.pause=qe,m}}const f=me;l.call=(m,x,A)=>je(m,f,x,A);let d=!1;o==="post"?l.scheduler=m=>{Re(m,f&&f.suspense)}:o!=="sync"&&(d=!0,l.scheduler=(m,x)=>{x?m():zs(m)}),l.augmentJob=m=>{t&&(m.flags|=4),d&&(m.flags|=2,f&&(m.id=f.uid,m.i=f))};const p=Nl(e,t,l);return fn&&(a?a.push(p):c&&p()),p}function bc(e,t,n){const s=this.proxy,r=ue(e)?e.includes(".")?vi(s,e):()=>s[e]:e.bind(s,s);let o;K(t)?o=t:(o=t.handler,n=t);const i=_n(this),l=_i(r,o.bind(s),n);return i(),l}function vi(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;r<n.length&&s;r++)s=s[n[r]];return s}}const Sc=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Ne(t)}Modifiers`]||e[`${_t(t)}Modifiers`];function xc(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||ie;let r=n;const o=t.startsWith("update:"),i=o&&Sc(s,t.slice(7));i&&(i.trim&&(r=n.map(f=>ue(f)?f.trim():f)),i.number&&(r=n.map(Tn)));let l,c=s[l=ns(t)]||s[l=ns(Ne(t))];!c&&o&&(c=s[l=ns(_t(t))]),c&&je(c,e,6,r);const a=s[l+"Once"];if(a){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,je(a,e,6,r)}}const Ec=new WeakMap;function bi(e,t,n=!1){const s=n?Ec:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const o=e.emits;let i={},l=!1;if(!K(e)){const c=a=>{const f=bi(a,t,!0);f&&(l=!0,de(i,f))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!o&&!l?(ne(e)&&s.set(e,null),null):(D(o)?o.forEach(c=>i[c]=null):de(i,o),ne(e)&&s.set(e,i),i)}function Qn(e,t){return!e||!kn(t)?!1:(t=t.slice(2).replace(/Once$/,""),ee(e,t[0].toLowerCase()+t.slice(1))||ee(e,_t(t))||ee(e,t))}function vr(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[o],slots:i,attrs:l,emit:c,render:a,renderCache:f,props:d,data:p,setupState:m,ctx:x,inheritAttrs:A}=e,k=Nn(e);let N,I;try{if(n.shapeFlag&4){const L=r||s,V=L;N=We(a.call(V,L,f,d,m,p,x)),I=l}else{const L=t;N=We(L.length>1?L(d,{attrs:l,slots:i,emit:c}):L(d,null)),I=t.props?l:wc(l)}}catch(L){Zt.length=0,qn(L,e,1),N=we(be)}let F=N;if(I&&A!==!1){const L=Object.keys(I),{shapeFlag:V}=F;L.length&&V&7&&(o&&L.some(Ns)&&(I=Cc(I,o)),F=yt(F,I,!1,!0))}return n.dirs&&(F=yt(F,null,!1,!0),F.dirs=F.dirs?F.dirs.concat(n.dirs):n.dirs),n.transition&&ln(F,n.transition),N=F,Nn(k),N}const wc=e=>{let t;for(const n in e)(n==="class"||n==="style"||kn(n))&&((t||(t={}))[n]=e[n]);return t},Cc=(e,t)=>{const n={};for(const s in e)(!Ns(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Rc(e,t,n){const{props:s,children:r,component:o}=e,{props:i,children:l,patchFlag:c}=t,a=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?br(s,i,a):!!i;if(c&8){const f=t.dynamicProps;for(let d=0;d<f.length;d++){const p=f[d];if(i[p]!==s[p]&&!Qn(a,p))return!0}}}else return(r||l)&&(!l||!l.$stable)?!0:s===i?!1:s?i?br(s,i,a):!0:!!i;return!1}function br(e,t,n){const s=Object.keys(t);if(s.length!==Object.keys(e).length)return!0;for(let r=0;r<s.length;r++){const o=s[r];if(t[o]!==e[o]&&!Qn(n,o))return!0}return!1}function Ac({vnode:e,parent:t},n){for(;t;){const s=t.subTree;if(s.suspense&&s.suspense.activeBranch===e&&(s.el=e.el),s===e)(e=t.vnode).el=n,t=t.parent;else break}}const Si=e=>e.__isSuspense;function Pc(e,t){t&&t.pendingBranch?D(e)?t.effects.push(...e):t.effects.push(e):Dl(e)}const Ue=Symbol.for("v-fgt"),Yn=Symbol.for("v-txt"),be=Symbol.for("v-cmt"),Rn=Symbol.for("v-stc"),Zt=[];let Pe=null;function Tc(e=!1){Zt.push(Pe=e?null:[])}function Oc(){Zt.pop(),Pe=Zt[Zt.length-1]||null}let cn=1;function Dn(e,t=!1){cn+=e,e<0&&Pe&&t&&(Pe.hasOnce=!0)}function xi(e){return e.dynamicChildren=cn>0?Pe||Lt:null,Oc(),cn>0&&Pe&&Pe.push(e),e}function ju(e,t,n,s,r,o){return xi(wi(e,t,n,s,r,o,!0))}function Mc(e,t,n,s,r){return xi(we(e,t,n,s,r,!0))}function $n(e){return e?e.__v_isVNode===!0:!1}function wt(e,t){return e.type===t.type&&e.key===t.key}const Ei=({key:e})=>e??null,An=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ue(e)||fe(e)||K(e)?{i:Ae,r:e,k:t,f:!!n}:e:null);function wi(e,t=null,n=null,s=0,r=null,o=e===Ue?0:1,i=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Ei(t),ref:t&&An(t),scopeId:Ko,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:Ae};return l?(Ys(c,n),o&128&&e.normalize(c)):n&&(c.shapeFlag|=ue(n)?8:16),cn>0&&!i&&Pe&&(c.patchFlag>0||o&6)&&c.patchFlag!==32&&Pe.push(c),c}const we=Ic;function Ic(e,t=null,n=null,s=0,r=null,o=!1){if((!e||e===si)&&(e=be),$n(e)){const l=yt(e,t,!0);return n&&Ys(l,n),cn>0&&!o&&Pe&&(l.shapeFlag&6?Pe[Pe.indexOf(e)]=l:Pe.push(l)),l.patchFlag=-2,l}if(Uc(e)&&(e=e.__vccOpts),t){t=Lc(t);let{class:l,style:c}=t;l&&!ue(l)&&(t.class=$s(l)),ne(c)&&(qs(c)&&!D(c)&&(c=de({},c)),t.style=Ds(c))}const i=ue(e)?1:Si(e)?128:Uo(e)?64:ne(e)?4:K(e)?2:0;return wi(e,t,n,s,r,i,o,!0)}function Lc(e){return e?qs(e)||ui(e)?de({},e):e:null}function yt(e,t,n=!1,s=!1){const{props:r,ref:o,patchFlag:i,children:l,transition:c}=e,a=t?Fc(r||{},t):r,f={__v_isVNode:!0,__v_skip:!0,type:e.type,props:a,key:a&&Ei(a),ref:t&&t.ref?n&&o?D(o)?o.concat(An(t)):[o,An(t)]:An(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ue?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&yt(e.ssContent),ssFallback:e.ssFallback&&yt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&ln(f,c.clone(f)),f}function Nc(e=" ",t=0){return we(Yn,null,e,t)}function Du(e,t){const n=we(Rn,null,e);return n.staticCount=t,n}function $u(e="",t=!1){return t?(Tc(),Mc(be,null,e)):we(be,null,e)}function We(e){return e==null||typeof e=="boolean"?we(be):D(e)?we(Ue,null,e.slice()):$n(e)?at(e):we(Yn,null,String(e))}function at(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:yt(e)}function Ys(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(D(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Ys(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!ui(t)?t._ctx=Ae:r===3&&Ae&&(Ae.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else K(t)?(t={default:t,_ctx:Ae},n=32):(t=String(t),s&64?(n=16,t=[Nc(t)]):n=8);e.children=t,e.shapeFlag|=n}function Fc(...e){const t={};for(let n=0;n<e.length;n++){const s=e[n];for(const r in s)if(r==="class")t.class!==s.class&&(t.class=$s([t.class,s.class]));else if(r==="style")t.style=Ds([t.style,s.style]);else if(kn(r)){const o=t[r],i=s[r];i&&o!==i&&!(D(o)&&o.includes(i))&&(t[r]=o?[].concat(o,i):i)}else r!==""&&(t[r]=s[r])}return t}function Be(e,t,n,s=null){je(e,t,7,[n,s])}const jc=li();let Dc=0;function $c(e,t,n){const s=e.type,r=(t?t.appContext:e.appContext)||jc,o={uid:Dc++,vnode:e,type:s,parent:t,appContext:r,root:null,next:null,subTree:null,effect:null,update:null,job:null,scope:new _o(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:t?t.provides:Object.create(r.provides),ids:t?t.ids:["",0,0],accessCache:null,renderCache:[],components:null,directives:null,propsOptions:di(s,r),emitsOptions:bi(s,r),emit:null,emitted:null,propsDefaults:ie,inheritAttrs:s.inheritAttrs,ctx:ie,data:ie,props:ie,attrs:ie,slots:ie,refs:ie,setupState:ie,setupContext:null,suspense:n,suspenseId:n?n.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null};return o.ctx={_:o},o.root=t?t.root:o,o.emit=xc.bind(null,o),e.ce&&e.ce(o),o}let me=null;const Xs=()=>me||Ae;let Hn,Rs;{const e=Kn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),o=>{r.length>1?r.forEach(i=>i(o)):r[0](o)}};Hn=t("__VUE_INSTANCE_SETTERS__",n=>me=n),Rs=t("__VUE_SSR_SETTERS__",n=>fn=n)}const _n=e=>{const t=me;return Hn(e),e.scope.on(),()=>{e.scope.off(),Hn(t)}},Sr=()=>{me&&me.scope.off(),Hn(null)};function Ci(e){return e.vnode.shapeFlag&4}let fn=!1;function Hc(e,t=!1,n=!1){t&&Rs(t);const{props:s,children:r}=e.vnode,o=Ci(e);cc(e,s,o,t),dc(e,r,n||t);const i=o?kc(e,t):void 0;return t&&Rs(!1),i}function kc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Zl);const{setup:s}=n;if(s){st();const r=e.setupContext=s.length>1?Bc(e):null,o=_n(e),i=yn(s,e,0,[e.props,r]),l=uo(i);if(rt(),o(),(l||e.sp)&&!Qt(e)&&Yo(e),l){if(i.then(Sr,Sr),t)return i.then(c=>{xr(e,c)}).catch(c=>{qn(c,e,0)});e.asyncDep=i}else xr(e,i)}else Ri(e)}function xr(e,t,n){K(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ne(t)&&(e.setupState=Do(t)),Ri(e)}function Ri(e,t,n){const s=e.type;e.render||(e.render=s.render||qe);{const r=_n(e);st();try{ec(e)}finally{rt(),r()}}}const Vc={get(e,t){return ge(e,"get",""),e[t]}};function Bc(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Vc),slots:e.slots,emit:e.emit,expose:t}}function Xn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Do(Gs(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Yt)return Yt[n](e)},has(t,n){return n in t||n in Yt}})):e.proxy}function Kc(e,t=!0){return K(e)?e.displayName||e.name:e.name||t&&e.__name}function Uc(e){return K(e)&&"__vccOpts"in e}const Me=(e,t)=>Il(e,t,fn);function Zs(e,t,n){try{Dn(-1);const s=arguments.length;return s===2?ne(t)&&!D(t)?$n(t)?we(e,null,[t]):we(e,t):we(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&$n(n)&&(n=[n]),we(e,t,n))}finally{Dn(1)}}const Wc="3.5.22";/**
14
- * @vue/runtime-dom v3.5.22
15
- * (c) 2018-present Yuxi (Evan) You and Vue contributors
16
- * @license MIT
17
- **/let As;const Er=typeof window<"u"&&window.trustedTypes;if(Er)try{As=Er.createPolicy("vue",{createHTML:e=>e})}catch{}const Ai=As?e=>As.createHTML(e):e=>e,qc="http://www.w3.org/2000/svg",Gc="http://www.w3.org/1998/Math/MathML",Xe=typeof document<"u"?document:null,wr=Xe&&Xe.createElement("template"),zc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Xe.createElementNS(qc,e):t==="mathml"?Xe.createElementNS(Gc,e):n?Xe.createElement(e,{is:n}):Xe.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Xe.createTextNode(e),createComment:e=>Xe.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Xe.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,o){const i=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===o||!(r=r.nextSibling)););else{wr.innerHTML=Ai(s==="svg"?`<svg>${e}</svg>`:s==="mathml"?`<math>${e}</math>`:e);const l=wr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},lt="transition",Kt="animation",un=Symbol("_vtc"),Pi={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Jc=de({},Wo,Pi),Qc=e=>(e.displayName="Transition",e.props=Jc,e),Hu=Qc((e,{slots:t})=>Zs(Bl,Yc(e),t)),St=(e,t=[])=>{D(e)?e.forEach(n=>n(...t)):e&&e(...t)},Cr=e=>e?D(e)?e.some(t=>t.length>1):e.length>1:!1;function Yc(e){const t={};for(const P in e)P in Pi||(t[P]=e[P]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:o=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=o,appearActiveClass:a=i,appearToClass:f=l,leaveFromClass:d=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,x=Xc(r),A=x&&x[0],k=x&&x[1],{onBeforeEnter:N,onEnter:I,onEnterCancelled:F,onLeave:L,onLeaveCancelled:V,onBeforeAppear:se=N,onAppear:G=I,onAppearCancelled:W=F}=t,T=(P,z,ae,ye)=>{P._enterCancelled=ye,xt(P,z?f:l),xt(P,z?a:i),ae&&ae()},U=(P,z)=>{P._isLeaving=!1,xt(P,d),xt(P,m),xt(P,p),z&&z()},Q=P=>(z,ae)=>{const ye=P?G:I,re=()=>T(z,P,ae);St(ye,[z,re]),Rr(()=>{xt(z,P?c:o),Qe(z,P?f:l),Cr(ye)||Ar(z,s,A,re)})};return de(t,{onBeforeEnter(P){St(N,[P]),Qe(P,o),Qe(P,i)},onBeforeAppear(P){St(se,[P]),Qe(P,c),Qe(P,a)},onEnter:Q(!1),onAppear:Q(!0),onLeave(P,z){P._isLeaving=!0;const ae=()=>U(P,z);Qe(P,d),P._enterCancelled?(Qe(P,p),Or(P)):(Or(P),Qe(P,p)),Rr(()=>{P._isLeaving&&(xt(P,d),Qe(P,m),Cr(L)||Ar(P,s,k,ae))}),St(L,[P,ae])},onEnterCancelled(P){T(P,!1,void 0,!0),St(F,[P])},onAppearCancelled(P){T(P,!0,void 0,!0),St(W,[P])},onLeaveCancelled(P){U(P),St(V,[P])}})}function Xc(e){if(e==null)return null;if(ne(e))return[us(e.enter),us(e.leave)];{const t=us(e);return[t,t]}}function us(e){return Qi(e)}function Qe(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[un]||(e[un]=new Set)).add(t)}function xt(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[un];n&&(n.delete(t),n.size||(e[un]=void 0))}function Rr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Zc=0;function Ar(e,t,n,s){const r=e._endId=++Zc,o=()=>{r===e._endId&&s()};if(n!=null)return setTimeout(o,n);const{type:i,timeout:l,propCount:c}=ef(e,t);if(!i)return s();const a=i+"end";let f=0;const d=()=>{e.removeEventListener(a,p),o()},p=m=>{m.target===e&&++f>=c&&d()};setTimeout(()=>{f<c&&d()},l+1),e.addEventListener(a,p)}function ef(e,t){const n=window.getComputedStyle(e),s=x=>(n[x]||"").split(", "),r=s(`${lt}Delay`),o=s(`${lt}Duration`),i=Pr(r,o),l=s(`${Kt}Delay`),c=s(`${Kt}Duration`),a=Pr(l,c);let f=null,d=0,p=0;t===lt?i>0&&(f=lt,d=i,p=o.length):t===Kt?a>0&&(f=Kt,d=a,p=c.length):(d=Math.max(i,a),f=d>0?i>a?lt:Kt:null,p=f?f===lt?o.length:c.length:0);const m=f===lt&&/\b(?:transform|all)(?:,|$)/.test(s(`${lt}Property`).toString());return{type:f,timeout:d,propCount:p,hasTransform:m}}function Pr(e,t){for(;e.length<t.length;)e=e.concat(e);return Math.max(...t.map((n,s)=>Tr(n)+Tr(e[s])))}function Tr(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Or(e){return(e?e.ownerDocument:document).body.offsetHeight}function tf(e,t,n){const s=e[un];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Mr=Symbol("_vod"),nf=Symbol("_vsh"),sf=Symbol(""),rf=/(?:^|;)\s*display\s*:/;function of(e,t,n){const s=e.style,r=ue(n);let o=!1;if(n&&!r){if(t)if(ue(t))for(const i of t.split(";")){const l=i.slice(0,i.indexOf(":")).trim();n[l]==null&&Pn(s,l,"")}else for(const i in t)n[i]==null&&Pn(s,i,"");for(const i in n)i==="display"&&(o=!0),Pn(s,i,n[i])}else if(r){if(t!==n){const i=s[sf];i&&(n+=";"+i),s.cssText=n,o=rf.test(n)}}else t&&e.removeAttribute("style");Mr in e&&(e[Mr]=o?s.display:"",e[nf]&&(s.display="none"))}const Ir=/\s*!important$/;function Pn(e,t,n){if(D(n))n.forEach(s=>Pn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=lf(e,t);Ir.test(n)?e.setProperty(_t(s),n.replace(Ir,""),"important"):e[s]=n}}const Lr=["Webkit","Moz","ms"],as={};function lf(e,t){const n=as[t];if(n)return n;let s=Ne(t);if(s!=="filter"&&s in e)return as[t]=s;s=Bn(s);for(let r=0;r<Lr.length;r++){const o=Lr[r]+s;if(o in e)return as[t]=o}return t}const Nr="http://www.w3.org/1999/xlink";function Fr(e,t,n,s,r,o=nl(t)){s&&t.startsWith("xlink:")?n==null?e.removeAttributeNS(Nr,t.slice(6,t.length)):e.setAttributeNS(Nr,t,n):n==null||o&&!go(n)?e.removeAttribute(t):e.setAttribute(t,o?"":Ge(n)?String(n):n)}function jr(e,t,n,s,r){if(t==="innerHTML"||t==="textContent"){n!=null&&(e[t]=t==="innerHTML"?Ai(n):n);return}const o=e.tagName;if(t==="value"&&o!=="PROGRESS"&&!o.includes("-")){const l=o==="OPTION"?e.getAttribute("value")||"":e.value,c=n==null?e.type==="checkbox"?"on":"":String(n);(l!==c||!("_value"in e))&&(e.value=c),n==null&&e.removeAttribute(t),e._value=n;return}let i=!1;if(n===""||n==null){const l=typeof e[t];l==="boolean"?n=go(n):n==null&&l==="string"?(n="",i=!0):l==="number"&&(n=0,i=!0)}try{e[t]=n}catch{}i&&e.removeAttribute(r||t)}function ht(e,t,n,s){e.addEventListener(t,n,s)}function cf(e,t,n,s){e.removeEventListener(t,n,s)}const Dr=Symbol("_vei");function ff(e,t,n,s,r=null){const o=e[Dr]||(e[Dr]={}),i=o[t];if(s&&i)i.value=s;else{const[l,c]=uf(t);if(s){const a=o[t]=hf(s,r);ht(e,l,a,c)}else i&&(cf(e,l,i,c),o[t]=void 0)}}const $r=/(?:Once|Passive|Capture)$/;function uf(e){let t;if($r.test(e)){t={};let s;for(;s=e.match($r);)e=e.slice(0,e.length-s[0].length),t[s[0].toLowerCase()]=!0}return[e[2]===":"?e.slice(3):_t(e.slice(2)),t]}let ds=0;const af=Promise.resolve(),df=()=>ds||(af.then(()=>ds=0),ds=Date.now());function hf(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;je(pf(s,n.value),t,5,[s])};return n.value=e,n.attached=df(),n}function pf(e,t){if(D(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Hr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,gf=(e,t,n,s,r,o)=>{const i=r==="svg";t==="class"?tf(e,s,i):t==="style"?of(e,n,s):kn(t)?Ns(t)||ff(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):mf(e,t,s,i))?(jr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Fr(e,t,s,i,o,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!ue(s))?jr(e,Ne(t),s,o,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Fr(e,t,s,i))};function mf(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Hr(t)&&K(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Hr(t)&&ue(n)?!1:t in e}const Dt=e=>{const t=e.props["onUpdate:modelValue"]||!1;return D(t)?n=>wn(t,n):t};function yf(e){e.target.composing=!0}function kr(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const nt=Symbol("_assign"),ku={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[nt]=Dt(r);const o=s||r.props&&r.props.type==="number";ht(e,t?"change":"input",i=>{if(i.target.composing)return;let l=e.value;n&&(l=l.trim()),o&&(l=Tn(l)),e[nt](l)}),n&&ht(e,"change",()=>{e.value=e.value.trim()}),t||(ht(e,"compositionstart",yf),ht(e,"compositionend",kr),ht(e,"change",kr))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:o}},i){if(e[nt]=Dt(i),e.composing)return;const l=(o||e.type==="number")&&!/^0\d/.test(e.value)?Tn(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c))}},Vu={deep:!0,created(e,t,n){e[nt]=Dt(n),ht(e,"change",()=>{const s=e._modelValue,r=an(e),o=e.checked,i=e[nt];if(D(s)){const l=Hs(s,r),c=l!==-1;if(o&&!c)i(s.concat(r));else if(!o&&c){const a=[...s];a.splice(l,1),i(a)}}else if(kt(s)){const l=new Set(s);o?l.add(r):l.delete(r),i(l)}else i(Ti(e,o))})},mounted:Vr,beforeUpdate(e,t,n){e[nt]=Dt(n),Vr(e,t,n)}};function Vr(e,{value:t,oldValue:n},s){e._modelValue=t;let r;if(D(t))r=Hs(t,s.props.value)>-1;else if(kt(t))r=t.has(s.props.value);else{if(t===n)return;r=gn(t,Ti(e,!0))}e.checked!==r&&(e.checked=r)}const Bu={deep:!0,created(e,{value:t,modifiers:{number:n}},s){const r=kt(t);ht(e,"change",()=>{const o=Array.prototype.filter.call(e.options,i=>i.selected).map(i=>n?Tn(an(i)):an(i));e[nt](e.multiple?r?new Set(o):o:o[0]),e._assigning=!0,Gn(()=>{e._assigning=!1})}),e[nt]=Dt(s)},mounted(e,{value:t}){Br(e,t)},beforeUpdate(e,t,n){e[nt]=Dt(n)},updated(e,{value:t}){e._assigning||Br(e,t)}};function Br(e,t){const n=e.multiple,s=D(t);if(!(n&&!s&&!kt(t))){for(let r=0,o=e.options.length;r<o;r++){const i=e.options[r],l=an(i);if(n)if(s){const c=typeof l;c==="string"||c==="number"?i.selected=t.some(a=>String(a)===String(l)):i.selected=Hs(t,l)>-1}else i.selected=t.has(l);else if(gn(an(i),t)){e.selectedIndex!==r&&(e.selectedIndex=r);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function an(e){return"_value"in e?e._value:e.value}function Ti(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const _f=["ctrl","shift","alt","meta"],vf={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>_f.some(n=>e[`${n}Key`]&&!t.includes(n))},Ku=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...o)=>{for(let i=0;i<t.length;i++){const l=vf[t[i]];if(l&&l(r,t))return}return e(r,...o)})},bf={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Uu=(e,t)=>{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const o=_t(r.key);if(t.some(i=>i===o||bf[i]===o))return e(r)})},Sf=de({patchProp:gf},zc);let Kr;function xf(){return Kr||(Kr=pc(Sf))}const Wu=(...e)=>{const t=xf().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=wf(s);if(!r)return;const o=t._component;!K(o)&&!o.render&&!o.template&&(o.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const i=n(r,!1,Ef(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),i},t};function Ef(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function wf(e){return ue(e)?document.querySelector(e):e}/*!
18
- * pinia v2.3.1
19
- * (c) 2025 Eduardo San Martin Morote
20
- * @license MIT
21
- */let Oi;const Zn=e=>Oi=e,Mi=Symbol();function Ps(e){return e&&typeof e=="object"&&Object.prototype.toString.call(e)==="[object Object]"&&typeof e.toJSON!="function"}var en;(function(e){e.direct="direct",e.patchObject="patch object",e.patchFunction="patch function"})(en||(en={}));function qu(){const e=vo(!0),t=e.run(()=>Wn({}));let n=[],s=[];const r=Gs({install(o){Zn(r),r._a=o,o.provide(Mi,r),o.config.globalProperties.$pinia=r,s.forEach(i=>n.push(i)),s=[]},use(o){return this._a?n.push(o):s.push(o),this},_p:n,_a:null,_e:e,_s:new Map,state:t});return r}const Ii=()=>{};function Ur(e,t,n,s=Ii){e.push(t);const r=()=>{const o=e.indexOf(t);o>-1&&(e.splice(o,1),s())};return!n&&bo()&&ol(r),r}function Ot(e,...t){e.slice().forEach(n=>{n(...t)})}const Cf=e=>e(),Wr=Symbol(),hs=Symbol();function Ts(e,t){e instanceof Map&&t instanceof Map?t.forEach((n,s)=>e.set(s,n)):e instanceof Set&&t instanceof Set&&t.forEach(e.add,e);for(const n in t){if(!t.hasOwnProperty(n))continue;const s=t[n],r=e[n];Ps(r)&&Ps(s)&&e.hasOwnProperty(n)&&!fe(s)&&!gt(s)?e[n]=Ts(r,s):e[n]=s}return e}const Rf=Symbol();function Af(e){return!Ps(e)||!e.hasOwnProperty(Rf)}const{assign:ft}=Object;function Pf(e){return!!(fe(e)&&e.effect)}function Tf(e,t,n,s){const{state:r,actions:o,getters:i}=t,l=n.state.value[e];let c;function a(){l||(n.state.value[e]=r?r():{});const f=Pl(n.state.value[e]);return ft(f,o,Object.keys(i||{}).reduce((d,p)=>(d[p]=Gs(Me(()=>{Zn(n);const m=n._s.get(e);return i[p].call(m,m)})),d),{}))}return c=Li(e,a,t,n,s,!0),c}function Li(e,t,n={},s,r,o){let i;const l=ft({actions:{}},n),c={deep:!0};let a,f,d=[],p=[],m;const x=s.state.value[e];!o&&!x&&(s.state.value[e]={}),Wn({});let A;function k(W){let T;a=f=!1,typeof W=="function"?(W(s.state.value[e]),T={type:en.patchFunction,storeId:e,events:m}):(Ts(s.state.value[e],W),T={type:en.patchObject,payload:W,storeId:e,events:m});const U=A=Symbol();Gn().then(()=>{A===U&&(a=!0)}),f=!0,Ot(d,T,s.state.value[e])}const N=o?function(){const{state:T}=n,U=T?T():{};this.$patch(Q=>{ft(Q,U)})}:Ii;function I(){i.stop(),d=[],p=[],s._s.delete(e)}const F=(W,T="")=>{if(Wr in W)return W[hs]=T,W;const U=function(){Zn(s);const Q=Array.from(arguments),P=[],z=[];function ae(q){P.push(q)}function ye(q){z.push(q)}Ot(p,{args:Q,name:U[hs],store:V,after:ae,onError:ye});let re;try{re=W.apply(this&&this.$id===e?this:V,Q)}catch(q){throw Ot(z,q),q}return re instanceof Promise?re.then(q=>(Ot(P,q),q)).catch(q=>(Ot(z,q),Promise.reject(q))):(Ot(P,re),re)};return U[Wr]=!0,U[hs]=T,U},L={_p:s,$id:e,$onAction:Ur.bind(null,p),$patch:k,$reset:N,$subscribe(W,T={}){const U=Ur(d,W,T.detached,()=>Q()),Q=i.run(()=>Xt(()=>s.state.value[e],P=>{(T.flush==="sync"?f:a)&&W({storeId:e,type:en.direct,events:m},P)},ft({},c,T)));return U},$dispose:I},V=mn(L);s._s.set(e,V);const G=(s._a&&s._a.runWithContext||Cf)(()=>s._e.run(()=>(i=vo()).run(()=>t({action:F}))));for(const W in G){const T=G[W];if(fe(T)&&!Pf(T)||gt(T))o||(x&&Af(T)&&(fe(T)?T.value=x[W]:Ts(T,x[W])),s.state.value[e][W]=T);else if(typeof T=="function"){const U=F(T,W);G[W]=U,l.actions[W]=T}}return ft(V,G),ft(Y(V),G),Object.defineProperty(V,"$state",{get:()=>s.state.value[e],set:W=>{k(T=>{ft(T,W)})}}),s._p.forEach(W=>{ft(V,i.run(()=>W({store:V,app:s._a,pinia:s,options:l})))}),x&&o&&n.hydrate&&n.hydrate(V.$state,x),a=!0,f=!0,V}/*! #__NO_SIDE_EFFECTS__ */function Gu(e,t,n){let s,r;const o=typeof t=="function";s=e,r=o?n:t;function i(l,c){const a=lc();return l=l||(a?Le(Mi,null):null),l&&Zn(l),l=Oi,l._s.has(s)||(o?Li(s,t,r,l):Tf(s,r,l)),l._s.get(s)}return i.$id=s,i}/*!
22
- * vue-router v4.5.1
23
- * (c) 2025 Eduardo San Martin Morote
24
- * @license MIT
25
- */const It=typeof document<"u";function Ni(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Of(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Ni(e.default)}const Z=Object.assign;function ps(e,t){const n={};for(const s in t){const r=t[s];n[s]=De(r)?r.map(e):e(r)}return n}const tn=()=>{},De=Array.isArray,Fi=/#/g,Mf=/&/g,If=/\//g,Lf=/=/g,Nf=/\?/g,ji=/\+/g,Ff=/%5B/g,jf=/%5D/g,Di=/%5E/g,Df=/%60/g,$i=/%7B/g,$f=/%7C/g,Hi=/%7D/g,Hf=/%20/g;function er(e){return encodeURI(""+e).replace($f,"|").replace(Ff,"[").replace(jf,"]")}function kf(e){return er(e).replace($i,"{").replace(Hi,"}").replace(Di,"^")}function Os(e){return er(e).replace(ji,"%2B").replace(Hf,"+").replace(Fi,"%23").replace(Mf,"%26").replace(Df,"`").replace($i,"{").replace(Hi,"}").replace(Di,"^")}function Vf(e){return Os(e).replace(Lf,"%3D")}function Bf(e){return er(e).replace(Fi,"%23").replace(Nf,"%3F")}function Kf(e){return e==null?"":Bf(e).replace(If,"%2F")}function dn(e){try{return decodeURIComponent(""+e)}catch{}return""+e}const Uf=/\/$/,Wf=e=>e.replace(Uf,"");function gs(e,t,n="/"){let s,r={},o="",i="";const l=t.indexOf("#");let c=t.indexOf("?");return l<c&&l>=0&&(c=-1),c>-1&&(s=t.slice(0,c),o=t.slice(c+1,l>-1?l:t.length),r=e(o)),l>-1&&(s=s||t.slice(0,l),i=t.slice(l,t.length)),s=Jf(s??t,n),{fullPath:s+(o&&"?")+o+i,path:s,query:r,hash:dn(i)}}function qf(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function qr(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Gf(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&$t(t.matched[s],n.matched[r])&&ki(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function $t(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function ki(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!zf(e[n],t[n]))return!1;return!0}function zf(e,t){return De(e)?Gr(e,t):De(t)?Gr(t,e):e===t}function Gr(e,t){return De(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function Jf(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let o=n.length-1,i,l;for(i=0;i<s.length;i++)if(l=s[i],l!==".")if(l==="..")o>1&&o--;else break;return n.slice(0,o).join("/")+"/"+s.slice(i).join("/")}const ct={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};var hn;(function(e){e.pop="pop",e.push="push"})(hn||(hn={}));var nn;(function(e){e.back="back",e.forward="forward",e.unknown=""})(nn||(nn={}));function Qf(e){if(!e)if(It){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),Wf(e)}const Yf=/^[^#]+#/;function Xf(e,t){return e.replace(Yf,"#")+t}function Zf(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const es=()=>({left:window.scrollX,top:window.scrollY});function eu(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=Zf(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function zr(e,t){return(history.state?history.state.position-t:-1)+e}const Ms=new Map;function tu(e,t){Ms.set(e,t)}function nu(e){const t=Ms.get(e);return Ms.delete(e),t}let su=()=>location.protocol+"//"+location.host;function Vi(e,t){const{pathname:n,search:s,hash:r}=t,o=e.indexOf("#");if(o>-1){let l=r.includes(e.slice(o))?e.slice(o).length:1,c=r.slice(l);return c[0]!=="/"&&(c="/"+c),qr(c,"")}return qr(n,e)+s+r}function ru(e,t,n,s){let r=[],o=[],i=null;const l=({state:p})=>{const m=Vi(e,location),x=n.value,A=t.value;let k=0;if(p){if(n.value=m,t.value=p,i&&i===x){i=null;return}k=A?p.position-A.position:0}else s(m);r.forEach(N=>{N(n.value,x,{delta:k,type:hn.pop,direction:k?k>0?nn.forward:nn.back:nn.unknown})})};function c(){i=n.value}function a(p){r.push(p);const m=()=>{const x=r.indexOf(p);x>-1&&r.splice(x,1)};return o.push(m),m}function f(){const{history:p}=window;p.state&&p.replaceState(Z({},p.state,{scroll:es()}),"")}function d(){for(const p of o)p();o=[],window.removeEventListener("popstate",l),window.removeEventListener("beforeunload",f)}return window.addEventListener("popstate",l),window.addEventListener("beforeunload",f,{passive:!0}),{pauseListeners:c,listen:a,destroy:d}}function Jr(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?es():null}}function ou(e){const{history:t,location:n}=window,s={value:Vi(e,n)},r={value:t.state};r.value||o(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function o(c,a,f){const d=e.indexOf("#"),p=d>-1?(n.host&&document.querySelector("base")?e:e.slice(d))+c:su()+e+c;try{t[f?"replaceState":"pushState"](a,"",p),r.value=a}catch(m){console.error(m),n[f?"replace":"assign"](p)}}function i(c,a){const f=Z({},t.state,Jr(r.value.back,c,r.value.forward,!0),a,{position:r.value.position});o(c,f,!0),s.value=c}function l(c,a){const f=Z({},r.value,t.state,{forward:c,scroll:es()});o(f.current,f,!0);const d=Z({},Jr(s.value,c,null),{position:f.position+1},a);o(c,d,!1),s.value=c}return{location:s,state:r,push:l,replace:i}}function zu(e){e=Qf(e);const t=ou(e),n=ru(e,t.state,t.location,t.replace);function s(o,i=!0){i||n.pauseListeners(),history.go(o)}const r=Z({location:"",base:e,go:s,createHref:Xf.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}function iu(e){return typeof e=="string"||e&&typeof e=="object"}function Bi(e){return typeof e=="string"||typeof e=="symbol"}const Ki=Symbol("");var Qr;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Qr||(Qr={}));function Ht(e,t){return Z(new Error,{type:e,[Ki]:!0},t)}function Ye(e,t){return e instanceof Error&&Ki in e&&(t==null||!!(e.type&t))}const Yr="[^/]+?",lu={sensitive:!1,strict:!1,start:!0,end:!0},cu=/[.+*?^${}()[\]/\\]/g;function fu(e,t){const n=Z({},lu,t),s=[];let r=n.start?"^":"";const o=[];for(const a of e){const f=a.length?[]:[90];n.strict&&!a.length&&(r+="/");for(let d=0;d<a.length;d++){const p=a[d];let m=40+(n.sensitive?.25:0);if(p.type===0)d||(r+="/"),r+=p.value.replace(cu,"\\$&"),m+=40;else if(p.type===1){const{value:x,repeatable:A,optional:k,regexp:N}=p;o.push({name:x,repeatable:A,optional:k});const I=N||Yr;if(I!==Yr){m+=10;try{new RegExp(`(${I})`)}catch(L){throw new Error(`Invalid custom RegExp for param "${x}" (${I}): `+L.message)}}let F=A?`((?:${I})(?:/(?:${I}))*)`:`(${I})`;d||(F=k&&a.length<2?`(?:/${F})`:"/"+F),k&&(F+="?"),r+=F,m+=20,k&&(m+=-8),A&&(m+=-20),I===".*"&&(m+=-50)}f.push(m)}s.push(f)}if(n.strict&&n.end){const a=s.length-1;s[a][s[a].length-1]+=.7000000000000001}n.strict||(r+="/?"),n.end?r+="$":n.strict&&!r.endsWith("/")&&(r+="(?:/|$)");const i=new RegExp(r,n.sensitive?"":"i");function l(a){const f=a.match(i),d={};if(!f)return null;for(let p=1;p<f.length;p++){const m=f[p]||"",x=o[p-1];d[x.name]=m&&x.repeatable?m.split("/"):m}return d}function c(a){let f="",d=!1;for(const p of e){(!d||!f.endsWith("/"))&&(f+="/"),d=!1;for(const m of p)if(m.type===0)f+=m.value;else if(m.type===1){const{value:x,repeatable:A,optional:k}=m,N=x in a?a[x]:"";if(De(N)&&!A)throw new Error(`Provided param "${x}" is an array but it is not repeatable (* or + modifiers)`);const I=De(N)?N.join("/"):N;if(!I)if(k)p.length<2&&(f.endsWith("/")?f=f.slice(0,-1):d=!0);else throw new Error(`Missing required param "${x}"`);f+=I}}return f||"/"}return{re:i,score:s,keys:o,parse:l,stringify:c}}function uu(e,t){let n=0;for(;n<e.length&&n<t.length;){const s=t[n]-e[n];if(s)return s;n++}return e.length<t.length?e.length===1&&e[0]===80?-1:1:e.length>t.length?t.length===1&&t[0]===80?1:-1:0}function Ui(e,t){let n=0;const s=e.score,r=t.score;for(;n<s.length&&n<r.length;){const o=uu(s[n],r[n]);if(o)return o;n++}if(Math.abs(r.length-s.length)===1){if(Xr(s))return 1;if(Xr(r))return-1}return r.length-s.length}function Xr(e){const t=e[e.length-1];return e.length>0&&t[t.length-1]<0}const au={type:0,value:""},du=/[a-zA-Z0-9_]/;function hu(e){if(!e)return[[]];if(e==="/")return[[au]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(m){throw new Error(`ERR (${n})/"${a}": ${m}`)}let n=0,s=n;const r=[];let o;function i(){o&&r.push(o),o=[]}let l=0,c,a="",f="";function d(){a&&(n===0?o.push({type:0,value:a}):n===1||n===2||n===3?(o.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${a}) must be alone in its segment. eg: '/:ids+.`),o.push({type:1,value:a,regexp:f,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),a="")}function p(){a+=c}for(;l<e.length;){if(c=e[l++],c==="\\"&&n!==2){s=n,n=4;continue}switch(n){case 0:c==="/"?(a&&d(),i()):c===":"?(d(),n=1):p();break;case 4:p(),n=s;break;case 1:c==="("?n=2:du.test(c)?p():(d(),n=0,c!=="*"&&c!=="?"&&c!=="+"&&l--);break;case 2:c===")"?f[f.length-1]=="\\"?f=f.slice(0,-1)+c:n=3:f+=c;break;case 3:d(),n=0,c!=="*"&&c!=="?"&&c!=="+"&&l--,f="";break;default:t("Unknown state");break}}return n===2&&t(`Unfinished custom RegExp for param "${a}"`),d(),i(),r}function pu(e,t,n){const s=fu(hu(e.path),n),r=Z(s,{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function gu(e,t){const n=[],s=new Map;t=no({strict:!1,end:!0,sensitive:!1},t);function r(d){return s.get(d)}function o(d,p,m){const x=!m,A=eo(d);A.aliasOf=m&&m.record;const k=no(t,d),N=[A];if("alias"in d){const L=typeof d.alias=="string"?[d.alias]:d.alias;for(const V of L)N.push(eo(Z({},A,{components:m?m.record.components:A.components,path:V,aliasOf:m?m.record:A})))}let I,F;for(const L of N){const{path:V}=L;if(p&&V[0]!=="/"){const se=p.record.path,G=se[se.length-1]==="/"?"":"/";L.path=p.record.path+(V&&G+V)}if(I=pu(L,p,k),m?m.alias.push(I):(F=F||I,F!==I&&F.alias.push(I),x&&d.name&&!to(I)&&i(d.name)),Wi(I)&&c(I),A.children){const se=A.children;for(let G=0;G<se.length;G++)o(se[G],I,m&&m.children[G])}m=m||I}return F?()=>{i(F)}:tn}function i(d){if(Bi(d)){const p=s.get(d);p&&(s.delete(d),n.splice(n.indexOf(p),1),p.children.forEach(i),p.alias.forEach(i))}else{const p=n.indexOf(d);p>-1&&(n.splice(p,1),d.record.name&&s.delete(d.record.name),d.children.forEach(i),d.alias.forEach(i))}}function l(){return n}function c(d){const p=_u(d,n);n.splice(p,0,d),d.record.name&&!to(d)&&s.set(d.record.name,d)}function a(d,p){let m,x={},A,k;if("name"in d&&d.name){if(m=s.get(d.name),!m)throw Ht(1,{location:d});k=m.record.name,x=Z(Zr(p.params,m.keys.filter(F=>!F.optional).concat(m.parent?m.parent.keys.filter(F=>F.optional):[]).map(F=>F.name)),d.params&&Zr(d.params,m.keys.map(F=>F.name))),A=m.stringify(x)}else if(d.path!=null)A=d.path,m=n.find(F=>F.re.test(A)),m&&(x=m.parse(A),k=m.record.name);else{if(m=p.name?s.get(p.name):n.find(F=>F.re.test(p.path)),!m)throw Ht(1,{location:d,currentLocation:p});k=m.record.name,x=Z({},p.params,d.params),A=m.stringify(x)}const N=[];let I=m;for(;I;)N.unshift(I.record),I=I.parent;return{name:k,path:A,params:x,matched:N,meta:yu(N)}}e.forEach(d=>o(d));function f(){n.length=0,s.clear()}return{addRoute:o,resolve:a,removeRoute:i,clearRoutes:f,getRoutes:l,getRecordMatcher:r}}function Zr(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function eo(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:mu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function mu(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function to(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function yu(e){return e.reduce((t,n)=>Z(t,n.meta),{})}function no(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}function _u(e,t){let n=0,s=t.length;for(;n!==s;){const o=n+s>>1;Ui(e,t[o])<0?s=o:n=o+1}const r=vu(e);return r&&(s=t.lastIndexOf(r,s-1)),s}function vu(e){let t=e;for(;t=t.parent;)if(Wi(t)&&Ui(e,t)===0)return t}function Wi({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function bu(e){const t={};if(e===""||e==="?")return t;const s=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<s.length;++r){const o=s[r].replace(ji," "),i=o.indexOf("="),l=dn(i<0?o:o.slice(0,i)),c=i<0?null:dn(o.slice(i+1));if(l in t){let a=t[l];De(a)||(a=t[l]=[a]),a.push(c)}else t[l]=c}return t}function so(e){let t="";for(let n in e){const s=e[n];if(n=Vf(n),s==null){s!==void 0&&(t+=(t.length?"&":"")+n);continue}(De(s)?s.map(o=>o&&Os(o)):[s&&Os(s)]).forEach(o=>{o!==void 0&&(t+=(t.length?"&":"")+n,o!=null&&(t+="="+o))})}return t}function Su(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=De(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const xu=Symbol(""),ro=Symbol(""),ts=Symbol(""),tr=Symbol(""),Is=Symbol("");function Ut(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function dt(e,t,n,s,r,o=i=>i()){const i=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((l,c)=>{const a=p=>{p===!1?c(Ht(4,{from:n,to:t})):p instanceof Error?c(p):iu(p)?c(Ht(2,{from:t,to:p})):(i&&s.enterCallbacks[r]===i&&typeof p=="function"&&i.push(p),l())},f=o(()=>e.call(s&&s.instances[r],t,n,a));let d=Promise.resolve(f);e.length<3&&(d=d.then(a)),d.catch(p=>c(p))})}function ms(e,t,n,s,r=o=>o()){const o=[];for(const i of e)for(const l in i.components){let c=i.components[l];if(!(t!=="beforeRouteEnter"&&!i.instances[l]))if(Ni(c)){const f=(c.__vccOpts||c)[t];f&&o.push(dt(f,n,s,i,l,r))}else{let a=c();o.push(()=>a.then(f=>{if(!f)throw new Error(`Couldn't resolve component "${l}" at "${i.path}"`);const d=Of(f)?f.default:f;i.mods[l]=f,i.components[l]=d;const m=(d.__vccOpts||d)[t];return m&&dt(m,n,s,i,l,r)()}))}}return o}function oo(e){const t=Le(ts),n=Le(tr),s=Me(()=>{const c=Ft(e.to);return t.resolve(c)}),r=Me(()=>{const{matched:c}=s.value,{length:a}=c,f=c[a-1],d=n.matched;if(!f||!d.length)return-1;const p=d.findIndex($t.bind(null,f));if(p>-1)return p;const m=io(c[a-2]);return a>1&&io(f)===m&&d[d.length-1].path!==m?d.findIndex($t.bind(null,c[a-2])):p}),o=Me(()=>r.value>-1&&Au(n.params,s.value.params)),i=Me(()=>r.value>-1&&r.value===n.matched.length-1&&ki(n.params,s.value.params));function l(c={}){if(Ru(c)){const a=t[Ft(e.replace)?"replace":"push"](Ft(e.to)).catch(tn);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>a),a}return Promise.resolve()}return{route:s,href:Me(()=>s.value.href),isActive:o,isExactActive:i,navigate:l}}function Eu(e){return e.length===1?e[0]:e}const wu=Qo({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:oo,setup(e,{slots:t}){const n=mn(oo(e)),{options:s}=Le(ts),r=Me(()=>({[lo(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[lo(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const o=t.default&&Eu(t.default(n));return e.custom?o:Zs("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},o)}}}),Cu=wu;function Ru(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Au(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!De(r)||r.length!==s.length||s.some((o,i)=>o!==r[i]))return!1}return!0}function io(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const lo=(e,t,n)=>e??t??n,Pu=Qo({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=Le(Is),r=Me(()=>e.route||s.value),o=Le(ro,0),i=Me(()=>{let a=Ft(o);const{matched:f}=r.value;let d;for(;(d=f[a])&&!d.components;)a++;return a}),l=Me(()=>r.value.matched[i.value]);Cn(ro,Me(()=>i.value+1)),Cn(xu,l),Cn(Is,r);const c=Wn();return Xt(()=>[c.value,l.value,e.name],([a,f,d],[p,m,x])=>{f&&(f.instances[d]=a,m&&m!==f&&a&&a===p&&(f.leaveGuards.size||(f.leaveGuards=m.leaveGuards),f.updateGuards.size||(f.updateGuards=m.updateGuards))),a&&f&&(!m||!$t(f,m)||!p)&&(f.enterCallbacks[d]||[]).forEach(A=>A(a))},{flush:"post"}),()=>{const a=r.value,f=e.name,d=l.value,p=d&&d.components[f];if(!p)return co(n.default,{Component:p,route:a});const m=d.props[f],x=m?m===!0?a.params:typeof m=="function"?m(a):m:null,k=Zs(p,Z({},x,t,{onVnodeUnmounted:N=>{N.component.isUnmounted&&(d.instances[f]=null)},ref:c}));return co(n.default,{Component:k,route:a})||k}}});function co(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Tu=Pu;function Ju(e){const t=gu(e.routes,e),n=e.parseQuery||bu,s=e.stringifyQuery||so,r=e.history,o=Ut(),i=Ut(),l=Ut(),c=Cl(ct);let a=ct;It&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const f=ps.bind(null,v=>""+v),d=ps.bind(null,Kf),p=ps.bind(null,dn);function m(v,M){let R,j;return Bi(v)?(R=t.getRecordMatcher(v),j=M):j=v,t.addRoute(j,R)}function x(v){const M=t.getRecordMatcher(v);M&&t.removeRoute(M)}function A(){return t.getRoutes().map(v=>v.record)}function k(v){return!!t.getRecordMatcher(v)}function N(v,M){if(M=Z({},M||c.value),typeof v=="string"){const g=gs(n,v,M.path),_=t.resolve({path:g.path},M),b=r.createHref(g.fullPath);return Z(g,_,{params:p(_.params),hash:dn(g.hash),redirectedFrom:void 0,href:b})}let R;if(v.path!=null)R=Z({},v,{path:gs(n,v.path,M.path).path});else{const g=Z({},v.params);for(const _ in g)g[_]==null&&delete g[_];R=Z({},v,{params:d(g)}),M.params=d(M.params)}const j=t.resolve(R,M),oe=v.hash||"";j.params=f(p(j.params));const u=qf(s,Z({},v,{hash:kf(oe),path:j.path})),h=r.createHref(u);return Z({fullPath:u,hash:oe,query:s===so?Su(v.query):v.query||{}},j,{redirectedFrom:void 0,href:h})}function I(v){return typeof v=="string"?gs(n,v,c.value.path):Z({},v)}function F(v,M){if(a!==v)return Ht(8,{from:M,to:v})}function L(v){return G(v)}function V(v){return L(Z(I(v),{replace:!0}))}function se(v){const M=v.matched[v.matched.length-1];if(M&&M.redirect){const{redirect:R}=M;let j=typeof R=="function"?R(v):R;return typeof j=="string"&&(j=j.includes("?")||j.includes("#")?j=I(j):{path:j},j.params={}),Z({query:v.query,hash:v.hash,params:j.path!=null?{}:v.params},j)}}function G(v,M){const R=a=N(v),j=c.value,oe=v.state,u=v.force,h=v.replace===!0,g=se(R);if(g)return G(Z(I(g),{state:typeof g=="object"?Z({},oe,g.state):oe,force:u,replace:h}),M||R);const _=R;_.redirectedFrom=M;let b;return!u&&Gf(s,j,R)&&(b=Ht(16,{to:_,from:j}),$e(j,j,!0,!1)),(b?Promise.resolve(b):U(_,j)).catch(y=>Ye(y)?Ye(y,2)?y:it(y):X(y,_,j)).then(y=>{if(y){if(Ye(y,2))return G(Z({replace:h},I(y.to),{state:typeof y.to=="object"?Z({},oe,y.to.state):oe,force:u}),M||_)}else y=P(_,j,!0,h,oe);return Q(_,j,y),y})}function W(v,M){const R=F(v,M);return R?Promise.reject(R):Promise.resolve()}function T(v){const M=Pt.values().next().value;return M&&typeof M.runWithContext=="function"?M.runWithContext(v):v()}function U(v,M){let R;const[j,oe,u]=Ou(v,M);R=ms(j.reverse(),"beforeRouteLeave",v,M);for(const g of j)g.leaveGuards.forEach(_=>{R.push(dt(_,v,M))});const h=W.bind(null,v,M);return R.push(h),Te(R).then(()=>{R=[];for(const g of o.list())R.push(dt(g,v,M));return R.push(h),Te(R)}).then(()=>{R=ms(oe,"beforeRouteUpdate",v,M);for(const g of oe)g.updateGuards.forEach(_=>{R.push(dt(_,v,M))});return R.push(h),Te(R)}).then(()=>{R=[];for(const g of u)if(g.beforeEnter)if(De(g.beforeEnter))for(const _ of g.beforeEnter)R.push(dt(_,v,M));else R.push(dt(g.beforeEnter,v,M));return R.push(h),Te(R)}).then(()=>(v.matched.forEach(g=>g.enterCallbacks={}),R=ms(u,"beforeRouteEnter",v,M,T),R.push(h),Te(R))).then(()=>{R=[];for(const g of i.list())R.push(dt(g,v,M));return R.push(h),Te(R)}).catch(g=>Ye(g,8)?g:Promise.reject(g))}function Q(v,M,R){l.list().forEach(j=>T(()=>j(v,M,R)))}function P(v,M,R,j,oe){const u=F(v,M);if(u)return u;const h=M===ct,g=It?history.state:{};R&&(j||h?r.replace(v.fullPath,Z({scroll:h&&g&&g.scroll},oe)):r.push(v.fullPath,oe)),c.value=v,$e(v,M,R,h),it()}let z;function ae(){z||(z=r.listen((v,M,R)=>{if(!vn.listening)return;const j=N(v),oe=se(j);if(oe){G(Z(oe,{replace:!0,force:!0}),j).catch(tn);return}a=j;const u=c.value;It&&tu(zr(u.fullPath,R.delta),es()),U(j,u).catch(h=>Ye(h,12)?h:Ye(h,2)?(G(Z(I(h.to),{force:!0}),j).then(g=>{Ye(g,20)&&!R.delta&&R.type===hn.pop&&r.go(-1,!1)}).catch(tn),Promise.reject()):(R.delta&&r.go(-R.delta,!1),X(h,j,u))).then(h=>{h=h||P(j,u,!1),h&&(R.delta&&!Ye(h,8)?r.go(-R.delta,!1):R.type===hn.pop&&Ye(h,20)&&r.go(-1,!1)),Q(j,u,h)}).catch(tn)}))}let ye=Ut(),re=Ut(),q;function X(v,M,R){it(v);const j=re.list();return j.length?j.forEach(oe=>oe(v,M,R)):console.error(v),Promise.reject(v)}function ze(){return q&&c.value!==ct?Promise.resolve():new Promise((v,M)=>{ye.add([v,M])})}function it(v){return q||(q=!v,ae(),ye.list().forEach(([M,R])=>v?R(v):M()),ye.reset()),v}function $e(v,M,R,j){const{scrollBehavior:oe}=e;if(!It||!oe)return Promise.resolve();const u=!R&&nu(zr(v.fullPath,0))||(j||!R)&&history.state&&history.state.scroll||null;return Gn().then(()=>oe(v,M,u)).then(h=>h&&eu(h)).catch(h=>X(h,v,M))}const Se=v=>r.go(v);let At;const Pt=new Set,vn={currentRoute:c,listening:!0,addRoute:m,removeRoute:x,clearRoutes:t.clearRoutes,hasRoute:k,getRoutes:A,resolve:N,options:e,push:L,replace:V,go:Se,back:()=>Se(-1),forward:()=>Se(1),beforeEach:o.add,beforeResolve:i.add,afterEach:l.add,onError:re.add,isReady:ze,install(v){const M=this;v.component("RouterLink",Cu),v.component("RouterView",Tu),v.config.globalProperties.$router=M,Object.defineProperty(v.config.globalProperties,"$route",{enumerable:!0,get:()=>Ft(c)}),It&&!At&&c.value===ct&&(At=!0,L(r.location).catch(oe=>{}));const R={};for(const oe in ct)Object.defineProperty(R,oe,{get:()=>c.value[oe],enumerable:!0});v.provide(ts,M),v.provide(tr,Fo(R)),v.provide(Is,c);const j=v.unmount;Pt.add(v),v.unmount=function(){Pt.delete(v),Pt.size<1&&(a=ct,z&&z(),z=null,c.value=ct,At=!1,q=!1),j()}}};function Te(v){return v.reduce((M,R)=>M.then(()=>T(R)),Promise.resolve())}return vn}function Ou(e,t){const n=[],s=[],r=[],o=Math.max(t.matched.length,e.matched.length);for(let i=0;i<o;i++){const l=t.matched[i];l&&(e.matched.find(a=>$t(a,l))?s.push(l):n.push(l));const c=e.matched[i];c&&(t.matched.find(a=>$t(a,c))||r.push(c))}return[n,s,r]}function Qu(){return Le(ts)}function Yu(e){return Le(tr)}export{Mu as A,$l as B,Mc as C,Nu as D,Gu as E,Ue as F,Yu as G,Qu as H,Ju as I,zu as J,Wu as K,qu as L,Hu as T,mn as a,ju as b,Me as c,wi as d,$u as e,Iu as f,Bu as g,Zs as h,Du as i,ti as j,$s as k,Nc as l,Fu as m,Ds as n,Tc as o,Gn as p,Xt as q,Wn as r,Zo as s,rl as t,we as u,ku as v,Ku as w,Lu as x,Vu as y,Uu as z};
26
- //# sourceMappingURL=vendor-Ctwcxhgt.js.map
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/dist/assets/vendor-Ctwcxhgt.js.map DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/index.html DELETED
@@ -1,16 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="de">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>SAAP - Multi-Agent Platform</title>
8
- <meta name="description" content="satware AI Autonomous Agent Platform - Lokale Multi-Agent Verwaltung" />
9
- <script type="module" crossorigin src="/assets/index-RrLk96-w.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-Ctwcxhgt.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-C4sh0Su3.css">
12
- </head>
13
- <body>
14
- <div id="app"></div>
15
- </body>
16
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/nginx.conf CHANGED
@@ -54,7 +54,7 @@ http {
54
 
55
  # API proxy to backend
56
  location /api/ {
57
- proxy_pass http://backend:8000/api/;
58
  proxy_http_version 1.1;
59
  proxy_set_header Upgrade $http_upgrade;
60
  proxy_set_header Connection 'upgrade';
 
54
 
55
  # API proxy to backend
56
  location /api/ {
57
+ proxy_pass http://backend:8000/;
58
  proxy_http_version 1.1;
59
  proxy_set_header Upgrade $http_upgrade;
60
  proxy_set_header Connection 'upgrade';
frontend/src/App.vue CHANGED
@@ -4,59 +4,12 @@
4
  <header class="saap-header">
5
  <div class="header-content">
6
  <div class="logo-section">
7
- <router-link to="/" class="logo-link">
8
- <h1 class="saap-heading-2">
9
- <span class="logo-text">SAAP</span>
10
- <span class="logo-subtitle">Multi-Agent Platform</span>
11
- </h1>
12
- </router-link>
13
  </div>
14
 
15
- <!-- Navigation Icons -->
16
- <nav class="nav-section">
17
- <router-link to="/" class="nav-icon" :class="{ active: $route.path === '/' }" title="Dashboard">
18
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
19
- <rect x="3" y="3" width="7" height="7"></rect>
20
- <rect x="14" y="3" width="7" height="7"></rect>
21
- <rect x="14" y="14" width="7" height="7"></rect>
22
- <rect x="3" y="14" width="7" height="7"></rect>
23
- </svg>
24
- <span class="nav-label">Dashboard</span>
25
- </router-link>
26
-
27
- <router-link to="/costs" class="nav-icon" :class="{ active: $route.path === '/costs' }" title="Cost Analytics">
28
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
29
- <line x1="12" y1="1" x2="12" y2="23"></line>
30
- <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
31
- </svg>
32
- <span class="nav-label">Costs</span>
33
- </router-link>
34
-
35
- <router-link to="/monitoring" class="nav-icon" :class="{ active: $route.path === '/monitoring' }" title="System Monitoring">
36
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
37
- <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
38
- </svg>
39
- <span class="nav-label">Monitoring</span>
40
- </router-link>
41
-
42
- <router-link to="/settings" class="nav-icon" :class="{ active: $route.path === '/settings' }" title="Settings">
43
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
44
- <circle cx="12" cy="12" r="3"></circle>
45
- <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
46
- </svg>
47
- <span class="nav-label">Settings</span>
48
- </router-link>
49
-
50
- <router-link to="/about" class="nav-icon" :class="{ active: $route.path === '/about' }" title="About SAAP">
51
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
52
- <circle cx="12" cy="12" r="10"></circle>
53
- <line x1="12" y1="16" x2="12" y2="12"></line>
54
- <line x1="12" y1="8" x2="12.01" y2="8"></line>
55
- </svg>
56
- <span class="nav-label">About</span>
57
- </router-link>
58
- </nav>
59
-
60
  <div class="status-section">
61
  <div class="connection-status" :class="connectionStatusClass">
62
  <div class="status-dot"></div>
@@ -71,9 +24,9 @@
71
  </div>
72
  </header>
73
 
74
- <!-- Main Content Area - Router View -->
75
  <main class="saap-main">
76
- <router-view />
77
  </main>
78
 
79
  <!-- Footer -->
@@ -82,7 +35,7 @@
82
  <span class="footer-text">
83
  SAAP v1.0.0 •
84
  Built with Vue.js •
85
- Powered by Hanan Wandji
86
  </span>
87
  <span class="footer-build">
88
  Build: {{ buildDate }}
@@ -215,50 +168,6 @@ export default {
215
  color: var(--saap-gray-600);
216
  }
217
 
218
- .logo-link {
219
- text-decoration: none;
220
- color: inherit;
221
- }
222
-
223
- /* Navigation Section */
224
- .nav-section {
225
- display: flex;
226
- align-items: center;
227
- gap: var(--saap-space-2);
228
- }
229
-
230
- .nav-icon {
231
- display: flex;
232
- flex-direction: column;
233
- align-items: center;
234
- gap: var(--saap-space-1);
235
- padding: var(--saap-space-2) var(--saap-space-3);
236
- border-radius: var(--saap-radius-lg);
237
- color: var(--saap-gray-500);
238
- text-decoration: none;
239
- transition: all var(--saap-transition-base);
240
- }
241
-
242
- .nav-icon:hover {
243
- background: var(--saap-gray-100);
244
- color: var(--saap-primary-600);
245
- }
246
-
247
- .nav-icon.active {
248
- background: linear-gradient(135deg, rgba(79, 70, 229, 0.1), rgba(34, 197, 94, 0.1));
249
- color: var(--saap-primary-600);
250
- }
251
-
252
- .nav-icon svg {
253
- width: 20px;
254
- height: 20px;
255
- }
256
-
257
- .nav-label {
258
- font-size: var(--saap-text-xs);
259
- font-weight: var(--saap-font-medium);
260
- }
261
-
262
  /* Status Section */
263
  .status-section {
264
  display: flex;
@@ -351,30 +260,14 @@ export default {
351
  @media (max-width: 768px) {
352
  .header-content {
353
  flex-direction: column;
354
- gap: var(--saap-space-3);
355
- padding: var(--saap-space-3);
356
  }
357
 
358
  .logo-subtitle {
359
  display: none;
360
  }
361
 
362
- .nav-section {
363
- order: 3;
364
- width: 100%;
365
- justify-content: center;
366
- border-top: 1px solid var(--saap-gray-200);
367
- padding-top: var(--saap-space-3);
368
- }
369
-
370
- .nav-label {
371
- display: none;
372
- }
373
-
374
- .nav-icon {
375
- padding: var(--saap-space-2);
376
- }
377
-
378
  .status-section {
379
  gap: var(--saap-space-4);
380
  }
 
4
  <header class="saap-header">
5
  <div class="header-content">
6
  <div class="logo-section">
7
+ <h1 class="saap-heading-2">
8
+ <span class="logo-text">SAAP</span>
9
+ <span class="logo-subtitle">Multi-Agent Platform</span>
10
+ </h1>
 
 
11
  </div>
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  <div class="status-section">
14
  <div class="connection-status" :class="connectionStatusClass">
15
  <div class="status-dot"></div>
 
24
  </div>
25
  </header>
26
 
27
+ <!-- Main Content Area - Direct Dashboard -->
28
  <main class="saap-main">
29
+ <SaapDashboard />
30
  </main>
31
 
32
  <!-- Footer -->
 
35
  <span class="footer-text">
36
  SAAP v1.0.0 •
37
  Built with Vue.js •
38
+ Powered by colossus Server
39
  </span>
40
  <span class="footer-build">
41
  Build: {{ buildDate }}
 
168
  color: var(--saap-gray-600);
169
  }
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  /* Status Section */
172
  .status-section {
173
  display: flex;
 
260
  @media (max-width: 768px) {
261
  .header-content {
262
  flex-direction: column;
263
+ gap: var(--saap-space-4);
264
+ padding: var(--saap-space-4);
265
  }
266
 
267
  .logo-subtitle {
268
  display: none;
269
  }
270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  .status-section {
272
  gap: var(--saap-space-4);
273
  }
frontend/src/components/modals/MultiAgentChatModal.vue CHANGED
@@ -123,38 +123,6 @@
123
 
124
  <!-- 🚀 INPUT BEREICH GARANTIERT SICHTBAR -->
125
  <div class="chat-input-section">
126
- <!-- 📄 Document Upload Area -->
127
- <div v-if="uploadedDocument" class="uploaded-document">
128
- <div class="document-info">
129
- <span class="document-icon">📄</span>
130
- <div class="document-details">
131
- <span class="document-name">{{ uploadedDocument.filename }}</span>
132
- <span class="document-size">{{ formatFileSize(uploadedDocument.size) }}</span>
133
- </div>
134
- <div v-if="uploadedDocument.privacyInfo" class="document-privacy">
135
- <span v-if="uploadedDocument.privacyInfo?.use_local" class="privacy-indicator sensitive">
136
- 🔒 Sensible Daten erkannt
137
- </span>
138
- <span v-else class="privacy-indicator safe">
139
- ✅ Keine sensiblen Daten
140
- </span>
141
- </div>
142
- </div>
143
- <button @click="removeDocument" class="remove-document" title="Dokument entfernen">
144
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
146
- </svg>
147
- </button>
148
- </div>
149
-
150
- <!-- 📤 Upload Progress -->
151
- <div v-if="isUploading" class="upload-progress">
152
- <div class="progress-bar-container">
153
- <div class="progress-bar-fill" :style="{ width: uploadProgress + '%' }"></div>
154
- </div>
155
- <span class="progress-text">{{ uploadProgress }}% - {{ uploadStatus }}</span>
156
- </div>
157
-
158
  <div class="input-group">
159
  <textarea
160
  v-model="currentMessage"
@@ -165,33 +133,16 @@
165
  rows="3"
166
  :disabled="isProcessing"
167
  ></textarea>
168
- <div class="input-buttons">
169
- <!-- 📎 File Upload Button -->
170
- <label for="file-upload" class="file-upload-button" :class="{ disabled: isProcessing || isUploading }">
171
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
172
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
173
- </svg>
174
- <input
175
- id="file-upload"
176
- type="file"
177
- accept=".pdf,.docx,.txt"
178
- @change="handleFileUpload"
179
- :disabled="isProcessing || isUploading"
180
- style="display: none;"
181
- />
182
- </label>
183
-
184
- <button
185
- @click="sendMessage"
186
- :disabled="!canSendMessage"
187
- class="send-button"
188
- >
189
- <svg v-if="!isProcessing" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
190
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
191
- </svg>
192
- <div v-else class="spinner"></div>
193
- </button>
194
- </div>
195
  </div>
196
 
197
  <div class="input-options">
@@ -289,7 +240,7 @@
289
  </template>
290
 
291
  <script>
292
- import { ref, computed, nextTick, onMounted, watch, toRef } from 'vue'
293
  import { saapApi } from '../../services/saapApi'
294
 
295
  export default {
@@ -316,12 +267,6 @@ export default {
316
  const selectedProvider = ref('auto')
317
  const showPrivacyInfo = ref(false)
318
 
319
- // 📄 Document Upload State
320
- const uploadedDocument = ref(null)
321
- const isUploading = ref(false)
322
- const uploadProgress = ref(0)
323
- const uploadStatus = ref('')
324
-
325
  // Available specialists data
326
  const availableSpecialists = ref([
327
  {
@@ -454,10 +399,6 @@ export default {
454
 
455
  chatMessages.value.push(message)
456
  scrollToBottom()
457
-
458
- // 💾 Auto-save to PostgreSQL (async, non-blocking)
459
- saveMessageToDb(message)
460
-
461
  return message
462
  }
463
 
@@ -504,34 +445,16 @@ export default {
504
  provider = null // Let backend decide
505
  }
506
 
507
- // 📄 Build message with document context if available
508
- let fullMessage = message
509
- if (uploadedDocument.value && uploadedDocument.value.parsedText) {
510
- const docContext = `\n\n📄 **Dokument-Kontext (${uploadedDocument.value.filename}):**\n${uploadedDocument.value.parsedText}`
511
- fullMessage = message + docContext
512
-
513
- // Show info that document is included
514
- processingStatus.value = 'Analysiert Dokument + Anfrage...'
515
- }
516
-
517
  // Call multi-agent API with provider selection
518
- const response = await saapApi.multiAgentChat(fullMessage, {
519
  user_context: {
520
  timestamp: new Date().toISOString().split('.')[0] + 'Z', // 🔧 FIX: Remove milliseconds for Python compatibility
521
- priority: taskPriority.value,
522
- has_document: !!uploadedDocument.value,
523
- document_name: uploadedDocument.value?.filename || null
524
  },
525
  preferred_agent: preferredAgent.value || null,
526
  task_priority: taskPriority.value,
527
  provider: provider,
528
- privacy_mode: forceProvider || selectedProvider.value,
529
- document_context: uploadedDocument.value ? {
530
- filename: uploadedDocument.value.filename,
531
- content: uploadedDocument.value.parsedText,
532
- size: uploadedDocument.value.size,
533
- is_sensitive: uploadedDocument.value.privacyInfo?.use_local || false
534
- } : null
535
  })
536
 
537
  // Update coordination chain based on backend response
@@ -616,130 +539,6 @@ export default {
616
  }
617
  }
618
 
619
- // 📄 Document Upload Functions
620
- const handleFileUpload = async (event) => {
621
- const file = event.target.files?.[0]
622
- if (!file) return
623
-
624
- // Validate file size (max 10MB)
625
- const maxSize = 10 * 1024 * 1024 // 10MB
626
- if (file.size > maxSize) {
627
- addMessage(
628
- `❌ Datei zu groß: ${formatFileSize(file.size)}. Maximum: 10MB`,
629
- 'system',
630
- 'System',
631
- 'error',
632
- { role: 'Upload Error' }
633
- )
634
- event.target.value = '' // Reset input
635
- return
636
- }
637
-
638
- // Validate file type
639
- const allowedTypes = ['.pdf', '.docx', '.txt']
640
- const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
641
- if (!allowedTypes.includes(fileExtension)) {
642
- addMessage(
643
- `❌ Ungültiger Dateityp: ${fileExtension}. Erlaubt: PDF, DOCX, TXT`,
644
- 'system',
645
- 'System',
646
- 'error',
647
- { role: 'Upload Error' }
648
- )
649
- event.target.value = '' // Reset input
650
- return
651
- }
652
-
653
- try {
654
- isUploading.value = true
655
- uploadProgress.value = 0
656
- uploadStatus.value = 'Uploading document...'
657
-
658
- // Upload to backend via saapApi (uses correct base URL for environment)
659
- uploadProgress.value = 50 // Simulated progress
660
- const data = await saapApi.uploadDocument(file)
661
-
662
- // 🔧 FIX: Defensive handling of backend response
663
- const hasSensitiveData = data?.has_sensitive_data ?? data?.is_sensitive ?? false
664
- const sensitiveDataTypes = data?.sensitive_data_types ?? data?.detected_types ?? []
665
-
666
- // Store uploaded document info with null-safe access
667
- // 📄 Use full_content for complete document analysis, fallback to content_preview
668
- uploadedDocument.value = {
669
- filename: data?.filename ?? file.name,
670
- size: data?.file_size ?? file.size,
671
- documentId: data?.document_id ?? null,
672
- charCount: data?.char_count ?? 0,
673
- privacyInfo: {
674
- use_local: hasSensitiveData,
675
- sensitive_types: sensitiveDataTypes
676
- },
677
- parsedText: data?.full_content ?? data?.content_preview ?? ''
678
- }
679
-
680
- uploadProgress.value = 100
681
- uploadStatus.value = 'Upload complete!'
682
-
683
- // 🔧 FIX: Add success message to chat with null-safe access
684
- const privacyStatus = hasSensitiveData
685
- ? '🔒 Sensible Daten erkannt - wird intern verarbeitet'
686
- : '✅ Keine sensiblen Daten'
687
-
688
- addMessage(
689
- `📄 Dokument hochgeladen: ${uploadedDocument.value.filename} (${formatFileSize(uploadedDocument.value.size)})\n${privacyStatus}`,
690
- 'system',
691
- 'System',
692
- 'coordinator',
693
- { role: 'Upload Success' }
694
- )
695
-
696
- // Clear file input
697
- event.target.value = ''
698
-
699
- // Show privacy toast if sensitive data detected
700
- if (hasSensitiveData) {
701
- showPrivacyInfo.value = true
702
- setTimeout(() => {
703
- showPrivacyInfo.value = false
704
- }, 3000)
705
- }
706
-
707
- } catch (error) {
708
- console.error('Document Upload Error:', error)
709
- addMessage(
710
- `❌ Upload fehlgeschlagen: ${error.message}`,
711
- 'system',
712
- 'System',
713
- 'error',
714
- { role: 'Upload Error' }
715
- )
716
- event.target.value = '' // Reset input
717
- } finally {
718
- isUploading.value = false
719
- uploadProgress.value = 0
720
- uploadStatus.value = ''
721
- }
722
- }
723
-
724
- const removeDocument = () => {
725
- uploadedDocument.value = null
726
- addMessage(
727
- '🗑️ Dokument entfernt',
728
- 'system',
729
- 'System',
730
- 'coordinator',
731
- { role: 'Document Removed' }
732
- )
733
- }
734
-
735
- const formatFileSize = (bytes) => {
736
- if (bytes === 0) return '0 Bytes'
737
- const k = 1024
738
- const sizes = ['Bytes', 'KB', 'MB', 'GB']
739
- const i = Math.floor(Math.log(bytes) / Math.log(k))
740
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
741
- }
742
-
743
  const checkPrivacy = async (message) => {
744
  // Simple client-side privacy detection
745
  const sensitiveKeywords = {
@@ -942,68 +741,10 @@ Jane Alesi ist deine Master Coordinatorin. Sie analysiert deine Anfragen und del
942
  addMessage(helpMessage, 'system', 'System', 'coordinator', { role: 'Help & Documentation' })
943
  }
944
 
945
- // Load chat history from PostgreSQL
946
- const loadChatHistory = async () => {
947
- try {
948
- const history = await saapApi.getMultiAgentHistory('default', 5)
949
- if (history.messages && history.messages.length > 0) {
950
- chatMessages.value = history.messages.map(msg => ({
951
- id: msg.id || Date.now() + Math.random(),
952
- content: msg.content,
953
- agent: msg.agent,
954
- agentName: msg.agentName,
955
- type: msg.type,
956
- timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
957
- role: msg.role,
958
- processingTime: msg.processingTime,
959
- cost: msg.cost,
960
- provider: msg.provider
961
- }))
962
- scrollToBottom()
963
- console.log(`📜 Loaded ${history.messages.length} messages from history`)
964
- } else {
965
- // No history, show welcome message
966
- clearChat()
967
- }
968
- } catch (error) {
969
- console.error('Failed to load chat history:', error)
970
- clearChat()
971
- }
972
- }
973
-
974
- // Save message to PostgreSQL
975
- const saveMessageToDb = async (message) => {
976
- try {
977
- await saapApi.saveMultiAgentMessage({
978
- session_id: 'default',
979
- content: message.content,
980
- agent: message.agent,
981
- agentName: message.agentName,
982
- type: message.type,
983
- role: message.role,
984
- provider: message.provider,
985
- processingTime: message.processingTime,
986
- cost: message.cost
987
- })
988
- } catch (error) {
989
- console.error('Failed to save message:', error)
990
- }
991
- }
992
-
993
- // Watch for modal visibility changes - load history when modal opens
994
- const isVisibleRef = toRef(props, 'isVisible')
995
- watch(isVisibleRef, (newValue, oldValue) => {
996
- if (newValue && !oldValue) {
997
- // Modal just opened - load chat history
998
- console.log('📂 Modal opened - loading chat history...')
999
- loadChatHistory()
1000
- }
1001
- })
1002
-
1003
  // Lifecycle
1004
  onMounted(() => {
1005
- if (props.isVisible) {
1006
- loadChatHistory()
1007
  }
1008
  })
1009
 
@@ -1022,10 +763,6 @@ Jane Alesi ist deine Master Coordinatorin. Sie analysiert deine Anfragen und del
1022
  availableSpecialists,
1023
  selectedProvider,
1024
  showPrivacyInfo,
1025
- uploadedDocument,
1026
- isUploading,
1027
- uploadProgress,
1028
- uploadStatus,
1029
 
1030
  // Computed
1031
  canSendMessage,
@@ -1043,10 +780,7 @@ Jane Alesi ist deine Master Coordinatorin. Sie analysiert deine Anfragen und del
1043
  showSystemStatus,
1044
  showPerformanceReport,
1045
  showListAgents,
1046
- showHelp,
1047
- handleFileUpload,
1048
- removeDocument,
1049
- formatFileSize
1050
  }
1051
  }
1052
  }
@@ -1555,174 +1289,12 @@ Jane Alesi ist deine Master Coordinatorin. Sie analysiert deine Anfragen und del
1555
  flex-shrink: 0;
1556
  }
1557
 
1558
- /* 📄 Document Upload Styles */
1559
- .uploaded-document {
1560
- display: flex;
1561
- align-items: center;
1562
- justify-content: space-between;
1563
- padding: 12px;
1564
- margin-bottom: 12px;
1565
- background: #f0f9ff;
1566
- border: 1px solid #38bdf8;
1567
- border-radius: 8px;
1568
- }
1569
-
1570
- .document-info {
1571
- display: flex;
1572
- align-items: center;
1573
- gap: 12px;
1574
- flex: 1;
1575
- }
1576
-
1577
- .document-icon {
1578
- font-size: 24px;
1579
- }
1580
-
1581
- .document-details {
1582
- display: flex;
1583
- flex-direction: column;
1584
- gap: 4px;
1585
- }
1586
-
1587
- .document-name {
1588
- font-size: 14px;
1589
- font-weight: 600;
1590
- color: #0c4a6e;
1591
- }
1592
-
1593
- .document-size {
1594
- font-size: 12px;
1595
- color: #6b7280;
1596
- }
1597
-
1598
- .document-privacy {
1599
- display: flex;
1600
- align-items: center;
1601
- }
1602
-
1603
- .privacy-indicator {
1604
- padding: 4px 10px;
1605
- border-radius: 6px;
1606
- font-size: 11px;
1607
- font-weight: 600;
1608
- }
1609
-
1610
- .privacy-indicator.sensitive {
1611
- background: #dcfce7;
1612
- color: #16a34a;
1613
- }
1614
-
1615
- .privacy-indicator.safe {
1616
- background: #dbeafe;
1617
- color: #2563eb;
1618
- }
1619
-
1620
- .remove-document {
1621
- padding: 6px;
1622
- background: #fee2e2;
1623
- border: 1px solid #fca5a5;
1624
- border-radius: 6px;
1625
- color: #dc2626;
1626
- cursor: pointer;
1627
- transition: all 0.2s;
1628
- }
1629
-
1630
- .remove-document:hover {
1631
- background: #fecaca;
1632
- transform: scale(1.05);
1633
- }
1634
-
1635
- .remove-document svg {
1636
- width: 16px;
1637
- height: 16px;
1638
- }
1639
-
1640
- /* 📤 Upload Progress Styles */
1641
- .upload-progress {
1642
- margin-bottom: 12px;
1643
- padding: 12px;
1644
- background: #f0f9ff;
1645
- border: 1px solid #38bdf8;
1646
- border-radius: 8px;
1647
- }
1648
-
1649
- .progress-bar-container {
1650
- width: 100%;
1651
- height: 8px;
1652
- background: #e0f2fe;
1653
- border-radius: 4px;
1654
- overflow: hidden;
1655
- margin-bottom: 8px;
1656
- }
1657
-
1658
- .progress-bar-fill {
1659
- height: 100%;
1660
- background: linear-gradient(90deg, #3b82f6, #2563eb);
1661
- transition: width 0.3s ease;
1662
- border-radius: 4px;
1663
- }
1664
-
1665
- .progress-text {
1666
- font-size: 12px;
1667
- color: #0c4a6e;
1668
- font-weight: 500;
1669
- text-align: center;
1670
- display: block;
1671
- }
1672
-
1673
- /* Input Group with Buttons */
1674
  .input-group {
1675
  display: flex;
1676
  gap: 12px;
1677
  margin-bottom: 12px;
1678
  }
1679
 
1680
- .input-buttons {
1681
- display: flex;
1682
- flex-direction: column;
1683
- gap: 8px;
1684
- align-self: flex-end;
1685
- }
1686
-
1687
- /* 📎 File Upload Button */
1688
- .file-upload-button {
1689
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
1690
- color: white;
1691
- border: none;
1692
- border-radius: 8px;
1693
- padding: 12px 16px;
1694
- cursor: pointer;
1695
- transition: all 0.2s;
1696
- display: flex;
1697
- align-items: center;
1698
- justify-content: center;
1699
- min-width: 48px;
1700
- }
1701
-
1702
- .file-upload-button:hover:not(.disabled) {
1703
- transform: translateY(-2px);
1704
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1705
- }
1706
-
1707
- .file-upload-button.disabled {
1708
- background: #9ca3af;
1709
- cursor: not-allowed;
1710
- transform: none;
1711
- }
1712
-
1713
- .file-upload-button svg {
1714
- width: 20px;
1715
- height: 20px;
1716
- }
1717
-
1718
- .w-4 {
1719
- width: 1rem;
1720
- }
1721
-
1722
- .h-4 {
1723
- height: 1rem;
1724
- }
1725
-
1726
  .message-input {
1727
  flex: 1;
1728
  border: 2px solid #e5e7eb;
 
123
 
124
  <!-- 🚀 INPUT BEREICH GARANTIERT SICHTBAR -->
125
  <div class="chat-input-section">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  <div class="input-group">
127
  <textarea
128
  v-model="currentMessage"
 
133
  rows="3"
134
  :disabled="isProcessing"
135
  ></textarea>
136
+ <button
137
+ @click="sendMessage"
138
+ :disabled="!canSendMessage"
139
+ class="send-button"
140
+ >
141
+ <svg v-if="!isProcessing" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
142
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
143
+ </svg>
144
+ <div v-else class="spinner"></div>
145
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  </div>
147
 
148
  <div class="input-options">
 
240
  </template>
241
 
242
  <script>
243
+ import { ref, computed, nextTick, onMounted } from 'vue'
244
  import { saapApi } from '../../services/saapApi'
245
 
246
  export default {
 
267
  const selectedProvider = ref('auto')
268
  const showPrivacyInfo = ref(false)
269
 
 
 
 
 
 
 
270
  // Available specialists data
271
  const availableSpecialists = ref([
272
  {
 
399
 
400
  chatMessages.value.push(message)
401
  scrollToBottom()
 
 
 
 
402
  return message
403
  }
404
 
 
445
  provider = null // Let backend decide
446
  }
447
 
 
 
 
 
 
 
 
 
 
 
448
  // Call multi-agent API with provider selection
449
+ const response = await saapApi.multiAgentChat(message, {
450
  user_context: {
451
  timestamp: new Date().toISOString().split('.')[0] + 'Z', // 🔧 FIX: Remove milliseconds for Python compatibility
452
+ priority: taskPriority.value
 
 
453
  },
454
  preferred_agent: preferredAgent.value || null,
455
  task_priority: taskPriority.value,
456
  provider: provider,
457
+ privacy_mode: forceProvider || selectedProvider.value
 
 
 
 
 
 
458
  })
459
 
460
  // Update coordination chain based on backend response
 
539
  }
540
  }
541
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  const checkPrivacy = async (message) => {
543
  // Simple client-side privacy detection
544
  const sensitiveKeywords = {
 
741
  addMessage(helpMessage, 'system', 'System', 'coordinator', { role: 'Help & Documentation' })
742
  }
743
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  // Lifecycle
745
  onMounted(() => {
746
+ if (chatMessages.value.length === 0) {
747
+ clearChat()
748
  }
749
  })
750
 
 
763
  availableSpecialists,
764
  selectedProvider,
765
  showPrivacyInfo,
 
 
 
 
766
 
767
  // Computed
768
  canSendMessage,
 
780
  showSystemStatus,
781
  showPerformanceReport,
782
  showListAgents,
783
+ showHelp
 
 
 
784
  }
785
  }
786
  }
 
1289
  flex-shrink: 0;
1290
  }
1291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1292
  .input-group {
1293
  display: flex;
1294
  gap: 12px;
1295
  margin-bottom: 12px;
1296
  }
1297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1298
  .message-input {
1299
  flex: 1;
1300
  border: 2px solid #e5e7eb;
frontend/src/composables/useApi.ts CHANGED
@@ -63,20 +63,8 @@ interface SystemStatus {
63
  timestamp: string
64
  }
65
 
66
- // 🌐 Environment-aware API Configuration (HuggingFace compatible)
67
- const getApiBaseUrl = () => {
68
- if (typeof window !== 'undefined') {
69
- if (window.location.hostname.includes('hf.space')) {
70
- return ''; // Relative URLs for HuggingFace
71
- }
72
- if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
73
- return 'http://localhost:8000';
74
- }
75
- }
76
- return ''; // Fallback: relative URLs
77
- }
78
-
79
- const API_BASE_URL = getApiBaseUrl()
80
 
81
  const api = axios.create({
82
  baseURL: API_BASE_URL,
@@ -86,8 +74,6 @@ const api = axios.create({
86
  },
87
  })
88
 
89
- console.log('🔧 useApi Configuration:', { API_BASE_URL, hostname: typeof window !== 'undefined' ? window.location.hostname : 'SSR' })
90
-
91
  // Request/Response interceptors
92
  api.interceptors.request.use(
93
  (config) => {
@@ -281,4 +267,4 @@ export const useApi = () => {
281
  }
282
 
283
  // Export types for use in components
284
- export type { SaapAgent, ChatMessage, SystemStatus }
 
63
  timestamp: string
64
  }
65
 
66
+ // API Configuration
67
+ const API_BASE_URL = 'http://localhost:8000'
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  const api = axios.create({
70
  baseURL: API_BASE_URL,
 
74
  },
75
  })
76
 
 
 
77
  // Request/Response interceptors
78
  api.interceptors.request.use(
79
  (config) => {
 
267
  }
268
 
269
  // Export types for use in components
270
+ export type { SaapAgent, ChatMessage, SystemStatus }
frontend/src/composables/useWebSocket.ts CHANGED
@@ -1,4 +1,4 @@
1
- /**
2
  * SAAP WebSocket Composable - Real-time Updates
3
  * WebSocket connection for live agent status and message updates
4
  */
@@ -38,28 +38,12 @@ export const useWebSocket = () => {
38
  const messages = ref<WebSocketMessage[]>([])
39
  const lastMessage = ref<WebSocketMessage | null>(null)
40
 
41
- // 🌐 Environment-aware WebSocket URL (HuggingFace compatible)
42
- const getWebSocketUrl = () => {
43
- if (typeof window !== 'undefined') {
44
- const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
45
- if (window.location.hostname.includes('hf.space')) {
46
- return `${wsProtocol}//${window.location.host}/ws`;
47
- }
48
- if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
49
- return 'ws://localhost:8000/ws';
50
- }
51
- return `${wsProtocol}//${window.location.host}/ws`;
52
- }
53
- return 'ws://localhost:8000/ws'; // Fallback for SSR
54
- }
55
-
56
- const WS_URL = getWebSocketUrl()
57
  const RECONNECT_DELAY = 3000
58
  const MAX_RECONNECT_ATTEMPTS = 5
59
  let reconnectAttempts = 0
60
 
61
- console.log('🔧 useWebSocket Configuration:', { WS_URL, hostname: typeof window !== 'undefined' ? window.location.hostname : 'SSR' })
62
-
63
  // Event listeners
64
  const eventListeners = new Map<string, Set<Function>>()
65
 
@@ -350,4 +334,4 @@ export const useWebSocket = () => {
350
  }
351
 
352
  // Export types
353
- export type { WebSocketMessage, ConnectionStatus }
 
1
+ /**
2
  * SAAP WebSocket Composable - Real-time Updates
3
  * WebSocket connection for live agent status and message updates
4
  */
 
38
  const messages = ref<WebSocketMessage[]>([])
39
  const lastMessage = ref<WebSocketMessage | null>(null)
40
 
41
+ // WebSocket URL (matching FastAPI WebSocket endpoint)
42
+ const WS_URL = 'ws://localhost:8000/ws'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  const RECONNECT_DELAY = 3000
44
  const MAX_RECONNECT_ATTEMPTS = 5
45
  let reconnectAttempts = 0
46
 
 
 
47
  // Event listeners
48
  const eventListeners = new Map<string, Set<Function>>()
49
 
 
334
  }
335
 
336
  // Export types
337
+ export type { WebSocketMessage, ConnectionStatus }
frontend/src/main.js CHANGED
@@ -11,33 +11,11 @@ import App from './App.vue'
11
  // Import global styles
12
  import './assets/css/main.css'
13
 
14
- // 🌐 Environment-aware Configuration (HuggingFace compatible)
15
- const getApiBaseUrl = () => {
16
- if (window.location.hostname.includes('hf.space')) {
17
- return '/api/v1';
18
- }
19
- if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
20
- return 'http://localhost:8000';
21
- }
22
- return ''; // Relative for production
23
- }
24
-
25
- const getWebSocketUrl = () => {
26
- const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
27
- if (window.location.hostname.includes('hf.space')) {
28
- return `${wsProtocol}//${window.location.host}/ws`;
29
- }
30
- if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
31
- return 'ws://localhost:8000/ws';
32
- }
33
- return `${wsProtocol}//${window.location.host}/ws`;
34
- }
35
-
36
  // SAAP Configuration
37
  const SAAP_CONFIG = {
38
  version: '1.0.0',
39
- apiBaseUrl: getApiBaseUrl(),
40
- websocketUrl: getWebSocketUrl(),
41
  debug: import.meta.env.DEV,
42
  buildDate: new Date().toLocaleDateString('de-DE')
43
  }
@@ -81,4 +59,4 @@ console.log('📊 Configuration:', SAAP_CONFIG)
81
  if (SAAP_CONFIG.debug) {
82
  window.__SAAP_APP__ = app
83
  window.__SAAP_CONFIG__ = SAAP_CONFIG
84
- }
 
11
  // Import global styles
12
  import './assets/css/main.css'
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  // SAAP Configuration
15
  const SAAP_CONFIG = {
16
  version: '1.0.0',
17
+ apiBaseUrl: 'http://localhost:8000',
18
+ websocketUrl: 'ws://localhost:8000/ws',
19
  debug: import.meta.env.DEV,
20
  buildDate: new Date().toLocaleDateString('de-DE')
21
  }
 
59
  if (SAAP_CONFIG.debug) {
60
  window.__SAAP_APP__ = app
61
  window.__SAAP_CONFIG__ = SAAP_CONFIG
62
+ }
frontend/src/router/index.js CHANGED
@@ -9,8 +9,6 @@ import DashboardSimple from '@/views/DashboardSimple.vue'
9
  import AgentDetails from '@/views/AgentDetails.vue'
10
  import Settings from '@/views/Settings.vue'
11
  import About from '@/views/About.vue'
12
- import Costs from '@/views/Costs.vue'
13
- import Monitoring from '@/views/Monitoring.vue'
14
 
15
  const router = createRouter({
16
  history: createWebHistory(import.meta.env.BASE_URL),
@@ -52,24 +50,6 @@ const router = createRouter({
52
  requiresAuth: false
53
  }
54
  },
55
- {
56
- path: '/costs',
57
- name: 'Costs',
58
- component: Costs,
59
- meta: {
60
- title: 'Cost Analytics',
61
- requiresAuth: false
62
- }
63
- },
64
- {
65
- path: '/monitoring',
66
- name: 'Monitoring',
67
- component: Monitoring,
68
- meta: {
69
- title: 'System Monitoring',
70
- requiresAuth: false
71
- }
72
- },
73
  {
74
  path: '/about',
75
  name: 'About',
 
9
  import AgentDetails from '@/views/AgentDetails.vue'
10
  import Settings from '@/views/Settings.vue'
11
  import About from '@/views/About.vue'
 
 
12
 
13
  const router = createRouter({
14
  history: createWebHistory(import.meta.env.BASE_URL),
 
50
  requiresAuth: false
51
  }
52
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  {
54
  path: '/about',
55
  name: 'About',
frontend/src/services/saapApi.js CHANGED
@@ -138,22 +138,6 @@ const saapApi = {
138
  return response.data;
139
  },
140
 
141
- // Multi-Agent Chat History (PostgreSQL persistence)
142
- async getMultiAgentHistory(sessionId = 'default', limit = 5) {
143
- const response = await apiClient.get(`/multi-agent/history?session_id=${sessionId}&limit=${limit}`);
144
- return response.data;
145
- },
146
-
147
- async saveMultiAgentMessage(message) {
148
- const response = await apiClient.post('/multi-agent/history/message', message);
149
- return response.data;
150
- },
151
-
152
- async clearMultiAgentHistory(sessionId = 'default') {
153
- const response = await apiClient.delete(`/multi-agent/history?session_id=${sessionId}`);
154
- return response.data;
155
- },
156
-
157
  // Templates
158
  async getAgentTemplates() {
159
  const response = await apiClient.get('/templates/agents');
@@ -170,69 +154,6 @@ const saapApi = {
170
  const response = await apiClient.get('/health');
171
  return response.data;
172
  },
173
-
174
- // Document Upload
175
- async uploadDocument(file, userMessage = null) {
176
- const formData = new FormData();
177
- formData.append('file', file);
178
- if (userMessage) {
179
- formData.append('user_message', userMessage);
180
- }
181
-
182
- const response = await apiClient.post('/documents/upload', formData, {
183
- headers: {
184
- 'Content-Type': 'multipart/form-data',
185
- },
186
- timeout: 60000, // 60 seconds for file upload
187
- });
188
- return response.data;
189
- },
190
-
191
- // Document Analysis
192
- async analyzeDocument(file) {
193
- const formData = new FormData();
194
- formData.append('file', file);
195
-
196
- const response = await apiClient.post('/documents/analyze', formData, {
197
- headers: {
198
- 'Content-Type': 'multipart/form-data',
199
- },
200
- timeout: 60000,
201
- });
202
- return response.data;
203
- },
204
-
205
- // Supported Document Formats
206
- async getSupportedFormats() {
207
- const response = await apiClient.get('/documents/supported-formats');
208
- return response.data;
209
- },
210
-
211
- // Cost Tracking & Analytics
212
- async getCostSummary(hours = 24) {
213
- const response = await apiClient.get(`/cost/summary?hours=${hours}`);
214
- return response.data;
215
- },
216
-
217
- async getCostAnalytics(hours = 24) {
218
- const response = await apiClient.get(`/cost/analytics?hours=${hours}`);
219
- return response.data;
220
- },
221
-
222
- async getBudgetStatus() {
223
- const response = await apiClient.get('/cost/budget');
224
- return response.data;
225
- },
226
-
227
- async getPerformanceBenchmarks(hours = 24) {
228
- const response = await apiClient.get(`/cost/benchmarks?hours=${hours}`);
229
- return response.data;
230
- },
231
-
232
- async getCostReport(hours = 24) {
233
- const response = await apiClient.get(`/cost/report?hours=${hours}`);
234
- return response.data;
235
- },
236
  };
237
 
238
  // WebSocket connection for real-time updates
 
138
  return response.data;
139
  },
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  // Templates
142
  async getAgentTemplates() {
143
  const response = await apiClient.get('/templates/agents');
 
154
  const response = await apiClient.get('/health');
155
  return response.data;
156
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  };
158
 
159
  // WebSocket connection for real-time updates
frontend/vite.config.js CHANGED
@@ -19,21 +19,7 @@ export default defineConfig({
19
  '/api': {
20
  target: 'http://localhost:8000',
21
  changeOrigin: true,
22
- secure: false,
23
- configure: (proxy) => {
24
- proxy.on('error', (err) => {
25
- console.log('Proxy error:', err);
26
- });
27
- proxy.on('proxyReq', (proxyReq, req) => {
28
- console.log('Proxying:', req.method, req.url, '→', 'http://localhost:8000' + req.url);
29
- });
30
- }
31
- },
32
- // WebSocket proxy
33
- '/ws': {
34
- target: 'ws://localhost:8000',
35
- ws: true,
36
- changeOrigin: true
37
  }
38
  }
39
  },
@@ -49,4 +35,4 @@ export default defineConfig({
49
  }
50
  }
51
  }
52
- })
 
19
  '/api': {
20
  target: 'http://localhost:8000',
21
  changeOrigin: true,
22
+ rewrite: (path) => path.replace(/^\/api/, '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
  }
25
  },
 
35
  }
36
  }
37
  }
38
+ })