kairo-code 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kairo_code-0.1.0 → kairo_code-0.2.0}/PKG-INFO +1 -1
- kairo_code-0.2.0/kairo/backend/api/agents.py +415 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/app.py +84 -4
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/config.py +4 -2
- kairo_code-0.2.0/kairo/backend/models/agent.py +244 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/api_key.py +4 -1
- kairo_code-0.2.0/kairo/backend/models/task.py +31 -0
- kairo_code-0.2.0/kairo/backend/models/user_provider_key.py +26 -0
- kairo_code-0.2.0/kairo/backend/schemas/agent.py +289 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/api_key.py +3 -0
- kairo_code-0.2.0/kairo/backend/services/agent/__init__.py +52 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_alerts_evaluation_service.py +224 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_alerts_service.py +201 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_commands_service.py +142 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_crud_service.py +150 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_events_service.py +103 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_heartbeat_service.py +207 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_metrics_rollup_service.py +248 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_metrics_service.py +259 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_service.py +315 -0
- kairo_code-0.2.0/kairo/backend/services/agent/agent_setup_service.py +180 -0
- kairo_code-0.2.0/kairo/backend/services/agent/constants.py +28 -0
- kairo_code-0.2.0/kairo/backend/services/agent_service.py +23 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/api_key_service.py +23 -3
- kairo_code-0.2.0/kairo/backend/services/byok_service.py +204 -0
- kairo_code-0.2.0/kairo/backend/services/chat_service.py +836 -0
- kairo_code-0.2.0/kairo/backend/services/deep_search_service.py +159 -0
- kairo_code-0.2.0/kairo/backend/services/email_service.py +454 -0
- kairo_code-0.2.0/kairo/backend/services/few_shot_service.py +223 -0
- kairo_code-0.2.0/kairo/backend/services/post_processor.py +261 -0
- kairo_code-0.2.0/kairo/backend/services/rag_service.py +150 -0
- kairo_code-0.2.0/kairo/backend/services/task_service.py +119 -0
- kairo_code-0.2.0/kairo/backend/tests/__init__.py +1 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/__init__.py +1 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/__init__.py +1 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/conftest.py +389 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_alerts.py +802 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_commands.py +456 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_crud.py +455 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_events.py +415 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_heartbeat.py +520 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_metrics.py +587 -0
- kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_setup.py +349 -0
- kairo_code-0.2.0/kairo/migrations/versions/010_agent_dashboard.py +246 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/PKG-INFO +1 -1
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/SOURCES.txt +35 -1
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/top_level.txt +2 -0
- kairo_code-0.2.0/kairo_migrations/env.py +92 -0
- kairo_code-0.2.0/kairo_migrations/versions/001_add_agent_dashboard_extensions.py +450 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/pyproject.toml +1 -1
- kairo_code-0.1.0/kairo/backend/api/agents.py +0 -94
- kairo_code-0.1.0/kairo/backend/models/agent.py +0 -30
- kairo_code-0.1.0/kairo/backend/schemas/agent.py +0 -42
- kairo_code-0.1.0/kairo/backend/services/agent_service.py +0 -107
- kairo_code-0.1.0/kairo/backend/services/chat_service.py +0 -501
- kairo_code-0.1.0/kairo/backend/services/email_service.py +0 -55
- {kairo_code-0.1.0 → kairo_code-0.2.0}/image-service/main.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/infra/chat/app/main.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/audit.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/content.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/incidents.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/stats.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/system.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/users.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/api_keys.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/billing.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/chat.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/conversations.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/device_auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/files.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/health.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/images.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/openai_compat.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/projects.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/usage.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/webhooks.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/admin_auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/api_key_auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/database.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/dependencies.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/logging.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/rate_limit.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/security.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/api_usage.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/audit_log.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/conversation.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/device_code.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/feature_flag.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/image_generation.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/incident.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/project.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/uptime_record.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/usage.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/user.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/audit.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/content.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/stats.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/system.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/users.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/chat.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/conversation.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/device_auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/image.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/openai_compat.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/project.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/status.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/usage.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/audit_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/content_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/incident_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/stats_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/system_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/user_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/api_usage_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/auth_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/conversation_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/device_auth_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/image_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/llm_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/project_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/status_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/stripe_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/usage_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/web_search_service.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/env.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/001_initial.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/002_usage_tracking_and_indexes.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/003_username_to_email.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/004_add_plans_and_verification.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/005_add_projects.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/006_add_image_generation.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/007_add_admin_portal.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/008_add_device_code_auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/009_add_status_page.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/extract_claude_data.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/filter_claude_data.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/generate_curated_data.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/mix_training_data.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/architect.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/audit.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/base.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/coder.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/database.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/docs.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/explorer.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/guardian.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/planner.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/reviewer.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/security.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/terraform.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/testing.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/uiux.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/auth.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/config.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/conversation.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/heartbeat.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/llm.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/logging_config.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/main.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/router.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/sandbox.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/settings.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/__init__.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/analysis.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/base.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/code.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/definitions.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/files.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/review.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/search.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/ui.py +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/dependency_links.txt +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/entry_points.txt +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/requires.txt +0 -0
- {kairo_code-0.1.0 → kairo_code-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
from datetime import datetime, UTC
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
+
|
|
6
|
+
from backend.config import settings
|
|
7
|
+
from backend.core.api_key_auth import get_api_key_user
|
|
8
|
+
from backend.core.database import get_db
|
|
9
|
+
from backend.core.dependencies import get_current_user
|
|
10
|
+
from backend.models.api_key import ApiKey
|
|
11
|
+
from backend.models.user import User
|
|
12
|
+
from backend.schemas.agent import (
|
|
13
|
+
AgentHeartbeatRequest,
|
|
14
|
+
AgentHeartbeatResponse,
|
|
15
|
+
AgentResponse,
|
|
16
|
+
AgentListResponse,
|
|
17
|
+
RegisterAgentRequest,
|
|
18
|
+
UpdateAgentRequest,
|
|
19
|
+
AgentMetricsResponse,
|
|
20
|
+
AgentEventResponse,
|
|
21
|
+
IssueCommandRequest,
|
|
22
|
+
CommandResponse,
|
|
23
|
+
CreateAlertConfigRequest,
|
|
24
|
+
UpdateAlertConfigRequest,
|
|
25
|
+
AlertConfigResponse,
|
|
26
|
+
AlertHistoryResponse,
|
|
27
|
+
SetupTokenResponse,
|
|
28
|
+
AgentRegistrationRequest,
|
|
29
|
+
AgentRegistrationResponse,
|
|
30
|
+
TelemetryBatchRequest,
|
|
31
|
+
TelemetryBatchResponse,
|
|
32
|
+
AgentCommand,
|
|
33
|
+
)
|
|
34
|
+
from backend.services.agent_service import AgentService
|
|
35
|
+
|
|
36
|
+
router = APIRouter(prefix="/agents", tags=["Agents"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check_enabled():
|
|
40
|
+
if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
|
|
41
|
+
raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ─── CRUD ─────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
@router.post("/", response_model=AgentResponse)
|
|
47
|
+
async def register_agent(
|
|
48
|
+
req: RegisterAgentRequest,
|
|
49
|
+
user: User = Depends(get_current_user),
|
|
50
|
+
db: AsyncSession = Depends(get_db),
|
|
51
|
+
):
|
|
52
|
+
"""Create a new agent."""
|
|
53
|
+
_check_enabled()
|
|
54
|
+
svc = AgentService(db)
|
|
55
|
+
agent = await svc.register(user.id, req)
|
|
56
|
+
return agent
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/", response_model=list[AgentListResponse])
|
|
60
|
+
async def list_agents(
|
|
61
|
+
user: User = Depends(get_current_user),
|
|
62
|
+
db: AsyncSession = Depends(get_db),
|
|
63
|
+
):
|
|
64
|
+
"""List all agents with 24h summary metrics."""
|
|
65
|
+
_check_enabled()
|
|
66
|
+
svc = AgentService(db)
|
|
67
|
+
agents = await svc.list_agents_with_metrics(user.id)
|
|
68
|
+
return agents
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.get("/{agent_id}", response_model=AgentResponse)
|
|
72
|
+
async def get_agent(
|
|
73
|
+
agent_id: str,
|
|
74
|
+
user: User = Depends(get_current_user),
|
|
75
|
+
db: AsyncSession = Depends(get_db),
|
|
76
|
+
):
|
|
77
|
+
"""Get agent details."""
|
|
78
|
+
_check_enabled()
|
|
79
|
+
svc = AgentService(db)
|
|
80
|
+
agent = await svc.get_agent(user.id, agent_id)
|
|
81
|
+
if not agent:
|
|
82
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
83
|
+
return agent
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.patch("/{agent_id}", response_model=AgentResponse)
|
|
87
|
+
async def update_agent(
|
|
88
|
+
agent_id: str,
|
|
89
|
+
req: UpdateAgentRequest,
|
|
90
|
+
user: User = Depends(get_current_user),
|
|
91
|
+
db: AsyncSession = Depends(get_db),
|
|
92
|
+
):
|
|
93
|
+
"""Update agent configuration."""
|
|
94
|
+
_check_enabled()
|
|
95
|
+
svc = AgentService(db)
|
|
96
|
+
agent = await svc.update_agent(user.id, agent_id, req)
|
|
97
|
+
if not agent:
|
|
98
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
99
|
+
return agent
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.delete("/{agent_id}")
|
|
103
|
+
async def delete_agent(
|
|
104
|
+
agent_id: str,
|
|
105
|
+
user: User = Depends(get_current_user),
|
|
106
|
+
db: AsyncSession = Depends(get_db),
|
|
107
|
+
):
|
|
108
|
+
"""Delete an agent (soft delete)."""
|
|
109
|
+
_check_enabled()
|
|
110
|
+
svc = AgentService(db)
|
|
111
|
+
deleted = await svc.delete_agent(user.id, agent_id)
|
|
112
|
+
if not deleted:
|
|
113
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
114
|
+
return {"deleted": True}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ─── Setup & Registration ─────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
@router.post("/{agent_id}/setup-token", response_model=SetupTokenResponse)
|
|
120
|
+
async def generate_setup_token(
|
|
121
|
+
agent_id: str,
|
|
122
|
+
user: User = Depends(get_current_user),
|
|
123
|
+
db: AsyncSession = Depends(get_db),
|
|
124
|
+
):
|
|
125
|
+
"""Generate a one-time setup token for agent registration."""
|
|
126
|
+
_check_enabled()
|
|
127
|
+
svc = AgentService(db)
|
|
128
|
+
result = await svc.generate_setup_token(user.id, agent_id)
|
|
129
|
+
if not result:
|
|
130
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
131
|
+
token, expires_at = result
|
|
132
|
+
return SetupTokenResponse(token=token, expires_at=expires_at, agent_id=agent_id)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.post("/register", response_model=AgentRegistrationResponse)
|
|
136
|
+
async def register_with_token(
|
|
137
|
+
req: AgentRegistrationRequest,
|
|
138
|
+
request: Request,
|
|
139
|
+
db: AsyncSession = Depends(get_db),
|
|
140
|
+
):
|
|
141
|
+
"""Register an agent using a setup token. Returns agent config and API key."""
|
|
142
|
+
_check_enabled()
|
|
143
|
+
client_ip = request.client.host if request.client else None
|
|
144
|
+
svc = AgentService(db)
|
|
145
|
+
|
|
146
|
+
# Consume token
|
|
147
|
+
agent = await svc.consume_setup_token(req.setup_token, client_ip)
|
|
148
|
+
if not agent:
|
|
149
|
+
raise HTTPException(status_code=401, detail="Invalid or expired setup token")
|
|
150
|
+
|
|
151
|
+
# Update SDK version
|
|
152
|
+
agent.sdk_version = req.sdk_version
|
|
153
|
+
if req.host_info:
|
|
154
|
+
agent.host_info = req.host_info.model_dump()
|
|
155
|
+
await db.commit()
|
|
156
|
+
|
|
157
|
+
# Create agent-scoped API key
|
|
158
|
+
api_key, _ = await svc.create_agent_api_key(agent.user_id, agent.id, agent.name)
|
|
159
|
+
|
|
160
|
+
return AgentRegistrationResponse(
|
|
161
|
+
agent_id=agent.id,
|
|
162
|
+
name=agent.name,
|
|
163
|
+
model_preference=agent.model_preference,
|
|
164
|
+
system_prompt=agent.system_prompt,
|
|
165
|
+
api_key=api_key,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@router.get("/{agent_id}/connection-status")
|
|
170
|
+
async def get_connection_status(
|
|
171
|
+
agent_id: str,
|
|
172
|
+
user: User = Depends(get_current_user),
|
|
173
|
+
db: AsyncSession = Depends(get_db),
|
|
174
|
+
):
|
|
175
|
+
"""Poll for agent connection status (for wizard verification step)."""
|
|
176
|
+
_check_enabled()
|
|
177
|
+
svc = AgentService(db)
|
|
178
|
+
agent = await svc.get_agent(user.id, agent_id)
|
|
179
|
+
if not agent:
|
|
180
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
181
|
+
return {
|
|
182
|
+
"connected": agent.first_connected_at is not None,
|
|
183
|
+
"state": agent.state,
|
|
184
|
+
"first_connected_at": agent.first_connected_at,
|
|
185
|
+
"sdk_version": agent.sdk_version,
|
|
186
|
+
"host_info": agent.host_info,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ─── Heartbeat ────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
@router.post("/heartbeat", response_model=AgentHeartbeatResponse)
|
|
193
|
+
async def agent_heartbeat(
|
|
194
|
+
req: AgentHeartbeatRequest,
|
|
195
|
+
request: Request,
|
|
196
|
+
auth: tuple[User, ApiKey] = Depends(get_api_key_user),
|
|
197
|
+
db: AsyncSession = Depends(get_db),
|
|
198
|
+
):
|
|
199
|
+
"""Agent heartbeat — authenticated via API key. Returns pending commands."""
|
|
200
|
+
_check_enabled()
|
|
201
|
+
user, _api_key = auth
|
|
202
|
+
client_ip = request.client.host if request.client else None
|
|
203
|
+
|
|
204
|
+
svc = AgentService(db)
|
|
205
|
+
agent, commands = await svc.heartbeat(req.agent_id, user.id, req, client_ip)
|
|
206
|
+
if not agent:
|
|
207
|
+
raise HTTPException(status_code=404, detail="Agent not found or not owned by this key's user")
|
|
208
|
+
|
|
209
|
+
return AgentHeartbeatResponse(
|
|
210
|
+
acknowledged=True,
|
|
211
|
+
server_time=datetime.now(UTC),
|
|
212
|
+
commands=[
|
|
213
|
+
AgentCommand(
|
|
214
|
+
command_id=cmd["command_id"],
|
|
215
|
+
type=cmd["type"],
|
|
216
|
+
payload=cmd["payload"],
|
|
217
|
+
issued_at=cmd["issued_at"],
|
|
218
|
+
expires_at=cmd["expires_at"],
|
|
219
|
+
signature=cmd["signature"],
|
|
220
|
+
)
|
|
221
|
+
for cmd in commands
|
|
222
|
+
],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ─── Commands ─────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
@router.post("/{agent_id}/restart", response_model=CommandResponse)
|
|
229
|
+
async def restart_agent(
|
|
230
|
+
agent_id: str,
|
|
231
|
+
user: User = Depends(get_current_user),
|
|
232
|
+
db: AsyncSession = Depends(get_db),
|
|
233
|
+
):
|
|
234
|
+
"""Request agent restart."""
|
|
235
|
+
_check_enabled()
|
|
236
|
+
svc = AgentService(db)
|
|
237
|
+
req = IssueCommandRequest(command_type="restart")
|
|
238
|
+
command = await svc.issue_command(user.id, agent_id, req)
|
|
239
|
+
if not command:
|
|
240
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
241
|
+
return command
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@router.post("/{agent_id}/stop", response_model=CommandResponse)
|
|
245
|
+
async def stop_agent(
|
|
246
|
+
agent_id: str,
|
|
247
|
+
user: User = Depends(get_current_user),
|
|
248
|
+
db: AsyncSession = Depends(get_db),
|
|
249
|
+
):
|
|
250
|
+
"""Request agent stop."""
|
|
251
|
+
_check_enabled()
|
|
252
|
+
svc = AgentService(db)
|
|
253
|
+
req = IssueCommandRequest(command_type="stop")
|
|
254
|
+
command = await svc.issue_command(user.id, agent_id, req)
|
|
255
|
+
if not command:
|
|
256
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
257
|
+
return command
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@router.post("/{agent_id}/command", response_model=CommandResponse)
|
|
261
|
+
async def issue_command(
|
|
262
|
+
agent_id: str,
|
|
263
|
+
req: IssueCommandRequest,
|
|
264
|
+
user: User = Depends(get_current_user),
|
|
265
|
+
db: AsyncSession = Depends(get_db),
|
|
266
|
+
):
|
|
267
|
+
"""Issue a command to an agent."""
|
|
268
|
+
_check_enabled()
|
|
269
|
+
svc = AgentService(db)
|
|
270
|
+
command = await svc.issue_command(user.id, agent_id, req)
|
|
271
|
+
if not command:
|
|
272
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
273
|
+
return command
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ─── Metrics ──────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
@router.get("/{agent_id}/metrics", response_model=AgentMetricsResponse)
|
|
279
|
+
async def get_agent_metrics(
|
|
280
|
+
agent_id: str,
|
|
281
|
+
range: str = Query("24h", pattern="^(1h|6h|24h|7d|30d)$"),
|
|
282
|
+
granularity: str = Query("auto", pattern="^(auto|1m|1h|1d)$"),
|
|
283
|
+
user: User = Depends(get_current_user),
|
|
284
|
+
db: AsyncSession = Depends(get_db),
|
|
285
|
+
):
|
|
286
|
+
"""Get agent metrics for a time range."""
|
|
287
|
+
_check_enabled()
|
|
288
|
+
svc = AgentService(db)
|
|
289
|
+
metrics = await svc.get_metrics(user.id, agent_id, range, granularity)
|
|
290
|
+
if metrics is None:
|
|
291
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
292
|
+
return metrics
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ─── Events ───────────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
@router.get("/{agent_id}/events", response_model=list[AgentEventResponse])
|
|
298
|
+
async def get_agent_events(
|
|
299
|
+
agent_id: str,
|
|
300
|
+
event_type: str | None = Query(None),
|
|
301
|
+
limit: int = Query(50, ge=1, le=500),
|
|
302
|
+
user: User = Depends(get_current_user),
|
|
303
|
+
db: AsyncSession = Depends(get_db),
|
|
304
|
+
):
|
|
305
|
+
"""Get agent events."""
|
|
306
|
+
_check_enabled()
|
|
307
|
+
svc = AgentService(db)
|
|
308
|
+
events = await svc.get_events(user.id, agent_id, event_type, limit)
|
|
309
|
+
if events is None:
|
|
310
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
311
|
+
return events
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ─── Alerts ───────────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
@router.get("/{agent_id}/alerts", response_model=list[AlertConfigResponse])
|
|
317
|
+
async def get_alert_configs(
|
|
318
|
+
agent_id: str,
|
|
319
|
+
user: User = Depends(get_current_user),
|
|
320
|
+
db: AsyncSession = Depends(get_db),
|
|
321
|
+
):
|
|
322
|
+
"""Get alert configurations for an agent."""
|
|
323
|
+
_check_enabled()
|
|
324
|
+
svc = AgentService(db)
|
|
325
|
+
configs = await svc.get_alert_configs(user.id, agent_id)
|
|
326
|
+
if configs is None:
|
|
327
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
328
|
+
return configs
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@router.post("/{agent_id}/alerts", response_model=AlertConfigResponse)
|
|
332
|
+
async def create_alert_config(
|
|
333
|
+
agent_id: str,
|
|
334
|
+
req: CreateAlertConfigRequest,
|
|
335
|
+
user: User = Depends(get_current_user),
|
|
336
|
+
db: AsyncSession = Depends(get_db),
|
|
337
|
+
):
|
|
338
|
+
"""Create an alert configuration."""
|
|
339
|
+
_check_enabled()
|
|
340
|
+
svc = AgentService(db)
|
|
341
|
+
config = await svc.create_alert_config(user.id, agent_id, req)
|
|
342
|
+
if not config:
|
|
343
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
344
|
+
return config
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@router.patch("/{agent_id}/alerts/{config_id}", response_model=AlertConfigResponse)
|
|
348
|
+
async def update_alert_config(
|
|
349
|
+
agent_id: str,
|
|
350
|
+
config_id: str,
|
|
351
|
+
req: UpdateAlertConfigRequest,
|
|
352
|
+
user: User = Depends(get_current_user),
|
|
353
|
+
db: AsyncSession = Depends(get_db),
|
|
354
|
+
):
|
|
355
|
+
"""Update an alert configuration."""
|
|
356
|
+
_check_enabled()
|
|
357
|
+
svc = AgentService(db)
|
|
358
|
+
config = await svc.update_alert_config(user.id, agent_id, config_id, req)
|
|
359
|
+
if not config:
|
|
360
|
+
raise HTTPException(status_code=404, detail="Alert config not found")
|
|
361
|
+
return config
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@router.delete("/{agent_id}/alerts/{config_id}")
|
|
365
|
+
async def delete_alert_config(
|
|
366
|
+
agent_id: str,
|
|
367
|
+
config_id: str,
|
|
368
|
+
user: User = Depends(get_current_user),
|
|
369
|
+
db: AsyncSession = Depends(get_db),
|
|
370
|
+
):
|
|
371
|
+
"""Delete an alert configuration."""
|
|
372
|
+
_check_enabled()
|
|
373
|
+
svc = AgentService(db)
|
|
374
|
+
deleted = await svc.delete_alert_config(user.id, agent_id, config_id)
|
|
375
|
+
if not deleted:
|
|
376
|
+
raise HTTPException(status_code=404, detail="Alert config not found")
|
|
377
|
+
return {"deleted": True}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@router.get("/{agent_id}/alerts/history", response_model=list[AlertHistoryResponse])
|
|
381
|
+
async def get_alert_history(
|
|
382
|
+
agent_id: str,
|
|
383
|
+
limit: int = Query(50, ge=1, le=500),
|
|
384
|
+
user: User = Depends(get_current_user),
|
|
385
|
+
db: AsyncSession = Depends(get_db),
|
|
386
|
+
):
|
|
387
|
+
"""Get alert history for an agent."""
|
|
388
|
+
_check_enabled()
|
|
389
|
+
svc = AgentService(db)
|
|
390
|
+
history = await svc.get_alert_history(user.id, agent_id, limit)
|
|
391
|
+
if history is None:
|
|
392
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
393
|
+
return history
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ─── SDK Telemetry ────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
@router.post("/telemetry/batch", response_model=TelemetryBatchResponse)
|
|
399
|
+
async def submit_telemetry_batch(
|
|
400
|
+
req: TelemetryBatchRequest,
|
|
401
|
+
auth: tuple[User, ApiKey] = Depends(get_api_key_user),
|
|
402
|
+
db: AsyncSession = Depends(get_db),
|
|
403
|
+
):
|
|
404
|
+
"""Submit a batch of telemetry events from the SDK."""
|
|
405
|
+
_check_enabled()
|
|
406
|
+
user, api_key = auth
|
|
407
|
+
|
|
408
|
+
# Verify agent belongs to user
|
|
409
|
+
svc = AgentService(db)
|
|
410
|
+
agent = await svc.get_agent(user.id, req.agent_id)
|
|
411
|
+
if not agent:
|
|
412
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
413
|
+
|
|
414
|
+
accepted, rejected, errors = await svc.process_telemetry_batch(req.agent_id, req)
|
|
415
|
+
return TelemetryBatchResponse(accepted=accepted, rejected=rejected, errors=errors)
|
|
@@ -8,8 +8,10 @@ from pathlib import Path
|
|
|
8
8
|
import httpx
|
|
9
9
|
from fastapi import FastAPI, Request
|
|
10
10
|
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
|
11
12
|
from fastapi.responses import FileResponse, JSONResponse
|
|
12
13
|
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
from starlette.responses import Response
|
|
13
15
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
14
16
|
|
|
15
17
|
from backend.core.database import init_db
|
|
@@ -124,6 +126,60 @@ async def lifespan(app: FastAPI):
|
|
|
124
126
|
agent_task = asyncio.create_task(_agent_staleness_loop())
|
|
125
127
|
logger.info("Agent staleness checker started (60s interval)")
|
|
126
128
|
|
|
129
|
+
# Background task for hourly metrics rollup
|
|
130
|
+
async def _metrics_rollup_hourly_loop():
|
|
131
|
+
from backend.core.database import async_session
|
|
132
|
+
from backend.services.agent_service import AgentService
|
|
133
|
+
# Wait 5 minutes after startup before first run
|
|
134
|
+
await asyncio.sleep(300)
|
|
135
|
+
while True:
|
|
136
|
+
try:
|
|
137
|
+
async with async_session() as db:
|
|
138
|
+
svc = AgentService(db)
|
|
139
|
+
await svc.rollup_1m_to_1h()
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning("Hourly metrics rollup error: %s", e)
|
|
142
|
+
await asyncio.sleep(3600) # Every hour
|
|
143
|
+
|
|
144
|
+
metrics_hourly_task = asyncio.create_task(_metrics_rollup_hourly_loop())
|
|
145
|
+
logger.info("Hourly metrics rollup started (3600s interval)")
|
|
146
|
+
|
|
147
|
+
# Background task for daily metrics rollup
|
|
148
|
+
async def _metrics_rollup_daily_loop():
|
|
149
|
+
from backend.core.database import async_session
|
|
150
|
+
from backend.services.agent_service import AgentService
|
|
151
|
+
# Wait 10 minutes after startup before first run
|
|
152
|
+
await asyncio.sleep(600)
|
|
153
|
+
while True:
|
|
154
|
+
try:
|
|
155
|
+
async with async_session() as db:
|
|
156
|
+
svc = AgentService(db)
|
|
157
|
+
await svc.rollup_1h_to_daily()
|
|
158
|
+
await svc.cleanup_old_metrics()
|
|
159
|
+
await svc.cleanup_old_events()
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.warning("Daily metrics rollup error: %s", e)
|
|
162
|
+
await asyncio.sleep(86400) # Every 24 hours
|
|
163
|
+
|
|
164
|
+
metrics_daily_task = asyncio.create_task(_metrics_rollup_daily_loop())
|
|
165
|
+
logger.info("Daily metrics rollup started (86400s interval)")
|
|
166
|
+
|
|
167
|
+
# Background task for alert evaluation
|
|
168
|
+
async def _alert_evaluation_loop():
|
|
169
|
+
from backend.core.database import async_session
|
|
170
|
+
from backend.services.agent_service import AgentService
|
|
171
|
+
while True:
|
|
172
|
+
try:
|
|
173
|
+
async with async_session() as db:
|
|
174
|
+
svc = AgentService(db)
|
|
175
|
+
await svc.evaluate_alerts()
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.warning("Alert evaluation error: %s", e)
|
|
178
|
+
await asyncio.sleep(60) # Every minute
|
|
179
|
+
|
|
180
|
+
alert_task = asyncio.create_task(_alert_evaluation_loop())
|
|
181
|
+
logger.info("Alert evaluation started (60s interval)")
|
|
182
|
+
|
|
127
183
|
# Background task to clean up expired device codes
|
|
128
184
|
async def _device_code_cleanup_loop():
|
|
129
185
|
from backend.core.database import async_session
|
|
@@ -222,6 +278,9 @@ async def lifespan(app: FastAPI):
|
|
|
222
278
|
agent_task.cancel()
|
|
223
279
|
device_cleanup_task.cancel()
|
|
224
280
|
uptime_task.cancel()
|
|
281
|
+
metrics_hourly_task.cancel()
|
|
282
|
+
metrics_daily_task.cancel()
|
|
283
|
+
alert_task.cancel()
|
|
225
284
|
await app.state.llm_service.close()
|
|
226
285
|
logger.info("Kairo shutting down")
|
|
227
286
|
|
|
@@ -231,6 +290,9 @@ def create_app() -> FastAPI:
|
|
|
231
290
|
|
|
232
291
|
app = FastAPI(title="Kairo", version="0.1.0", lifespan=lifespan)
|
|
233
292
|
|
|
293
|
+
# GZip compression for responses > 500 bytes
|
|
294
|
+
app.add_middleware(GZipMiddleware, minimum_size=500)
|
|
295
|
+
|
|
234
296
|
# CORS — configurable via env
|
|
235
297
|
origins = [o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()]
|
|
236
298
|
app.add_middleware(
|
|
@@ -270,20 +332,38 @@ def create_app() -> FastAPI:
|
|
|
270
332
|
app.include_router(device_auth_router, prefix="/api")
|
|
271
333
|
app.include_router(files_router, prefix="/api")
|
|
272
334
|
|
|
273
|
-
# Serve static frontend build
|
|
335
|
+
# Serve static frontend build with proper cache headers
|
|
274
336
|
static_dir = Path(__file__).parent / "static"
|
|
275
337
|
if static_dir.exists():
|
|
276
|
-
|
|
338
|
+
# Custom middleware to add cache headers for assets
|
|
339
|
+
class CachedStaticFiles(StaticFiles):
|
|
340
|
+
async def __call__(self, scope, receive, send):
|
|
341
|
+
async def send_with_cache(message):
|
|
342
|
+
if message["type"] == "http.response.start":
|
|
343
|
+
headers = list(message.get("headers", []))
|
|
344
|
+
# Hashed assets are immutable - cache for 1 year
|
|
345
|
+
headers.append((b"cache-control", b"public, max-age=31536000, immutable"))
|
|
346
|
+
message["headers"] = headers
|
|
347
|
+
await send(message)
|
|
348
|
+
await super().__call__(scope, receive, send_with_cache)
|
|
349
|
+
|
|
350
|
+
app.mount("/assets", CachedStaticFiles(directory=static_dir / "assets"), name="assets")
|
|
277
351
|
|
|
278
352
|
@app.get("/{full_path:path}")
|
|
279
353
|
async def serve_spa(full_path: str):
|
|
280
354
|
file_path = (static_dir / full_path).resolve()
|
|
281
355
|
# Path traversal guard
|
|
282
356
|
if not str(file_path).startswith(str(static_dir.resolve())):
|
|
283
|
-
return FileResponse(
|
|
357
|
+
return FileResponse(
|
|
358
|
+
static_dir / "index.html",
|
|
359
|
+
headers={"Cache-Control": "no-cache, must-revalidate"}
|
|
360
|
+
)
|
|
284
361
|
if file_path.is_file():
|
|
285
362
|
return FileResponse(file_path)
|
|
286
|
-
return FileResponse(
|
|
363
|
+
return FileResponse(
|
|
364
|
+
static_dir / "index.html",
|
|
365
|
+
headers={"Cache-Control": "no-cache, must-revalidate"}
|
|
366
|
+
)
|
|
287
367
|
|
|
288
368
|
return app
|
|
289
369
|
|
|
@@ -118,13 +118,15 @@ class Settings(BaseSettings):
|
|
|
118
118
|
|
|
119
119
|
# Context window limits per model (in estimated tokens).
|
|
120
120
|
# Reserve space for system prompt + response.
|
|
121
|
+
# Small models (nyx-lite) get tighter limits to leave room for output
|
|
122
|
+
# and trigger earlier compression for better quality.
|
|
121
123
|
CONTEXT_LIMITS: dict[str, int] = {
|
|
122
124
|
"nyx": 6000, # 8k context, reserve 2k for response
|
|
123
|
-
"nyx-lite":
|
|
125
|
+
"nyx-lite": 3000, # 8k context, aggressive reserve for 14B quality
|
|
124
126
|
"theron": 14000, # 16k+ context
|
|
125
127
|
"helios": 14000, # 16k+ context
|
|
126
128
|
}
|
|
127
|
-
SUMMARY_TRIGGER_TOKENS: int =
|
|
129
|
+
SUMMARY_TRIGGER_TOKENS: int = 3000 # Summarize history when it exceeds this (lower for quality)
|
|
128
130
|
|
|
129
131
|
MODEL_MAP: dict[str, str] = {
|
|
130
132
|
"nyx": "nyx",
|