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.
Files changed (188) hide show
  1. {kairo_code-0.1.0 → kairo_code-0.2.0}/PKG-INFO +1 -1
  2. kairo_code-0.2.0/kairo/backend/api/agents.py +415 -0
  3. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/app.py +84 -4
  4. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/config.py +4 -2
  5. kairo_code-0.2.0/kairo/backend/models/agent.py +244 -0
  6. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/api_key.py +4 -1
  7. kairo_code-0.2.0/kairo/backend/models/task.py +31 -0
  8. kairo_code-0.2.0/kairo/backend/models/user_provider_key.py +26 -0
  9. kairo_code-0.2.0/kairo/backend/schemas/agent.py +289 -0
  10. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/api_key.py +3 -0
  11. kairo_code-0.2.0/kairo/backend/services/agent/__init__.py +52 -0
  12. kairo_code-0.2.0/kairo/backend/services/agent/agent_alerts_evaluation_service.py +224 -0
  13. kairo_code-0.2.0/kairo/backend/services/agent/agent_alerts_service.py +201 -0
  14. kairo_code-0.2.0/kairo/backend/services/agent/agent_commands_service.py +142 -0
  15. kairo_code-0.2.0/kairo/backend/services/agent/agent_crud_service.py +150 -0
  16. kairo_code-0.2.0/kairo/backend/services/agent/agent_events_service.py +103 -0
  17. kairo_code-0.2.0/kairo/backend/services/agent/agent_heartbeat_service.py +207 -0
  18. kairo_code-0.2.0/kairo/backend/services/agent/agent_metrics_rollup_service.py +248 -0
  19. kairo_code-0.2.0/kairo/backend/services/agent/agent_metrics_service.py +259 -0
  20. kairo_code-0.2.0/kairo/backend/services/agent/agent_service.py +315 -0
  21. kairo_code-0.2.0/kairo/backend/services/agent/agent_setup_service.py +180 -0
  22. kairo_code-0.2.0/kairo/backend/services/agent/constants.py +28 -0
  23. kairo_code-0.2.0/kairo/backend/services/agent_service.py +23 -0
  24. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/api_key_service.py +23 -3
  25. kairo_code-0.2.0/kairo/backend/services/byok_service.py +204 -0
  26. kairo_code-0.2.0/kairo/backend/services/chat_service.py +836 -0
  27. kairo_code-0.2.0/kairo/backend/services/deep_search_service.py +159 -0
  28. kairo_code-0.2.0/kairo/backend/services/email_service.py +454 -0
  29. kairo_code-0.2.0/kairo/backend/services/few_shot_service.py +223 -0
  30. kairo_code-0.2.0/kairo/backend/services/post_processor.py +261 -0
  31. kairo_code-0.2.0/kairo/backend/services/rag_service.py +150 -0
  32. kairo_code-0.2.0/kairo/backend/services/task_service.py +119 -0
  33. kairo_code-0.2.0/kairo/backend/tests/__init__.py +1 -0
  34. kairo_code-0.2.0/kairo/backend/tests/e2e/__init__.py +1 -0
  35. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/__init__.py +1 -0
  36. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/conftest.py +389 -0
  37. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_alerts.py +802 -0
  38. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_commands.py +456 -0
  39. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_crud.py +455 -0
  40. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_events.py +415 -0
  41. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_heartbeat.py +520 -0
  42. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_metrics.py +587 -0
  43. kairo_code-0.2.0/kairo/backend/tests/e2e/agents/test_agent_setup.py +349 -0
  44. kairo_code-0.2.0/kairo/migrations/versions/010_agent_dashboard.py +246 -0
  45. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/PKG-INFO +1 -1
  46. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/SOURCES.txt +35 -1
  47. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/top_level.txt +2 -0
  48. kairo_code-0.2.0/kairo_migrations/env.py +92 -0
  49. kairo_code-0.2.0/kairo_migrations/versions/001_add_agent_dashboard_extensions.py +450 -0
  50. {kairo_code-0.1.0 → kairo_code-0.2.0}/pyproject.toml +1 -1
  51. kairo_code-0.1.0/kairo/backend/api/agents.py +0 -94
  52. kairo_code-0.1.0/kairo/backend/models/agent.py +0 -30
  53. kairo_code-0.1.0/kairo/backend/schemas/agent.py +0 -42
  54. kairo_code-0.1.0/kairo/backend/services/agent_service.py +0 -107
  55. kairo_code-0.1.0/kairo/backend/services/chat_service.py +0 -501
  56. kairo_code-0.1.0/kairo/backend/services/email_service.py +0 -55
  57. {kairo_code-0.1.0 → kairo_code-0.2.0}/image-service/main.py +0 -0
  58. {kairo_code-0.1.0 → kairo_code-0.2.0}/infra/chat/app/main.py +0 -0
  59. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/__init__.py +0 -0
  60. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/__init__.py +0 -0
  61. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/__init__.py +0 -0
  62. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/audit.py +0 -0
  63. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/content.py +0 -0
  64. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/incidents.py +0 -0
  65. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/stats.py +0 -0
  66. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/system.py +0 -0
  67. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/admin/users.py +0 -0
  68. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/api_keys.py +0 -0
  69. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/auth.py +0 -0
  70. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/billing.py +0 -0
  71. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/chat.py +0 -0
  72. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/conversations.py +0 -0
  73. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/device_auth.py +0 -0
  74. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/files.py +0 -0
  75. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/health.py +0 -0
  76. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/images.py +0 -0
  77. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/openai_compat.py +0 -0
  78. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/projects.py +0 -0
  79. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/usage.py +0 -0
  80. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/api/webhooks.py +0 -0
  81. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/__init__.py +0 -0
  82. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/admin_auth.py +0 -0
  83. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/api_key_auth.py +0 -0
  84. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/database.py +0 -0
  85. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/dependencies.py +0 -0
  86. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/logging.py +0 -0
  87. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/rate_limit.py +0 -0
  88. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/core/security.py +0 -0
  89. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/__init__.py +0 -0
  90. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/api_usage.py +0 -0
  91. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/audit_log.py +0 -0
  92. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/conversation.py +0 -0
  93. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/device_code.py +0 -0
  94. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/feature_flag.py +0 -0
  95. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/image_generation.py +0 -0
  96. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/incident.py +0 -0
  97. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/project.py +0 -0
  98. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/uptime_record.py +0 -0
  99. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/usage.py +0 -0
  100. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/models/user.py +0 -0
  101. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/__init__.py +0 -0
  102. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/__init__.py +0 -0
  103. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/audit.py +0 -0
  104. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/content.py +0 -0
  105. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/stats.py +0 -0
  106. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/system.py +0 -0
  107. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/admin/users.py +0 -0
  108. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/auth.py +0 -0
  109. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/chat.py +0 -0
  110. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/conversation.py +0 -0
  111. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/device_auth.py +0 -0
  112. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/image.py +0 -0
  113. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/openai_compat.py +0 -0
  114. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/project.py +0 -0
  115. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/status.py +0 -0
  116. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/schemas/usage.py +0 -0
  117. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/__init__.py +0 -0
  118. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/__init__.py +0 -0
  119. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/audit_service.py +0 -0
  120. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/content_service.py +0 -0
  121. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/incident_service.py +0 -0
  122. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/stats_service.py +0 -0
  123. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/system_service.py +0 -0
  124. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/admin/user_service.py +0 -0
  125. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/api_usage_service.py +0 -0
  126. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/auth_service.py +0 -0
  127. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/conversation_service.py +0 -0
  128. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/device_auth_service.py +0 -0
  129. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/image_service.py +0 -0
  130. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/llm_service.py +0 -0
  131. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/project_service.py +0 -0
  132. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/status_service.py +0 -0
  133. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/stripe_service.py +0 -0
  134. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/usage_service.py +0 -0
  135. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/backend/services/web_search_service.py +0 -0
  136. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/env.py +0 -0
  137. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/001_initial.py +0 -0
  138. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/002_usage_tracking_and_indexes.py +0 -0
  139. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/003_username_to_email.py +0 -0
  140. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/004_add_plans_and_verification.py +0 -0
  141. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/005_add_projects.py +0 -0
  142. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/006_add_image_generation.py +0 -0
  143. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/007_add_admin_portal.py +0 -0
  144. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/008_add_device_code_auth.py +0 -0
  145. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/migrations/versions/009_add_status_page.py +0 -0
  146. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/extract_claude_data.py +0 -0
  147. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/filter_claude_data.py +0 -0
  148. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/generate_curated_data.py +0 -0
  149. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo/tools/mix_training_data.py +0 -0
  150. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/__init__.py +0 -0
  151. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/__init__.py +0 -0
  152. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/architect.py +0 -0
  153. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/audit.py +0 -0
  154. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/base.py +0 -0
  155. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/coder.py +0 -0
  156. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/database.py +0 -0
  157. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/docs.py +0 -0
  158. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/explorer.py +0 -0
  159. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/guardian.py +0 -0
  160. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/planner.py +0 -0
  161. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/reviewer.py +0 -0
  162. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/security.py +0 -0
  163. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/terraform.py +0 -0
  164. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/testing.py +0 -0
  165. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/agents/uiux.py +0 -0
  166. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/auth.py +0 -0
  167. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/config.py +0 -0
  168. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/conversation.py +0 -0
  169. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/heartbeat.py +0 -0
  170. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/llm.py +0 -0
  171. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/logging_config.py +0 -0
  172. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/main.py +0 -0
  173. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/router.py +0 -0
  174. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/sandbox.py +0 -0
  175. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/settings.py +0 -0
  176. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/__init__.py +0 -0
  177. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/analysis.py +0 -0
  178. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/base.py +0 -0
  179. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/code.py +0 -0
  180. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/definitions.py +0 -0
  181. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/files.py +0 -0
  182. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/review.py +0 -0
  183. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/tools/search.py +0 -0
  184. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code/ui.py +0 -0
  185. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/dependency_links.txt +0 -0
  186. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/entry_points.txt +0 -0
  187. {kairo_code-0.1.0 → kairo_code-0.2.0}/kairo_code.egg-info/requires.txt +0 -0
  188. {kairo_code-0.1.0 → kairo_code-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kairo-code
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Kairo Code - AI Coding Assistant by Kairon Labs
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -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
- app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets")
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(static_dir / "index.html")
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(static_dir / "index.html")
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": 4000, # 8k context, conservative reserve for 14B
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 = 4000 # Summarize history when it exceeds this
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",