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.
- image-service/main.py +178 -0
- infra/chat/app/main.py +84 -0
- kairo/backend/__init__.py +0 -0
- kairo/backend/api/__init__.py +0 -0
- kairo/backend/api/admin/__init__.py +23 -0
- kairo/backend/api/admin/audit.py +54 -0
- kairo/backend/api/admin/content.py +142 -0
- kairo/backend/api/admin/incidents.py +148 -0
- kairo/backend/api/admin/stats.py +125 -0
- kairo/backend/api/admin/system.py +87 -0
- kairo/backend/api/admin/users.py +279 -0
- kairo/backend/api/agents.py +94 -0
- kairo/backend/api/api_keys.py +85 -0
- kairo/backend/api/auth.py +116 -0
- kairo/backend/api/billing.py +41 -0
- kairo/backend/api/chat.py +72 -0
- kairo/backend/api/conversations.py +125 -0
- kairo/backend/api/device_auth.py +100 -0
- kairo/backend/api/files.py +83 -0
- kairo/backend/api/health.py +36 -0
- kairo/backend/api/images.py +80 -0
- kairo/backend/api/openai_compat.py +225 -0
- kairo/backend/api/projects.py +102 -0
- kairo/backend/api/usage.py +32 -0
- kairo/backend/api/webhooks.py +79 -0
- kairo/backend/app.py +297 -0
- kairo/backend/config.py +179 -0
- kairo/backend/core/__init__.py +0 -0
- kairo/backend/core/admin_auth.py +24 -0
- kairo/backend/core/api_key_auth.py +55 -0
- kairo/backend/core/database.py +28 -0
- kairo/backend/core/dependencies.py +70 -0
- kairo/backend/core/logging.py +23 -0
- kairo/backend/core/rate_limit.py +73 -0
- kairo/backend/core/security.py +29 -0
- kairo/backend/models/__init__.py +19 -0
- kairo/backend/models/agent.py +30 -0
- kairo/backend/models/api_key.py +25 -0
- kairo/backend/models/api_usage.py +29 -0
- kairo/backend/models/audit_log.py +26 -0
- kairo/backend/models/conversation.py +48 -0
- kairo/backend/models/device_code.py +30 -0
- kairo/backend/models/feature_flag.py +21 -0
- kairo/backend/models/image_generation.py +24 -0
- kairo/backend/models/incident.py +28 -0
- kairo/backend/models/project.py +28 -0
- kairo/backend/models/uptime_record.py +24 -0
- kairo/backend/models/usage.py +24 -0
- kairo/backend/models/user.py +49 -0
- kairo/backend/schemas/__init__.py +0 -0
- kairo/backend/schemas/admin/__init__.py +0 -0
- kairo/backend/schemas/admin/audit.py +28 -0
- kairo/backend/schemas/admin/content.py +53 -0
- kairo/backend/schemas/admin/stats.py +77 -0
- kairo/backend/schemas/admin/system.py +44 -0
- kairo/backend/schemas/admin/users.py +48 -0
- kairo/backend/schemas/agent.py +42 -0
- kairo/backend/schemas/api_key.py +30 -0
- kairo/backend/schemas/auth.py +57 -0
- kairo/backend/schemas/chat.py +26 -0
- kairo/backend/schemas/conversation.py +39 -0
- kairo/backend/schemas/device_auth.py +40 -0
- kairo/backend/schemas/image.py +15 -0
- kairo/backend/schemas/openai_compat.py +76 -0
- kairo/backend/schemas/project.py +21 -0
- kairo/backend/schemas/status.py +81 -0
- kairo/backend/schemas/usage.py +15 -0
- kairo/backend/services/__init__.py +0 -0
- kairo/backend/services/admin/__init__.py +0 -0
- kairo/backend/services/admin/audit_service.py +78 -0
- kairo/backend/services/admin/content_service.py +119 -0
- kairo/backend/services/admin/incident_service.py +94 -0
- kairo/backend/services/admin/stats_service.py +281 -0
- kairo/backend/services/admin/system_service.py +126 -0
- kairo/backend/services/admin/user_service.py +157 -0
- kairo/backend/services/agent_service.py +107 -0
- kairo/backend/services/api_key_service.py +66 -0
- kairo/backend/services/api_usage_service.py +126 -0
- kairo/backend/services/auth_service.py +101 -0
- kairo/backend/services/chat_service.py +501 -0
- kairo/backend/services/conversation_service.py +264 -0
- kairo/backend/services/device_auth_service.py +193 -0
- kairo/backend/services/email_service.py +55 -0
- kairo/backend/services/image_service.py +181 -0
- kairo/backend/services/llm_service.py +186 -0
- kairo/backend/services/project_service.py +109 -0
- kairo/backend/services/status_service.py +167 -0
- kairo/backend/services/stripe_service.py +78 -0
- kairo/backend/services/usage_service.py +150 -0
- kairo/backend/services/web_search_service.py +96 -0
- kairo/migrations/env.py +60 -0
- kairo/migrations/versions/001_initial.py +55 -0
- kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
- kairo/migrations/versions/003_username_to_email.py +21 -0
- kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
- kairo/migrations/versions/005_add_projects.py +52 -0
- kairo/migrations/versions/006_add_image_generation.py +63 -0
- kairo/migrations/versions/007_add_admin_portal.py +107 -0
- kairo/migrations/versions/008_add_device_code_auth.py +76 -0
- kairo/migrations/versions/009_add_status_page.py +65 -0
- kairo/tools/extract_claude_data.py +465 -0
- kairo/tools/filter_claude_data.py +303 -0
- kairo/tools/generate_curated_data.py +157 -0
- kairo/tools/mix_training_data.py +295 -0
- kairo_code/__init__.py +3 -0
- kairo_code/agents/__init__.py +25 -0
- kairo_code/agents/architect.py +98 -0
- kairo_code/agents/audit.py +100 -0
- kairo_code/agents/base.py +463 -0
- kairo_code/agents/coder.py +155 -0
- kairo_code/agents/database.py +77 -0
- kairo_code/agents/docs.py +88 -0
- kairo_code/agents/explorer.py +62 -0
- kairo_code/agents/guardian.py +80 -0
- kairo_code/agents/planner.py +66 -0
- kairo_code/agents/reviewer.py +91 -0
- kairo_code/agents/security.py +94 -0
- kairo_code/agents/terraform.py +88 -0
- kairo_code/agents/testing.py +97 -0
- kairo_code/agents/uiux.py +88 -0
- kairo_code/auth.py +232 -0
- kairo_code/config.py +172 -0
- kairo_code/conversation.py +173 -0
- kairo_code/heartbeat.py +63 -0
- kairo_code/llm.py +291 -0
- kairo_code/logging_config.py +156 -0
- kairo_code/main.py +818 -0
- kairo_code/router.py +217 -0
- kairo_code/sandbox.py +248 -0
- kairo_code/settings.py +183 -0
- kairo_code/tools/__init__.py +51 -0
- kairo_code/tools/analysis.py +509 -0
- kairo_code/tools/base.py +417 -0
- kairo_code/tools/code.py +58 -0
- kairo_code/tools/definitions.py +617 -0
- kairo_code/tools/files.py +315 -0
- kairo_code/tools/review.py +390 -0
- kairo_code/tools/search.py +185 -0
- kairo_code/ui.py +418 -0
- kairo_code-0.1.0.dist-info/METADATA +13 -0
- kairo_code-0.1.0.dist-info/RECORD +144 -0
- kairo_code-0.1.0.dist-info/WHEEL +5 -0
- kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
- kairo_code-0.1.0.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""OpenAI-compatible API endpoint — mounted at /v1/ for SDK compatibility."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
9
|
+
from fastapi.responses import StreamingResponse
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
from backend.config import settings
|
|
13
|
+
from backend.core.api_key_auth import get_api_key_user
|
|
14
|
+
from backend.core.database import get_db
|
|
15
|
+
from backend.core.rate_limit import rate_limit_api
|
|
16
|
+
from backend.models.api_key import ApiKey
|
|
17
|
+
from backend.models.user import User
|
|
18
|
+
from backend.schemas.openai_compat import (
|
|
19
|
+
ChatCompletionChunk,
|
|
20
|
+
ChatCompletionRequest,
|
|
21
|
+
ChatCompletionResponse,
|
|
22
|
+
ChatCompletionChoice,
|
|
23
|
+
ChatMessage,
|
|
24
|
+
ChoiceDelta,
|
|
25
|
+
StreamChoice,
|
|
26
|
+
UsageInfo,
|
|
27
|
+
)
|
|
28
|
+
from backend.services.api_usage_service import ApiUsageService
|
|
29
|
+
from backend.services.llm_service import LLMService
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
router = APIRouter(prefix="/v1", tags=["OpenAI Compatible"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_llm_service(request: Request) -> LLMService:
|
|
37
|
+
return request.app.state.llm_service
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.post("/chat/completions")
|
|
41
|
+
async def chat_completions(
|
|
42
|
+
req: ChatCompletionRequest,
|
|
43
|
+
request: Request,
|
|
44
|
+
auth: tuple[User, ApiKey] = Depends(get_api_key_user),
|
|
45
|
+
db: AsyncSession = Depends(get_db),
|
|
46
|
+
llm_service: LLMService = Depends(_get_llm_service),
|
|
47
|
+
_rate: None = Depends(rate_limit_api),
|
|
48
|
+
):
|
|
49
|
+
if not settings.FEATURE_KAIRO_API_ENABLED:
|
|
50
|
+
raise HTTPException(status_code=503, detail="Kairo API is coming soon.")
|
|
51
|
+
|
|
52
|
+
user, api_key = auth
|
|
53
|
+
|
|
54
|
+
# Check API usage limits
|
|
55
|
+
usage_svc = ApiUsageService(db)
|
|
56
|
+
allowed, reason = await usage_svc.check_limits(user.id)
|
|
57
|
+
if not allowed:
|
|
58
|
+
raise HTTPException(status_code=429, detail=reason)
|
|
59
|
+
|
|
60
|
+
# Resolve model
|
|
61
|
+
model_id = llm_service.resolve_model(req.model)
|
|
62
|
+
client, base_url, resolved_model, _is_fallback = llm_service._select_backend(req.model)
|
|
63
|
+
|
|
64
|
+
# Build vLLM payload — serialize full request, override model with resolved name
|
|
65
|
+
payload = req.model_dump(exclude_none=True)
|
|
66
|
+
payload["model"] = resolved_model
|
|
67
|
+
|
|
68
|
+
request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
|
69
|
+
created = int(time.time())
|
|
70
|
+
|
|
71
|
+
if req.stream:
|
|
72
|
+
payload["stream_options"] = {"include_usage": True}
|
|
73
|
+
return StreamingResponse(
|
|
74
|
+
_stream_proxy(
|
|
75
|
+
client, base_url, payload, request_id, created, resolved_model,
|
|
76
|
+
api_key.id, user.id, db,
|
|
77
|
+
),
|
|
78
|
+
media_type="text/event-stream",
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
# Non-streaming: proxy full request
|
|
82
|
+
try:
|
|
83
|
+
resp = await client.post(f"{base_url}/v1/chat/completions", json=payload)
|
|
84
|
+
resp.raise_for_status()
|
|
85
|
+
data = resp.json()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error("vLLM proxy error: %s", e)
|
|
88
|
+
raise HTTPException(status_code=502, detail="Inference backend unavailable")
|
|
89
|
+
|
|
90
|
+
# Record usage
|
|
91
|
+
usage = data.get("usage", {})
|
|
92
|
+
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
93
|
+
completion_tokens = usage.get("completion_tokens", 0)
|
|
94
|
+
await usage_svc.record(
|
|
95
|
+
api_key_id=api_key.id, user_id=user.id, model=resolved_model,
|
|
96
|
+
prompt_tokens=prompt_tokens, completion_tokens=completion_tokens,
|
|
97
|
+
endpoint="/v1/chat/completions",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Reformat as our response — forward all message fields including tool_calls
|
|
101
|
+
choices = []
|
|
102
|
+
for c in data.get("choices", []):
|
|
103
|
+
msg = c.get("message", {})
|
|
104
|
+
choices.append(ChatCompletionChoice(
|
|
105
|
+
index=c.get("index", 0),
|
|
106
|
+
message=ChatMessage(
|
|
107
|
+
role=msg.get("role", "assistant"),
|
|
108
|
+
content=msg.get("content"),
|
|
109
|
+
name=msg.get("name"),
|
|
110
|
+
tool_calls=msg.get("tool_calls"),
|
|
111
|
+
tool_call_id=msg.get("tool_call_id"),
|
|
112
|
+
),
|
|
113
|
+
finish_reason=c.get("finish_reason"),
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
return ChatCompletionResponse(
|
|
117
|
+
id=request_id, created=created, model=resolved_model,
|
|
118
|
+
choices=choices,
|
|
119
|
+
usage=UsageInfo(
|
|
120
|
+
prompt_tokens=prompt_tokens,
|
|
121
|
+
completion_tokens=completion_tokens,
|
|
122
|
+
total_tokens=prompt_tokens + completion_tokens,
|
|
123
|
+
) if usage else None,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def _stream_proxy(
|
|
128
|
+
client, base_url, payload, request_id, created, model,
|
|
129
|
+
api_key_id, user_id, db,
|
|
130
|
+
):
|
|
131
|
+
"""Stream SSE from vLLM, relay in OpenAI format, record usage at end."""
|
|
132
|
+
prompt_tokens = 0
|
|
133
|
+
completion_tokens = 0
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
async with client.stream("POST", f"{base_url}/v1/chat/completions", json=payload) as response:
|
|
137
|
+
response.raise_for_status()
|
|
138
|
+
async for line in response.aiter_lines():
|
|
139
|
+
if not line.startswith("data: "):
|
|
140
|
+
continue
|
|
141
|
+
data_str = line[6:]
|
|
142
|
+
if data_str.strip() == "[DONE]":
|
|
143
|
+
break
|
|
144
|
+
try:
|
|
145
|
+
chunk = json.loads(data_str)
|
|
146
|
+
# Capture usage
|
|
147
|
+
if "usage" in chunk and chunk["usage"]:
|
|
148
|
+
prompt_tokens = chunk["usage"].get("prompt_tokens", 0)
|
|
149
|
+
completion_tokens = chunk["usage"].get("completion_tokens", 0)
|
|
150
|
+
|
|
151
|
+
choices = chunk.get("choices", [])
|
|
152
|
+
if not choices:
|
|
153
|
+
# Emit usage-only final chunk if present
|
|
154
|
+
if prompt_tokens or completion_tokens:
|
|
155
|
+
usage_chunk = ChatCompletionChunk(
|
|
156
|
+
id=request_id, created=created, model=model,
|
|
157
|
+
choices=[],
|
|
158
|
+
usage=UsageInfo(
|
|
159
|
+
prompt_tokens=prompt_tokens,
|
|
160
|
+
completion_tokens=completion_tokens,
|
|
161
|
+
total_tokens=prompt_tokens + completion_tokens,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
yield f"data: {usage_chunk.model_dump_json()}\n\n"
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# Forward all choices, including tool_calls deltas
|
|
168
|
+
stream_choices = []
|
|
169
|
+
for c in choices:
|
|
170
|
+
delta = c.get("delta", {})
|
|
171
|
+
stream_choices.append(StreamChoice(
|
|
172
|
+
index=c.get("index", 0),
|
|
173
|
+
delta=ChoiceDelta(
|
|
174
|
+
role=delta.get("role"),
|
|
175
|
+
content=delta.get("content"),
|
|
176
|
+
tool_calls=delta.get("tool_calls"),
|
|
177
|
+
),
|
|
178
|
+
finish_reason=c.get("finish_reason"),
|
|
179
|
+
))
|
|
180
|
+
out = ChatCompletionChunk(
|
|
181
|
+
id=request_id, created=created, model=model,
|
|
182
|
+
choices=stream_choices,
|
|
183
|
+
)
|
|
184
|
+
yield f"data: {out.model_dump_json()}\n\n"
|
|
185
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
yield "data: [DONE]\n\n"
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error("Stream proxy error: %s", e)
|
|
191
|
+
yield f"data: {json.dumps({'error': 'Inference backend error'})}\n\n"
|
|
192
|
+
yield "data: [DONE]\n\n"
|
|
193
|
+
|
|
194
|
+
# Record usage after stream completes
|
|
195
|
+
if prompt_tokens or completion_tokens:
|
|
196
|
+
try:
|
|
197
|
+
usage_svc = ApiUsageService(db)
|
|
198
|
+
await usage_svc.record(
|
|
199
|
+
api_key_id=api_key_id, user_id=user_id, model=model,
|
|
200
|
+
prompt_tokens=prompt_tokens, completion_tokens=completion_tokens,
|
|
201
|
+
endpoint="/v1/chat/completions",
|
|
202
|
+
)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.error("Failed to record API usage: %s", e)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.get("/models")
|
|
208
|
+
async def list_models(
|
|
209
|
+
llm_service: LLMService = Depends(_get_llm_service),
|
|
210
|
+
):
|
|
211
|
+
"""List available models — public endpoint, no auth required."""
|
|
212
|
+
models = await llm_service.list_available_models()
|
|
213
|
+
return {
|
|
214
|
+
"object": "list",
|
|
215
|
+
"data": [
|
|
216
|
+
{
|
|
217
|
+
"id": m["id"],
|
|
218
|
+
"object": "model",
|
|
219
|
+
"owned_by": "kairon-labs",
|
|
220
|
+
"permission": [],
|
|
221
|
+
}
|
|
222
|
+
for m in models
|
|
223
|
+
if m.get("available", True)
|
|
224
|
+
],
|
|
225
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
|
|
3
|
+
from backend.core.dependencies import get_project_service
|
|
4
|
+
from backend.schemas.project import ProjectCreate, ProjectUpdate, ProjectResponse
|
|
5
|
+
from backend.services.project_service import ProjectService
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@router.post("/", response_model=ProjectResponse)
|
|
11
|
+
async def create_project(
|
|
12
|
+
body: ProjectCreate,
|
|
13
|
+
service: ProjectService = Depends(get_project_service),
|
|
14
|
+
):
|
|
15
|
+
project = await service.create(body.name)
|
|
16
|
+
return {
|
|
17
|
+
"id": project.id,
|
|
18
|
+
"name": project.name,
|
|
19
|
+
"instructions": project.instructions,
|
|
20
|
+
"conversation_count": 0,
|
|
21
|
+
"created_at": project.created_at,
|
|
22
|
+
"updated_at": project.updated_at,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/", response_model=list[ProjectResponse])
|
|
27
|
+
async def list_projects(
|
|
28
|
+
service: ProjectService = Depends(get_project_service),
|
|
29
|
+
):
|
|
30
|
+
return await service.list_all()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get("/{project_id}", response_model=ProjectResponse)
|
|
34
|
+
async def get_project(
|
|
35
|
+
project_id: str,
|
|
36
|
+
service: ProjectService = Depends(get_project_service),
|
|
37
|
+
):
|
|
38
|
+
project = await service.get(project_id)
|
|
39
|
+
if not project:
|
|
40
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
41
|
+
return {
|
|
42
|
+
"id": project.id,
|
|
43
|
+
"name": project.name,
|
|
44
|
+
"instructions": project.instructions,
|
|
45
|
+
"conversation_count": len(project.conversations),
|
|
46
|
+
"created_at": project.created_at,
|
|
47
|
+
"updated_at": project.updated_at,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.patch("/{project_id}", response_model=ProjectResponse)
|
|
52
|
+
async def update_project(
|
|
53
|
+
project_id: str,
|
|
54
|
+
body: ProjectUpdate,
|
|
55
|
+
service: ProjectService = Depends(get_project_service),
|
|
56
|
+
):
|
|
57
|
+
updates = body.model_dump(exclude_unset=True)
|
|
58
|
+
if not updates:
|
|
59
|
+
raise HTTPException(status_code=400, detail="No fields to update")
|
|
60
|
+
project = await service.update(project_id, **updates)
|
|
61
|
+
if not project:
|
|
62
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
63
|
+
return {
|
|
64
|
+
"id": project.id,
|
|
65
|
+
"name": project.name,
|
|
66
|
+
"instructions": project.instructions,
|
|
67
|
+
"conversation_count": len(project.conversations),
|
|
68
|
+
"created_at": project.created_at,
|
|
69
|
+
"updated_at": project.updated_at,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.delete("/{project_id}", status_code=204)
|
|
74
|
+
async def delete_project(
|
|
75
|
+
project_id: str,
|
|
76
|
+
service: ProjectService = Depends(get_project_service),
|
|
77
|
+
):
|
|
78
|
+
deleted = await service.delete(project_id)
|
|
79
|
+
if not deleted:
|
|
80
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post("/{project_id}/conversations/{conversation_id}", status_code=204)
|
|
84
|
+
async def add_conversation_to_project(
|
|
85
|
+
project_id: str,
|
|
86
|
+
conversation_id: str,
|
|
87
|
+
service: ProjectService = Depends(get_project_service),
|
|
88
|
+
):
|
|
89
|
+
added = await service.add_conversation(project_id, conversation_id)
|
|
90
|
+
if not added:
|
|
91
|
+
raise HTTPException(status_code=404, detail="Project or conversation not found")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.delete("/{project_id}/conversations/{conversation_id}", status_code=204)
|
|
95
|
+
async def remove_conversation_from_project(
|
|
96
|
+
project_id: str,
|
|
97
|
+
conversation_id: str,
|
|
98
|
+
service: ProjectService = Depends(get_project_service),
|
|
99
|
+
):
|
|
100
|
+
removed = await service.remove_conversation(conversation_id)
|
|
101
|
+
if not removed:
|
|
102
|
+
raise HTTPException(status_code=404, detail="Conversation not found")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, Query
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
|
|
4
|
+
from backend.core.database import get_db
|
|
5
|
+
from backend.core.dependencies import get_current_user
|
|
6
|
+
from backend.models.user import User
|
|
7
|
+
from backend.schemas.usage import UsageSummaryResponse, UsageHistoryResponse, UsageHistoryEntry
|
|
8
|
+
from backend.services.usage_service import UsageService
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/usage", tags=["usage"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/summary", response_model=UsageSummaryResponse)
|
|
14
|
+
async def usage_summary(
|
|
15
|
+
user: User = Depends(get_current_user),
|
|
16
|
+
db: AsyncSession = Depends(get_db),
|
|
17
|
+
):
|
|
18
|
+
service = UsageService(db)
|
|
19
|
+
return await service.get_usage_summary(user.id)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/history", response_model=UsageHistoryResponse)
|
|
23
|
+
async def usage_history(
|
|
24
|
+
days: int = Query(default=30, ge=1, le=365),
|
|
25
|
+
user: User = Depends(get_current_user),
|
|
26
|
+
db: AsyncSession = Depends(get_db),
|
|
27
|
+
):
|
|
28
|
+
service = UsageService(db)
|
|
29
|
+
entries = await service.get_usage_history(user.id, days=days)
|
|
30
|
+
return UsageHistoryResponse(
|
|
31
|
+
entries=[UsageHistoryEntry(**e) for e in entries]
|
|
32
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
4
|
+
from sqlalchemy import select
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
|
|
7
|
+
from backend.core.database import get_db
|
|
8
|
+
from backend.models.user import PlanType, User
|
|
9
|
+
from backend.services.stripe_service import StripeService
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/webhooks", tags=["webhooks"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.post("/stripe")
|
|
17
|
+
async def stripe_webhook(request: Request, db: AsyncSession = Depends(get_db)):
|
|
18
|
+
payload = await request.body()
|
|
19
|
+
sig = request.headers.get("stripe-signature", "")
|
|
20
|
+
|
|
21
|
+
stripe_service = StripeService()
|
|
22
|
+
try:
|
|
23
|
+
result = stripe_service.handle_webhook_event(payload, sig)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.warning("Webhook signature verification failed: %s", e)
|
|
26
|
+
raise HTTPException(status_code=400, detail="Invalid signature")
|
|
27
|
+
|
|
28
|
+
action = result.get("action")
|
|
29
|
+
|
|
30
|
+
if action == "upgrade":
|
|
31
|
+
customer_id = result["customer_id"]
|
|
32
|
+
subscription_id = result["subscription_id"]
|
|
33
|
+
user_id = result.get("user_id")
|
|
34
|
+
|
|
35
|
+
user = None
|
|
36
|
+
if user_id:
|
|
37
|
+
user = await db.get(User, user_id)
|
|
38
|
+
if not user:
|
|
39
|
+
stmt = select(User).where(User.stripe_customer_id == customer_id)
|
|
40
|
+
res = await db.execute(stmt)
|
|
41
|
+
user = res.scalar_one_or_none()
|
|
42
|
+
if user:
|
|
43
|
+
user.plan = PlanType.PRO.value
|
|
44
|
+
user.stripe_customer_id = customer_id
|
|
45
|
+
user.stripe_subscription_id = subscription_id
|
|
46
|
+
await db.commit()
|
|
47
|
+
logger.info("User %s upgraded to Pro", user.id)
|
|
48
|
+
else:
|
|
49
|
+
logger.error("Webhook upgrade: no user found for customer=%s user_id=%s", customer_id, user_id)
|
|
50
|
+
|
|
51
|
+
elif action == "downgrade":
|
|
52
|
+
customer_id = result["customer_id"]
|
|
53
|
+
stmt = select(User).where(User.stripe_customer_id == customer_id)
|
|
54
|
+
res = await db.execute(stmt)
|
|
55
|
+
user = res.scalar_one_or_none()
|
|
56
|
+
if user:
|
|
57
|
+
user.plan = PlanType.FREE.value
|
|
58
|
+
user.stripe_subscription_id = None
|
|
59
|
+
await db.commit()
|
|
60
|
+
logger.info("User %s downgraded to Free", user.id)
|
|
61
|
+
else:
|
|
62
|
+
logger.error("Webhook downgrade: no user found for customer=%s", customer_id)
|
|
63
|
+
|
|
64
|
+
elif action == "sync_active":
|
|
65
|
+
customer_id = result["customer_id"]
|
|
66
|
+
subscription_id = result["subscription_id"]
|
|
67
|
+
stmt = select(User).where(User.stripe_customer_id == customer_id)
|
|
68
|
+
res = await db.execute(stmt)
|
|
69
|
+
user = res.scalar_one_or_none()
|
|
70
|
+
if user:
|
|
71
|
+
user.plan = PlanType.PRO.value
|
|
72
|
+
user.stripe_subscription_id = subscription_id
|
|
73
|
+
await db.commit()
|
|
74
|
+
|
|
75
|
+
elif action == "payment_failed":
|
|
76
|
+
customer_id = result["customer_id"]
|
|
77
|
+
logger.warning("Payment failed for customer %s", customer_id)
|
|
78
|
+
|
|
79
|
+
return {"received": True}
|