- matrix-gateway: POST /internal/matrix/presence/online endpoint - usePresenceHeartbeat hook with activity tracking - Auto away after 5 min inactivity - Offline on page close/visibility change - Integrated in MatrixChatRoom component
475 lines
16 KiB
Python
475 lines
16 KiB
Python
"""
|
|
Agent Cabinet Service - FastAPI сервіс для кабінетів агентів
|
|
Надає API для метрик, CrewAI команд та управління оркестраторами
|
|
"""
|
|
import os
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional, Any
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, HTTPException, Query, Depends, Body
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
|
|
# Налаштування логування
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ============================================================================
|
|
# Pydantic Models
|
|
# ============================================================================
|
|
|
|
class AgentMetrics(BaseModel):
|
|
agent_id: str
|
|
agent_name: str
|
|
status: str # 'active' | 'inactive' | 'error'
|
|
uptime_hours: float
|
|
total_requests: int
|
|
successful_requests: int
|
|
failed_requests: int
|
|
avg_response_time_ms: float
|
|
last_active: str
|
|
model: str
|
|
model_backend: str
|
|
node: str
|
|
is_orchestrator: bool
|
|
team_size: int
|
|
workspace: Optional[str] = None
|
|
workspace_info: Optional[Dict[str, Any]] = None
|
|
sub_agents: Optional[List[Dict[str, str]]] = None
|
|
|
|
class CrewAICrew(BaseModel):
|
|
id: str
|
|
name: str
|
|
agents: List[Dict[str, str]]
|
|
tasks: List[Dict[str, str]]
|
|
status: str # 'active' | 'inactive'
|
|
created_at: str
|
|
|
|
class BecomeOrchestratorRequest(BaseModel):
|
|
agent_id: str
|
|
|
|
class BecomeOrchestratorResponse(BaseModel):
|
|
status: str
|
|
agent_id: str
|
|
is_orchestrator: bool
|
|
|
|
# ============================================================================
|
|
# FastAPI App
|
|
# ============================================================================
|
|
|
|
app = FastAPI(
|
|
title="Agent Cabinet Service",
|
|
description="API для кабінетів агентів, метрик та CrewAI команд",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ============================================================================
|
|
# Data Storage (in-memory, можна замінити на базу даних)
|
|
# ============================================================================
|
|
|
|
# Зберігаємо метрики агентів
|
|
agent_metrics_store: Dict[str, AgentMetrics] = {}
|
|
|
|
# Зберігаємо CrewAI команди
|
|
agent_crews_store: Dict[str, List[CrewAICrew]] = {}
|
|
|
|
# Зберігаємо інформацію про оркестраторів
|
|
orchestrators_store: Dict[str, bool] = {}
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
def get_agent_from_router(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Отримати інформацію про агента з DAGI Router
|
|
"""
|
|
try:
|
|
import httpx
|
|
router_url = os.getenv("ROUTER_URL", "http://localhost:9102")
|
|
|
|
# Спробувати отримати інформацію про агента
|
|
# Це залежить від API Router
|
|
# Поки що повертаємо None
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error getting agent from router: {e}")
|
|
return None
|
|
|
|
def get_agent_metrics_from_system(agent_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Зібрати метрики агента з системи
|
|
"""
|
|
# Отримати базову інформацію про агента
|
|
agent_info = get_agent_from_router(agent_id)
|
|
|
|
# Якщо агент вже є в store, використовуємо його метрики
|
|
if agent_id in agent_metrics_store:
|
|
metrics = agent_metrics_store[agent_id]
|
|
# Оновлюємо last_active
|
|
metrics.last_active = datetime.utcnow().isoformat()
|
|
return metrics.dict()
|
|
|
|
# Створюємо базові метрики для нового агента
|
|
default_metrics = {
|
|
"agent_id": agent_id,
|
|
"agent_name": agent_id.replace("_", " ").title(),
|
|
"status": "active",
|
|
"uptime_hours": 0.0,
|
|
"total_requests": 0,
|
|
"successful_requests": 0,
|
|
"failed_requests": 0,
|
|
"avg_response_time_ms": 0.0,
|
|
"last_active": datetime.utcnow().isoformat(),
|
|
"model": "qwen3:8b",
|
|
"model_backend": "ollama",
|
|
"node": "node-1",
|
|
"is_orchestrator": orchestrators_store.get(agent_id, False),
|
|
"team_size": 0,
|
|
"sub_agents": [],
|
|
}
|
|
|
|
# Зберігаємо метрики
|
|
agent_metrics_store[agent_id] = AgentMetrics(**default_metrics)
|
|
|
|
return default_metrics
|
|
|
|
def get_crews_for_agent(agent_id: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Отримати CrewAI команди для агента
|
|
"""
|
|
if agent_id not in agent_crews_store:
|
|
return []
|
|
|
|
return [crew.dict() for crew in agent_crews_store[agent_id]]
|
|
|
|
def create_crew_for_agent(agent_id: str, crew_name: str, agents: List[Dict], tasks: List[Dict]) -> CrewAICrew:
|
|
"""
|
|
Створити нову CrewAI команду для агента
|
|
"""
|
|
crew_id = f"crew-{agent_id}-{len(agent_crews_store.get(agent_id, [])) + 1}"
|
|
|
|
crew = CrewAICrew(
|
|
id=crew_id,
|
|
name=crew_name,
|
|
agents=agents,
|
|
tasks=tasks,
|
|
status="active",
|
|
created_at=datetime.utcnow().isoformat()
|
|
)
|
|
|
|
if agent_id not in agent_crews_store:
|
|
agent_crews_store[agent_id] = []
|
|
|
|
agent_crews_store[agent_id].append(crew)
|
|
|
|
return crew
|
|
|
|
# ============================================================================
|
|
# API Endpoints
|
|
# ============================================================================
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
"""Health check endpoint"""
|
|
return {"status": "healthy", "service": "agent-cabinet-service"}
|
|
|
|
@app.get("/api/agent/{agent_id}/metrics", response_model=AgentMetrics)
|
|
async def get_agent_metrics(agent_id: str):
|
|
"""
|
|
Отримати метрики агента
|
|
|
|
Повертає детальні метрики агента включаючи:
|
|
- Uptime, запити, успішність
|
|
- Статус, модель, backend
|
|
- Інформацію про команду (якщо оркестратор)
|
|
"""
|
|
try:
|
|
metrics_data = get_agent_metrics_from_system(agent_id)
|
|
|
|
# Оновлюємо інформацію про команду якщо агент оркестратор
|
|
if orchestrators_store.get(agent_id, False):
|
|
crews = get_crews_for_agent(agent_id)
|
|
if crews:
|
|
# Отримуємо всіх агентів з команд
|
|
all_sub_agents = []
|
|
for crew in crews:
|
|
all_sub_agents.extend(crew.get("agents", []))
|
|
metrics_data["sub_agents"] = all_sub_agents
|
|
metrics_data["team_size"] = len(all_sub_agents)
|
|
|
|
return AgentMetrics(**metrics_data)
|
|
except Exception as e:
|
|
logger.error(f"Error getting metrics for agent {agent_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/agent/{agent_id}/crews", response_model=List[CrewAICrew])
|
|
async def get_agent_crews(agent_id: str):
|
|
"""
|
|
Отримати CrewAI команди для агента
|
|
|
|
Повертає список всіх створених CrewAI команд для оркестратора
|
|
"""
|
|
if not orchestrators_store.get(agent_id, False):
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="Agent is not an orchestrator. Use /become-orchestrator endpoint first."
|
|
)
|
|
|
|
try:
|
|
crews = get_crews_for_agent(agent_id)
|
|
return [CrewAICrew(**crew) for crew in crews]
|
|
except Exception as e:
|
|
logger.error(f"Error getting crews for agent {agent_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.post("/api/agent/{agent_id}/become-orchestrator", response_model=BecomeOrchestratorResponse)
|
|
async def become_orchestrator(agent_id: str):
|
|
"""
|
|
Перетворити агента на оркестратора
|
|
|
|
Дозволяє агенту:
|
|
- Додавати агентів до команди
|
|
- Створювати CrewAI команди
|
|
- Делегувати завдання
|
|
"""
|
|
try:
|
|
# Перевіряємо чи агент існує
|
|
metrics = get_agent_metrics_from_system(agent_id)
|
|
|
|
# Встановлюємо статус оркестратора
|
|
orchestrators_store[agent_id] = True
|
|
|
|
# Оновлюємо метрики
|
|
if agent_id in agent_metrics_store:
|
|
agent_metrics_store[agent_id].is_orchestrator = True
|
|
|
|
# Створюємо workspace для CrewAI (якщо потрібно)
|
|
workspace_id = f"workspace-{agent_id}"
|
|
if agent_id in agent_metrics_store:
|
|
agent_metrics_store[agent_id].workspace = workspace_id
|
|
|
|
logger.info(f"Agent {agent_id} became orchestrator")
|
|
|
|
return BecomeOrchestratorResponse(
|
|
status="success",
|
|
agent_id=agent_id,
|
|
is_orchestrator=True
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error making agent {agent_id} orchestrator: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
class CreateCrewRequest(BaseModel):
|
|
crew_name: str
|
|
agents: List[Dict[str, str]]
|
|
tasks: List[Dict[str, str]] = []
|
|
|
|
@app.post("/api/agent/{agent_id}/crews/create")
|
|
async def create_crew(
|
|
agent_id: str,
|
|
request: CreateCrewRequest
|
|
):
|
|
"""
|
|
Створити нову CrewAI команду для оркестратора
|
|
"""
|
|
if not orchestrators_store.get(agent_id, False):
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="Agent is not an orchestrator"
|
|
)
|
|
|
|
try:
|
|
crew = create_crew_for_agent(agent_id, request.crew_name, request.agents, request.tasks)
|
|
return {"status": "success", "crew": crew.dict()}
|
|
except Exception as e:
|
|
logger.error(f"Error creating crew for agent {agent_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
class AddSubAgentRequest(BaseModel):
|
|
id: str
|
|
name: str
|
|
role: str
|
|
|
|
@app.post("/api/agent/{agent_id}/add-sub-agent")
|
|
async def add_sub_agent(
|
|
agent_id: str,
|
|
sub_agent: AddSubAgentRequest
|
|
):
|
|
"""
|
|
Додати агента до команди оркестратора
|
|
"""
|
|
if not orchestrators_store.get(agent_id, False):
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="Agent is not an orchestrator"
|
|
)
|
|
|
|
try:
|
|
if agent_id not in agent_metrics_store:
|
|
get_agent_metrics_from_system(agent_id)
|
|
|
|
metrics = agent_metrics_store[agent_id]
|
|
|
|
if not metrics.sub_agents:
|
|
metrics.sub_agents = []
|
|
|
|
# Перевіряємо чи агент вже в команді
|
|
if any(agent["id"] == sub_agent.id for agent in metrics.sub_agents):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Agent already in team"
|
|
)
|
|
|
|
metrics.sub_agents.append({
|
|
"id": sub_agent.id,
|
|
"name": sub_agent.name,
|
|
"role": sub_agent.role
|
|
})
|
|
metrics.team_size = len(metrics.sub_agents)
|
|
|
|
return {"status": "success", "team_size": metrics.team_size}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error adding sub-agent to {agent_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.post("/api/agent/{agent_id}/remove-sub-agent")
|
|
async def remove_sub_agent(
|
|
agent_id: str,
|
|
sub_agent_id: str = Query(..., description="ID агента для видалення")
|
|
):
|
|
"""
|
|
Видалити агента з команди оркестратора
|
|
"""
|
|
if not orchestrators_store.get(agent_id, False):
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="Agent is not an orchestrator"
|
|
)
|
|
|
|
try:
|
|
if agent_id not in agent_metrics_store:
|
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
|
|
metrics = agent_metrics_store[agent_id]
|
|
|
|
if not metrics.sub_agents:
|
|
raise HTTPException(status_code=400, detail="Team is empty")
|
|
|
|
# Видаляємо агента
|
|
metrics.sub_agents = [
|
|
agent for agent in metrics.sub_agents
|
|
if agent.get("id") != sub_agent_id
|
|
]
|
|
metrics.team_size = len(metrics.sub_agents)
|
|
|
|
return {"status": "success", "team_size": metrics.team_size}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error removing sub-agent from {agent_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
class ChatRequest(BaseModel):
|
|
message: str
|
|
|
|
@app.post("/api/agent/{agent_id}/chat")
|
|
async def chat_with_agent(
|
|
agent_id: str,
|
|
request: ChatRequest
|
|
):
|
|
"""
|
|
Надіслати повідомлення агенту через DAGI Router
|
|
"""
|
|
try:
|
|
import httpx
|
|
|
|
router_url = os.getenv("ROUTER_URL", "http://localhost:9102")
|
|
|
|
# Формуємо запит до DAGI Router
|
|
payload = {
|
|
"agent": agent_id,
|
|
"mode": "chat",
|
|
"message": request.message,
|
|
"payload": {
|
|
"message": request.message
|
|
}
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.post(
|
|
f"{router_url}/v1/chat/completions",
|
|
json=payload
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
# Оновлюємо метрики
|
|
if agent_id in agent_metrics_store:
|
|
metrics = agent_metrics_store[agent_id]
|
|
metrics.total_requests += 1
|
|
metrics.successful_requests += 1
|
|
metrics.last_active = datetime.utcnow().isoformat()
|
|
|
|
return {
|
|
"status": "success",
|
|
"reply": data.get("content", ""),
|
|
"agent_id": agent_id
|
|
}
|
|
except httpx.HTTPStatusError as e:
|
|
# Оновлюємо метрики (помилка)
|
|
if agent_id in agent_metrics_store:
|
|
metrics = agent_metrics_store[agent_id]
|
|
metrics.total_requests += 1
|
|
metrics.failed_requests += 1
|
|
|
|
logger.error(f"Router HTTP error: {e}")
|
|
raise HTTPException(
|
|
status_code=e.response.status_code,
|
|
detail=f"Router error: {e.response.text}"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error chatting with agent {agent_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# ============================================================================
|
|
# Initialization
|
|
# ============================================================================
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
"""Ініціалізація при запуску"""
|
|
logger.info("Agent Cabinet Service starting up...")
|
|
|
|
# Завантажуємо збережені дані (якщо є)
|
|
# Можна додати завантаження з бази даних
|
|
|
|
logger.info("Agent Cabinet Service ready")
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown():
|
|
"""Очищення при зупинці"""
|
|
logger.info("Agent Cabinet Service shutting down...")
|
|
|
|
# Зберігаємо дані (якщо потрібно)
|
|
# Можна додати збереження в базу даних
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8898)
|
|
|