kairo-code 0.1.0__py3-none-any.whl

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 (144) hide show
  1. image-service/main.py +178 -0
  2. infra/chat/app/main.py +84 -0
  3. kairo/backend/__init__.py +0 -0
  4. kairo/backend/api/__init__.py +0 -0
  5. kairo/backend/api/admin/__init__.py +23 -0
  6. kairo/backend/api/admin/audit.py +54 -0
  7. kairo/backend/api/admin/content.py +142 -0
  8. kairo/backend/api/admin/incidents.py +148 -0
  9. kairo/backend/api/admin/stats.py +125 -0
  10. kairo/backend/api/admin/system.py +87 -0
  11. kairo/backend/api/admin/users.py +279 -0
  12. kairo/backend/api/agents.py +94 -0
  13. kairo/backend/api/api_keys.py +85 -0
  14. kairo/backend/api/auth.py +116 -0
  15. kairo/backend/api/billing.py +41 -0
  16. kairo/backend/api/chat.py +72 -0
  17. kairo/backend/api/conversations.py +125 -0
  18. kairo/backend/api/device_auth.py +100 -0
  19. kairo/backend/api/files.py +83 -0
  20. kairo/backend/api/health.py +36 -0
  21. kairo/backend/api/images.py +80 -0
  22. kairo/backend/api/openai_compat.py +225 -0
  23. kairo/backend/api/projects.py +102 -0
  24. kairo/backend/api/usage.py +32 -0
  25. kairo/backend/api/webhooks.py +79 -0
  26. kairo/backend/app.py +297 -0
  27. kairo/backend/config.py +179 -0
  28. kairo/backend/core/__init__.py +0 -0
  29. kairo/backend/core/admin_auth.py +24 -0
  30. kairo/backend/core/api_key_auth.py +55 -0
  31. kairo/backend/core/database.py +28 -0
  32. kairo/backend/core/dependencies.py +70 -0
  33. kairo/backend/core/logging.py +23 -0
  34. kairo/backend/core/rate_limit.py +73 -0
  35. kairo/backend/core/security.py +29 -0
  36. kairo/backend/models/__init__.py +19 -0
  37. kairo/backend/models/agent.py +30 -0
  38. kairo/backend/models/api_key.py +25 -0
  39. kairo/backend/models/api_usage.py +29 -0
  40. kairo/backend/models/audit_log.py +26 -0
  41. kairo/backend/models/conversation.py +48 -0
  42. kairo/backend/models/device_code.py +30 -0
  43. kairo/backend/models/feature_flag.py +21 -0
  44. kairo/backend/models/image_generation.py +24 -0
  45. kairo/backend/models/incident.py +28 -0
  46. kairo/backend/models/project.py +28 -0
  47. kairo/backend/models/uptime_record.py +24 -0
  48. kairo/backend/models/usage.py +24 -0
  49. kairo/backend/models/user.py +49 -0
  50. kairo/backend/schemas/__init__.py +0 -0
  51. kairo/backend/schemas/admin/__init__.py +0 -0
  52. kairo/backend/schemas/admin/audit.py +28 -0
  53. kairo/backend/schemas/admin/content.py +53 -0
  54. kairo/backend/schemas/admin/stats.py +77 -0
  55. kairo/backend/schemas/admin/system.py +44 -0
  56. kairo/backend/schemas/admin/users.py +48 -0
  57. kairo/backend/schemas/agent.py +42 -0
  58. kairo/backend/schemas/api_key.py +30 -0
  59. kairo/backend/schemas/auth.py +57 -0
  60. kairo/backend/schemas/chat.py +26 -0
  61. kairo/backend/schemas/conversation.py +39 -0
  62. kairo/backend/schemas/device_auth.py +40 -0
  63. kairo/backend/schemas/image.py +15 -0
  64. kairo/backend/schemas/openai_compat.py +76 -0
  65. kairo/backend/schemas/project.py +21 -0
  66. kairo/backend/schemas/status.py +81 -0
  67. kairo/backend/schemas/usage.py +15 -0
  68. kairo/backend/services/__init__.py +0 -0
  69. kairo/backend/services/admin/__init__.py +0 -0
  70. kairo/backend/services/admin/audit_service.py +78 -0
  71. kairo/backend/services/admin/content_service.py +119 -0
  72. kairo/backend/services/admin/incident_service.py +94 -0
  73. kairo/backend/services/admin/stats_service.py +281 -0
  74. kairo/backend/services/admin/system_service.py +126 -0
  75. kairo/backend/services/admin/user_service.py +157 -0
  76. kairo/backend/services/agent_service.py +107 -0
  77. kairo/backend/services/api_key_service.py +66 -0
  78. kairo/backend/services/api_usage_service.py +126 -0
  79. kairo/backend/services/auth_service.py +101 -0
  80. kairo/backend/services/chat_service.py +501 -0
  81. kairo/backend/services/conversation_service.py +264 -0
  82. kairo/backend/services/device_auth_service.py +193 -0
  83. kairo/backend/services/email_service.py +55 -0
  84. kairo/backend/services/image_service.py +181 -0
  85. kairo/backend/services/llm_service.py +186 -0
  86. kairo/backend/services/project_service.py +109 -0
  87. kairo/backend/services/status_service.py +167 -0
  88. kairo/backend/services/stripe_service.py +78 -0
  89. kairo/backend/services/usage_service.py +150 -0
  90. kairo/backend/services/web_search_service.py +96 -0
  91. kairo/migrations/env.py +60 -0
  92. kairo/migrations/versions/001_initial.py +55 -0
  93. kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
  94. kairo/migrations/versions/003_username_to_email.py +21 -0
  95. kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
  96. kairo/migrations/versions/005_add_projects.py +52 -0
  97. kairo/migrations/versions/006_add_image_generation.py +63 -0
  98. kairo/migrations/versions/007_add_admin_portal.py +107 -0
  99. kairo/migrations/versions/008_add_device_code_auth.py +76 -0
  100. kairo/migrations/versions/009_add_status_page.py +65 -0
  101. kairo/tools/extract_claude_data.py +465 -0
  102. kairo/tools/filter_claude_data.py +303 -0
  103. kairo/tools/generate_curated_data.py +157 -0
  104. kairo/tools/mix_training_data.py +295 -0
  105. kairo_code/__init__.py +3 -0
  106. kairo_code/agents/__init__.py +25 -0
  107. kairo_code/agents/architect.py +98 -0
  108. kairo_code/agents/audit.py +100 -0
  109. kairo_code/agents/base.py +463 -0
  110. kairo_code/agents/coder.py +155 -0
  111. kairo_code/agents/database.py +77 -0
  112. kairo_code/agents/docs.py +88 -0
  113. kairo_code/agents/explorer.py +62 -0
  114. kairo_code/agents/guardian.py +80 -0
  115. kairo_code/agents/planner.py +66 -0
  116. kairo_code/agents/reviewer.py +91 -0
  117. kairo_code/agents/security.py +94 -0
  118. kairo_code/agents/terraform.py +88 -0
  119. kairo_code/agents/testing.py +97 -0
  120. kairo_code/agents/uiux.py +88 -0
  121. kairo_code/auth.py +232 -0
  122. kairo_code/config.py +172 -0
  123. kairo_code/conversation.py +173 -0
  124. kairo_code/heartbeat.py +63 -0
  125. kairo_code/llm.py +291 -0
  126. kairo_code/logging_config.py +156 -0
  127. kairo_code/main.py +818 -0
  128. kairo_code/router.py +217 -0
  129. kairo_code/sandbox.py +248 -0
  130. kairo_code/settings.py +183 -0
  131. kairo_code/tools/__init__.py +51 -0
  132. kairo_code/tools/analysis.py +509 -0
  133. kairo_code/tools/base.py +417 -0
  134. kairo_code/tools/code.py +58 -0
  135. kairo_code/tools/definitions.py +617 -0
  136. kairo_code/tools/files.py +315 -0
  137. kairo_code/tools/review.py +390 -0
  138. kairo_code/tools/search.py +185 -0
  139. kairo_code/ui.py +418 -0
  140. kairo_code-0.1.0.dist-info/METADATA +13 -0
  141. kairo_code-0.1.0.dist-info/RECORD +144 -0
  142. kairo_code-0.1.0.dist-info/WHEEL +5 -0
  143. kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
  144. kairo_code-0.1.0.dist-info/top_level.txt +4 -0
