Sync with GitHub: docs reorganization
Browse files- backend/agent.py +343 -0
- backend/agent_init_fix.py +45 -0
- backend/agent_manager.py +989 -0
- backend/agent_manager_database_enhanced.py +328 -0
- backend/agent_manager_enhanced.py +651 -0
- backend/agent_manager_fixed.py +978 -0
- backend/agent_manager_hybrid.py +575 -0
- backend/agent_manager_hybrid_fixed.py +494 -0
- backend/agent_schema.json +267 -0
- backend/agent_schema.py +784 -0
- backend/agent_templates.json +144 -0
- backend/api/cost_tracking.py +241 -375
- backend/api/multi_agent_endpoints.py +12 -242
- backend/config/settings.py +5 -4
- backend/connection.py +415 -0
- backend/cost_efficiency_logger.py +478 -0
- backend/database/connection.py +1 -6
- backend/database/models.py +0 -92
- backend/database_service.py +382 -0
- backend/main.py +15 -204
- backend/multi_agent_coordinator.py +606 -0
- backend/openrouter_integration.py +252 -0
- backend/privacy_detector.py +288 -0
- backend/requirements.txt +2 -9
- backend/service.py +320 -0
- backend/services/privacy_detector.py +0 -94
- backend/settings.py +482 -0
- backend/test_colossus_integration.py +259 -0
- backend/websocket_manager.py +361 -0
- frontend/dist/assets/icons-l0sNRNKZ.js +0 -2
- frontend/dist/assets/icons-l0sNRNKZ.js.map +0 -1
- frontend/dist/assets/index-C4sh0Su3.css +0 -0
- frontend/dist/assets/index-RrLk96-w.js +0 -0
- frontend/dist/assets/index-RrLk96-w.js.map +0 -0
- frontend/dist/assets/vendor-Ctwcxhgt.js +0 -26
- frontend/dist/assets/vendor-Ctwcxhgt.js.map +0 -0
- frontend/dist/index.html +0 -16
- frontend/nginx.conf +1 -1
- frontend/src/App.vue +9 -116
- frontend/src/components/modals/MultiAgentChatModal.vue +17 -445
- frontend/src/composables/useApi.ts +3 -17
- frontend/src/composables/useWebSocket.ts +4 -20
- frontend/src/main.js +3 -25
- frontend/src/router/index.js +0 -20
- frontend/src/services/saapApi.js +0 -79
- 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
|
| 3 |
-
|
| 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
|
| 13 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
logger = logging.getLogger(__name__)
|
| 16 |
router = APIRouter(prefix="/api/v1/cost", tags=["Cost Tracking"])
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
"""
|
| 31 |
-
Get
|
| 32 |
|
| 33 |
-
Returns:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
| 37 |
"""
|
| 38 |
try:
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 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 |
-
|
| 69 |
-
|
| 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 |
-
#
|
| 79 |
by_provider = {}
|
| 80 |
-
for cost in
|
| 81 |
-
provider =
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 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 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 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 |
-
|
| 161 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 162 |
-
|
| 163 |
|
| 164 |
-
@router.get("/
|
| 165 |
-
async def
|
| 166 |
-
|
| 167 |
-
):
|
| 168 |
"""
|
| 169 |
-
Get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
"""
|
| 171 |
try:
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 229 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 230 |
-
|
| 231 |
|
| 232 |
-
@router.get("/
|
| 233 |
-
async def
|
| 234 |
-
hours: int = Query(
|
| 235 |
-
|
| 236 |
-
):
|
| 237 |
"""
|
| 238 |
-
Get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
"""
|
| 240 |
try:
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 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 |
-
|
| 267 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 268 |
-
|
| 269 |
|
| 270 |
-
@router.get("/
|
| 271 |
-
async def
|
| 272 |
-
hours: int = Query(default=24, ge=1, le=8760),
|
| 273 |
-
db: AsyncSession = Depends(get_db)
|
| 274 |
-
):
|
| 275 |
"""
|
| 276 |
-
Get
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
"""
|
| 279 |
try:
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
)
|
| 285 |
-
costs = result.scalars().all()
|
| 286 |
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 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 |
-
"
|
| 313 |
-
"
|
| 314 |
-
|
| 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 |
-
|
| 340 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 341 |
-
|
| 342 |
|
| 343 |
-
@router.post("/
|
| 344 |
-
async def
|
| 345 |
-
cost_data: Dict,
|
| 346 |
-
db: AsyncSession = Depends(get_db)
|
| 347 |
-
):
|
| 348 |
"""
|
| 349 |
-
|
|
|
|
|
|
|
| 350 |
"""
|
| 351 |
try:
|
| 352 |
-
|
| 353 |
-
|
| 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 |
-
|
|
|
|
| 369 |
|
| 370 |
return {
|
| 371 |
-
"
|
| 372 |
-
"
|
| 373 |
-
"
|
| 374 |
}
|
| 375 |
|
| 376 |
except Exception as e:
|
| 377 |
-
|
| 378 |
-
await db.rollback()
|
| 379 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 380 |
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
format: str = Query(default="json", regex="^(json|csv)$"),
|
| 386 |
-
db: AsyncSession = Depends(get_db)
|
| 387 |
-
):
|
| 388 |
"""
|
| 389 |
-
|
|
|
|
|
|
|
| 390 |
"""
|
| 391 |
try:
|
| 392 |
-
|
| 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 |
-
"
|
| 430 |
-
"
|
| 431 |
-
"
|
| 432 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 433 |
}
|
| 434 |
|
| 435 |
except Exception as e:
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 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:
|
| 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 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
| 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(
|
| 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 |
-
|
| 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&>(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/
|
| 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 |
-
<
|
| 8 |
-
<
|
| 9 |
-
|
| 10 |
-
|
| 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 -
|
| 75 |
<main class="saap-main">
|
| 76 |
-
<
|
| 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
|
| 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-
|
| 355 |
-
padding: var(--saap-space-
|
| 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 |
-
<
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
<
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 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
|
| 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(
|
| 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 (
|
| 1006 |
-
|
| 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 |
-
//
|
| 67 |
-
const
|
| 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 |
-
//
|
| 42 |
-
const
|
| 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:
|
| 40 |
-
websocketUrl:
|
| 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 |
-
|
| 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 |
+
})
|