@@ -0,0 +1,125 @@
1
+ import logging
2
+
3
+ from fastapi import APIRouter, Depends, Query
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from backend.core.database import get_db
7
+ from backend.schemas.admin.stats import (
8
+ DailyCountResponse,
9
+ DailyTokensResponse,
10
+ OverviewStats,
11
+ PlanDistributionResponse,
12
+ RevenueStats,
13
+ TopUserAnalyticsResponse,
14
+ TopUserEntry,
15
+ UsageStatsEntry,
16
+ UserGrowthEntry,
17
+ )
18
+ from backend.services.admin.stats_service import AdminStatsService
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ router = APIRouter(prefix="/stats", tags=["admin-stats"])
23
+
24
+
25
+ @router.get("/overview", response_model=OverviewStats)
26
+ async def get_overview(db: AsyncSession = Depends(get_db)):
27
+ svc = AdminStatsService(db)
28
+ data = await svc.get_overview()
29
+ return OverviewStats(**data)
30
+
31
+
32
+ @router.get("/users", response_model=list[UserGrowthEntry])
33
+ async def get_user_growth(
34
+ days: int = Query(30, ge=1, le=365),
35
+ db: AsyncSession = Depends(get_db),
36
+ ):
37
+ svc = AdminStatsService(db)
38
+ data = await svc.get_user_growth(days=days)
39
+ return [UserGrowthEntry(**entry) for entry in data]
40
+
41
+
42
+ @router.get("/usage", response_model=list[UsageStatsEntry])
43
+ async def get_usage_stats(
44
+ days: int = Query(30, ge=1, le=365),
45
+ db: AsyncSession = Depends(get_db),
46
+ ):
47
+ svc = AdminStatsService(db)
48
+ data = await svc.get_usage_stats(days=days)
49
+ return [UsageStatsEntry(**entry) for entry in data]
50
+
51
+
52
+ @router.get("/revenue", response_model=RevenueStats)
53
+ async def get_revenue_stats(db: AsyncSession = Depends(get_db)):
54
+ svc = AdminStatsService(db)
55
+ data = await svc.get_revenue_stats()
56
+ return RevenueStats(**data)
57
+
58
+
59
+ @router.get("/top-users", response_model=list[TopUserEntry])
60
+ async def get_top_users(
61
+ limit: int = Query(20, ge=1, le=100),
62
+ db: AsyncSession = Depends(get_db),
63
+ ):
64
+ svc = AdminStatsService(db)
65
+ data = await svc.get_top_users(limit=limit)
66
+ return [TopUserEntry(**entry) for entry in data]
67
+
68
+
69
+ # ------------------------------------------------------------------ #
70
+ # Analytics chart / table endpoints #
71
+ # ------------------------------------------------------------------ #
72
+
73
+
74
+ @router.get("/signups", response_model=DailyCountResponse)
75
+ async def get_signups(
76
+ days: int = Query(30, ge=1, le=365),
77
+ db: AsyncSession = Depends(get_db),
78
+ ):
79
+ """Daily signup counts for the past N days (zero-filled)."""
80
+ svc = AdminStatsService(db)
81
+ data = await svc.get_daily_signups(days=days)
82
+ return DailyCountResponse(data=data)
83
+
84
+
85
+ @router.get("/active-users", response_model=DailyCountResponse)
86
+ async def get_active_users(
87
+ days: int = Query(30, ge=1, le=365),
88
+ db: AsyncSession = Depends(get_db),
89
+ ):
90
+ """Daily active user counts (users with usage records) for the past N days."""
91
+ svc = AdminStatsService(db)
92
+ data = await svc.get_daily_active_users(days=days)
93
+ return DailyCountResponse(data=data)
94
+
95
+
96
+ @router.get("/usage-over-time", response_model=DailyTokensResponse)
97
+ async def get_usage_over_time(
98
+ days: int = Query(30, ge=1, le=365),
99
+ db: AsyncSession = Depends(get_db),
100
+ ):
101
+ """Daily total tokens used for the past N days (zero-filled)."""
102
+ svc = AdminStatsService(db)
103
+ data = await svc.get_usage_over_time(days=days)
104
+ return DailyTokensResponse(data=data)
105
+
106
+
107
+ @router.get("/plan-distribution", response_model=PlanDistributionResponse)
108
+ async def get_plan_distribution(
109
+ db: AsyncSession = Depends(get_db),
110
+ ):
111
+ """Current count of users per plan."""
112
+ svc = AdminStatsService(db)
113
+ data = await svc.get_plan_distribution()
114
+ return PlanDistributionResponse(data=data)
115
+
116
+
117
+ @router.get("/top-users/analytics", response_model=TopUserAnalyticsResponse)
118
+ async def get_top_users_analytics(
119
+ limit: int = Query(20, ge=1, le=100),
120
+ db: AsyncSession = Depends(get_db),
121
+ ):
122
+ """Top N users by token usage with conversation and image counts."""
123
+ svc = AdminStatsService(db)
124
+ data = await svc.get_top_users_analytics(limit=limit)
125
+ return TopUserAnalyticsResponse(data=data)
@@ -0,0 +1,87 @@
1
+ import logging
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, Request
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from backend.core.admin_auth import require_role
7
+ from backend.core.database import get_db
8
+ from backend.models.user import User
9
+ from backend.schemas.admin.system import (
10
+ ConfigEntry,
11
+ ConfigResponse,
12
+ FeatureFlagItem,
13
+ FeatureFlagResponse,
14
+ FeatureFlagUpdate,
15
+ HealthStatus,
16
+ )
17
+ from backend.services.admin.audit_service import AuditService
18
+ from backend.services.admin.system_service import AdminSystemService
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ router = APIRouter(prefix="/system", tags=["admin-system"])
23
+
24
+
25
+ @router.get("/health", response_model=HealthStatus)
26
+ async def get_health(db: AsyncSession = Depends(get_db)):
27
+ svc = AdminSystemService(db)
28
+ data = await svc.get_health()
29
+ return HealthStatus(**data)
30
+
31
+
32
+ @router.get("/feature-flags", response_model=FeatureFlagResponse)
33
+ async def get_feature_flags(db: AsyncSession = Depends(get_db)):
34
+ svc = AdminSystemService(db)
35
+ flags = await svc.get_feature_flags()
36
+ items = []
37
+ for f in flags:
38
+ items.append(FeatureFlagItem(
39
+ key=f.key,
40
+ enabled=f.enabled,
41
+ updated_by=f.updated_by,
42
+ updated_at=f.updated_at.isoformat() if f.updated_at else None,
43
+ ))
44
+ return FeatureFlagResponse(flags=items)
45
+
46
+
47
+ @router.patch(
48
+ "/feature-flags/{key}",
49
+ dependencies=[Depends(require_role("superadmin"))],
50
+ )
51
+ async def toggle_feature_flag(
52
+ key: str,
53
+ body: FeatureFlagUpdate,
54
+ request: Request,
55
+ db: AsyncSession = Depends(get_db),
56
+ admin: User = Depends(require_role("superadmin")),
57
+ ):
58
+ svc = AdminSystemService(db)
59
+ flag = await svc.toggle_feature_flag(key, body.enabled, admin.id)
60
+ if not flag:
61
+ raise HTTPException(status_code=404, detail="Feature flag not found")
62
+
63
+ audit = AuditService(db)
64
+ ip = request.client.host if request.client else "unknown"
65
+ ua = request.headers.get("user-agent", "")
66
+ await audit.log(
67
+ admin_user_id=admin.id,
68
+ action="system.feature_flag_toggled",
69
+ target_type="feature_flag",
70
+ target_id=key,
71
+ details={"enabled": body.enabled, "reason": body.reason},
72
+ ip_address=ip,
73
+ user_agent=ua,
74
+ )
75
+ return FeatureFlagItem(
76
+ key=flag.key,
77
+ enabled=flag.enabled,
78
+ updated_by=flag.updated_by,
79
+ updated_at=flag.updated_at.isoformat() if flag.updated_at else None,
80
+ )
81
+
82
+
83
+ @router.get("/config", response_model=ConfigResponse)
84
+ async def get_config(db: AsyncSession = Depends(get_db)):
85
+ svc = AdminSystemService(db)
86
+ entries = await svc.get_config()
87
+ return ConfigResponse(config=[ConfigEntry(**e) for e in entries])
@@ -0,0 +1,279 @@
1
+ import logging
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from backend.core.admin_auth import require_role
7
+ from backend.core.database import get_db
8
+ from backend.models.user import User
9
+ from backend.schemas.admin.users import (
10
+ AdminUserDetailResponse,
11
+ AdminUserListItem,
12
+ PlanChangeRequest,
13
+ RoleChangeRequest,
14
+ StatusChangeRequest,
15
+ )
16
+ from backend.services.admin.audit_service import AuditService
17
+ from backend.services.admin.user_service import AdminUserService
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ router = APIRouter(prefix="/users", tags=["admin-users"])
22
+
23
+
24
+ def _get_ip(request: Request) -> str:
25
+ return request.client.host if request.client else "unknown"
26
+
27
+
28
+ def _get_ua(request: Request) -> str:
29
+ return request.headers.get("user-agent", "")
30
+
31
+
32
+ @router.get("")
33
+ async def list_users(
34
+ search: str | None = Query(None),
35
+ plan: str | None = Query(None),
36
+ status: str | None = Query(None),
37
+ cursor: str | None = Query(None),
38
+ limit: int = Query(25, ge=1, le=100),
39
+ db: AsyncSession = Depends(get_db),
40
+ ):
41
+ svc = AdminUserService(db)
42
+ users = await svc.list_users(search=search, plan=plan, status=status, cursor=cursor, limit=limit + 1)
43
+ has_more = len(users) > limit
44
+ if has_more:
45
+ users = users[:limit]
46
+ items = [AdminUserListItem.model_validate(u) for u in users]
47
+ next_cursor = users[-1].id if has_more else None
48
+ return {"items": items, "next_cursor": next_cursor}
49
+
50
+
51
+ @router.get("/{user_id}", response_model=AdminUserDetailResponse)
52
+ async def get_user_detail(
53
+ user_id: str,
54
+ request: Request,
55
+ db: AsyncSession = Depends(get_db),
56
+ admin: User = Depends(require_role("admin")),
57
+ ):
58
+ svc = AdminUserService(db)
59
+ user = await svc.get_user_detail(user_id)
60
+ if not user:
61
+ raise HTTPException(status_code=404, detail="User not found")
62
+
63
+ # Audit the read
64
+ audit = AuditService(db)
65
+ await audit.log(
66
+ admin_user_id=admin.id,
67
+ action="user.detail_viewed",
68
+ target_type="user",
69
+ target_id=user_id,
70
+ ip_address=_get_ip(request),
71
+ user_agent=_get_ua(request),
72
+ )
73
+ return AdminUserDetailResponse.model_validate(user)
74
+
75
+
76
+ @router.patch("/{user_id}/plan", response_model=AdminUserDetailResponse)
77
+ async def change_user_plan(
78
+ user_id: str,
79
+ body: PlanChangeRequest,
80
+ request: Request,
81
+ db: AsyncSession = Depends(get_db),
82
+ admin: User = Depends(require_role("admin")),
83
+ ):
84
+ svc = AdminUserService(db)
85
+ old_user = await svc.get_user_detail(user_id)
86
+ if not old_user:
87
+ raise HTTPException(status_code=404, detail="User not found")
88
+
89
+ old_plan = old_user.plan
90
+ user = await svc.change_plan(user_id, body.plan)
91
+
92
+ audit = AuditService(db)
93
+ await audit.log(
94
+ admin_user_id=admin.id,
95
+ action="user.plan_changed",
96
+ target_type="user",
97
+ target_id=user_id,
98
+ details={"old_plan": old_plan, "new_plan": body.plan, "reason": body.reason},
99
+ ip_address=_get_ip(request),
100
+ user_agent=_get_ua(request),
101
+ )
102
+ return AdminUserDetailResponse.model_validate(user)
103
+
104
+
105
+ @router.patch("/{user_id}/status", response_model=AdminUserDetailResponse)
106
+ async def change_user_status(
107
+ user_id: str,
108
+ body: StatusChangeRequest,
109
+ request: Request,
110
+ db: AsyncSession = Depends(get_db),
111
+ admin: User = Depends(require_role("admin")),
112
+ ):
113
+ svc = AdminUserService(db)
114
+ old_user = await svc.get_user_detail(user_id)
115
+ if not old_user:
116
+ raise HTTPException(status_code=404, detail="User not found")
117
+
118
+ old_status = getattr(old_user, "status", "active")
119
+ user = await svc.change_status(user_id, body.status)
120
+
121
+ audit = AuditService(db)
122
+ await audit.log(
123
+ admin_user_id=admin.id,
124
+ action="user.status_changed",
125
+ target_type="user",
126
+ target_id=user_id,
127
+ details={"old_status": old_status, "new_status": body.status, "reason": body.reason},
128
+ ip_address=_get_ip(request),
129
+ user_agent=_get_ua(request),
130
+ )
131
+ return AdminUserDetailResponse.model_validate(user)
132
+
133
+
134
+ @router.patch(
135
+ "/{user_id}/role",
136
+ response_model=AdminUserDetailResponse,
137
+ dependencies=[Depends(require_role("superadmin"))],
138
+ )
139
+ async def change_user_role(
140
+ user_id: str,
141
+ body: RoleChangeRequest,
142
+ request: Request,
143
+ db: AsyncSession = Depends(get_db),
144
+ admin: User = Depends(require_role("superadmin")),
145
+ ):
146
+ svc = AdminUserService(db)
147
+ old_user = await svc.get_user_detail(user_id)
148
+ if not old_user:
149
+ raise HTTPException(status_code=404, detail="User not found")
150
+
151
+ old_role = getattr(old_user, "role", "user")
152
+ user = await svc.change_role(user_id, body.role)
153
+
154
+ audit = AuditService(db)
155
+ await audit.log(
156
+ admin_user_id=admin.id,
157
+ action="user.role_changed",
158
+ target_type="user",
159
+ target_id=user_id,
160
+ details={"old_role": old_role, "new_role": body.role, "reason": body.reason},
161
+ ip_address=_get_ip(request),
162
+ user_agent=_get_ua(request),
163
+ )
164
+ return AdminUserDetailResponse.model_validate(user)
165
+
166
+
167
+ @router.get("/{user_id}/usage")
168
+ async def get_user_usage(
169
+ user_id: str,
170
+ request: Request,
171
+ db: AsyncSession = Depends(get_db),
172
+ admin: User = Depends(require_role("admin")),
173
+ ):
174
+ svc = AdminUserService(db)
175
+ user = await svc.get_user_detail(user_id)
176
+ if not user:
177
+ raise HTTPException(status_code=404, detail="User not found")
178
+
179
+ audit = AuditService(db)
180
+ await audit.log(
181
+ admin_user_id=admin.id,
182
+ action="user.usage_viewed",
183
+ target_type="user",
184
+ target_id=user_id,
185
+ ip_address=_get_ip(request),
186
+ user_agent=_get_ua(request),
187
+ )
188
+ return await svc.get_user_usage(user_id)
189
+
190
+
191
+ @router.get("/{user_id}/conversations")
192
+ async def get_user_conversations(
193
+ user_id: str,
194
+ request: Request,
195
+ cursor: str | None = Query(None),
196
+ limit: int = Query(50, ge=1, le=100),
197
+ db: AsyncSession = Depends(get_db),
198
+ admin: User = Depends(require_role("admin")),
199
+ ):
200
+ svc = AdminUserService(db)
201
+ user = await svc.get_user_detail(user_id)
202
+ if not user:
203
+ raise HTTPException(status_code=404, detail="User not found")
204
+
205
+ audit = AuditService(db)
206
+ await audit.log(
207
+ admin_user_id=admin.id,
208
+ action="user.conversations_viewed",
209
+ target_type="user",
210
+ target_id=user_id,
211
+ ip_address=_get_ip(request),
212
+ user_agent=_get_ua(request),
213
+ )
214
+
215
+ conversations = await svc.get_user_conversations(user_id, cursor=cursor, limit=limit)
216
+ return [
217
+ {
218
+ "id": c.id,
219
+ "title": c.title,
220
+ "model": c.model,
221
+ "created_at": c.created_at,
222
+ "updated_at": c.updated_at,
223
+ }
224
+ for c in conversations
225
+ ]
226
+
227
+
228
+ @router.get("/{user_id}/images")
229
+ async def get_user_images(
230
+ user_id: str,
231
+ request: Request,
232
+ cursor: str | None = Query(None),
233
+ limit: int = Query(50, ge=1, le=100),
234
+ db: AsyncSession = Depends(get_db),
235
+ admin: User = Depends(require_role("admin")),
236
+ ):
237
+ svc = AdminUserService(db)
238
+ user = await svc.get_user_detail(user_id)
239
+ if not user:
240
+ raise HTTPException(status_code=404, detail="User not found")
241
+
242
+ audit = AuditService(db)
243
+ await audit.log(
244
+ admin_user_id=admin.id,
245
+ action="user.images_viewed",
246
+ target_type="user",
247
+ target_id=user_id,
248
+ ip_address=_get_ip(request),
249
+ user_agent=_get_ua(request),
250
+ )
251
+
252
+ from backend.schemas.admin.content import AdminImageListItem
253
+
254
+ images = await svc.get_user_images(user_id, cursor=cursor, limit=limit)
255
+ return [AdminImageListItem.model_validate(img) for img in images]
256
+
257
+
258
+ @router.get("/{user_id}/api-keys")
259
+ async def get_user_api_keys(
260
+ user_id: str,
261
+ request: Request,
262
+ db: AsyncSession = Depends(get_db),
263
+ admin: User = Depends(require_role("admin")),
264
+ ):
265
+ svc = AdminUserService(db)
266
+ user = await svc.get_user_detail(user_id)
267
+ if not user:
268
+ raise HTTPException(status_code=404, detail="User not found")
269
+
270
+ audit = AuditService(db)
271
+ await audit.log(
272
+ admin_user_id=admin.id,
273
+ action="user.api_keys_viewed",
274
+ target_type="user",
275
+ target_id=user_id,
276
+ ip_address=_get_ip(request),
277
+ user_agent=_get_ua(request),
278
+ )
279
+ return await svc.get_user_api_keys(user_id)
@@ -0,0 +1,94 @@
1
+ from datetime import datetime, UTC
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException
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
+ RegisterAgentRequest,
17
+ UpdateAgentRequest,
18
+ )
19
+ from backend.services.agent_service import AgentService
20
+
21
+ router = APIRouter(prefix="/agents", tags=["Agents"])
22
+
23
+
24
+ @router.post("/", response_model=AgentResponse)
25
+ async def register_agent(
26
+ req: RegisterAgentRequest,
27
+ user: User = Depends(get_current_user),
28
+ db: AsyncSession = Depends(get_db),
29
+ ):
30
+ if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
31
+ raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
32
+ svc = AgentService(db)
33
+ agent = await svc.register(user.id, req)
34
+ return agent
35
+
36
+
37
+ @router.get("/", response_model=list[AgentResponse])
38
+ async def list_agents(
39
+ user: User = Depends(get_current_user),
40
+ db: AsyncSession = Depends(get_db),
41
+ ):
42
+ if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
43
+ raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
44
+ svc = AgentService(db)
45
+ agents = await svc.list_agents(user.id)
46
+ return agents
47
+
48
+
49
+ @router.patch("/{agent_id}", response_model=AgentResponse)
50
+ async def update_agent(
51
+ agent_id: str,
52
+ req: UpdateAgentRequest,
53
+ user: User = Depends(get_current_user),
54
+ db: AsyncSession = Depends(get_db),
55
+ ):
56
+ if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
57
+ raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
58
+ svc = AgentService(db)
59
+ agent = await svc.update_agent(user.id, agent_id, req)
60
+ if not agent:
61
+ raise HTTPException(status_code=404, detail="Agent not found")
62
+ return agent
63
+
64
+
65
+ @router.delete("/{agent_id}")
66
+ async def delete_agent(
67
+ agent_id: str,
68
+ user: User = Depends(get_current_user),
69
+ db: AsyncSession = Depends(get_db),
70
+ ):
71
+ if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
72
+ raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
73
+ svc = AgentService(db)
74
+ deleted = await svc.delete_agent(user.id, agent_id)
75
+ if not deleted:
76
+ raise HTTPException(status_code=404, detail="Agent not found")
77
+ return {"deleted": True}
78
+
79
+
80
+ @router.post("/heartbeat", response_model=AgentHeartbeatResponse)
81
+ async def agent_heartbeat(
82
+ req: AgentHeartbeatRequest,
83
+ auth: tuple[User, ApiKey] = Depends(get_api_key_user),
84
+ db: AsyncSession = Depends(get_db),
85
+ ):
86
+ """Agent heartbeat — authenticated via API key, not JWT."""
87
+ if not settings.FEATURE_KAIRO_AGENTS_ENABLED:
88
+ raise HTTPException(status_code=503, detail="Kairo Agents is coming soon.")
89
+ user, _api_key = auth
90
+ svc = AgentService(db)
91
+ agent = await svc.heartbeat(req.agent_id, user.id, req.status)
92
+ if not agent:
93
+ raise HTTPException(status_code=404, detail="Agent not found or not owned by this key's user")
94
+ return AgentHeartbeatResponse(acknowledged=True, server_time=datetime.now(UTC))
@@ -0,0 +1,85 @@
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+
4
+ from backend.config import settings
5
+ from backend.core.database import get_db
6
+ from backend.core.dependencies import get_current_user
7
+ from backend.models.user import User
8
+ from backend.schemas.api_key import (
9
+ ApiKeyCreatedResponse,
10
+ ApiKeyResponse,
11
+ CreateApiKeyRequest,
12
+ RevokeApiKeyResponse,
13
+ )
14
+ from backend.services.api_key_service import ApiKeyService
15
+ from backend.services.api_usage_service import ApiUsageService
16
+
17
+ router = APIRouter(prefix="/api-keys", tags=["API Keys"])
18
+
19
+
20
+ @router.post("/", response_model=ApiKeyCreatedResponse)
21
+ async def create_api_key(
22
+ req: CreateApiKeyRequest,
23
+ user: User = Depends(get_current_user),
24
+ db: AsyncSession = Depends(get_db),
25
+ ):
26
+ if not settings.FEATURE_KAIRO_API_ENABLED:
27
+ raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
28
+ svc = ApiKeyService(db)
29
+ api_key, raw_key = await svc.create_key(user.id, req.name, req.expires_in_days)
30
+ return ApiKeyCreatedResponse(
31
+ id=api_key.id,
32
+ name=api_key.name,
33
+ key_prefix=api_key.key_prefix,
34
+ is_active=api_key.is_active,
35
+ last_used_at=api_key.last_used_at,
36
+ created_at=api_key.created_at,
37
+ expires_at=api_key.expires_at,
38
+ full_key=raw_key,
39
+ )
40
+
41
+
42
+ @router.get("/", response_model=list[ApiKeyResponse])
43
+ async def list_api_keys(
44
+ user: User = Depends(get_current_user),
45
+ db: AsyncSession = Depends(get_db),
46
+ ):
47
+ if not settings.FEATURE_KAIRO_API_ENABLED:
48
+ raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
49
+ svc = ApiKeyService(db)
50
+ keys = await svc.list_keys(user.id)
51
+ return keys
52
+
53
+
54
+ @router.delete("/{key_id}", response_model=RevokeApiKeyResponse)
55
+ async def revoke_api_key(
56
+ key_id: str,
57
+ user: User = Depends(get_current_user),
58
+ db: AsyncSession = Depends(get_db),
59
+ ):
60
+ if not settings.FEATURE_KAIRO_API_ENABLED:
61
+ raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
62
+ svc = ApiKeyService(db)
63
+ revoked = await svc.revoke_key(user.id, key_id)
64
+ if not revoked:
65
+ raise HTTPException(status_code=404, detail="API key not found")
66
+ return RevokeApiKeyResponse(id=key_id, revoked=True)
67
+
68
+
69
+ @router.get("/{key_id}/usage")
70
+ async def get_key_usage(
71
+ key_id: str,
72
+ days: int = 30,
73
+ user: User = Depends(get_current_user),
74
+ db: AsyncSession = Depends(get_db),
75
+ ):
76
+ if not settings.FEATURE_KAIRO_API_ENABLED:
77
+ raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
78
+ # Verify key belongs to user
79
+ key_svc = ApiKeyService(db)
80
+ keys = await key_svc.list_keys(user.id)
81
+ if not any(k.id == key_id for k in keys):
82
+ raise HTTPException(status_code=404, detail="API key not found")
83
+ usage_svc = ApiUsageService(db)
84
+ history = await usage_svc.get_key_usage_summary(key_id, days)
85
+ return {"usage": history}