ppt-tool/backend/services/ai_usage_service.py
Vadym Samoilenko d3d1667a79 Phase 2: Admin panel, analytics, storage, template pipeline, multi-provider LLM
- Fix admin sidebar: remove duplicate Teams, add Storage nav item
- Analytics: client-scoped queries, super_admin sees all (including NULL client_id)
- Storage management: list/download/delete presentations with file metadata
- Settings page with brand config router
- AI usage tracking: new AIUsageModel, ai_usage_service, analytics endpoint
- Master deck → template bridge: _register_as_template creates TemplateModel
  + PresentationLayoutCodeModel so parsed layouts appear in template picker
- Multi-provider LLM vision in parser: Anthropic/Google/OpenAI with asyncio.to_thread
- Fix PPTX upload 400: accept by .pptx extension (browser sends octet-stream)
- Fix reparse FK violation: presentation_id=None for parse_master_deck jobs
- Worker job_timeout increased to 1800s for LLM-heavy master deck parsing
- PYTHONUNBUFFERED=1 in docker-compose worker for real-time log output
- Auth: clientId in /me response, dev-login cookie improvements
- Frontend: auth slice clientId, master-deck thumbnails, storage page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:39:34 +00:00

80 lines
2.4 KiB
Python

"""Fire-and-forget AI usage logging service."""
import asyncio
import uuid
from typing import Optional
from services.database import async_session_maker
from utils.llm_context import get_llm_context
def log(
provider: str,
model: str,
call_type: str,
input_tokens: Optional[int] = None,
output_tokens: Optional[int] = None,
total_tokens: Optional[int] = None,
duration_ms: Optional[int] = None,
user_id: Optional[uuid.UUID] = None,
client_id: Optional[uuid.UUID] = None,
presentation_id: Optional[uuid.UUID] = None,
) -> None:
"""Log AI usage asynchronously (fire-and-forget).
If user_id/client_id/presentation_id are not provided,
reads them from the LLMCallContext contextvar.
"""
ctx = get_llm_context()
if ctx:
user_id = user_id or ctx.user_id
client_id = client_id or ctx.client_id
presentation_id = presentation_id or ctx.presentation_id
asyncio.create_task(
_write_log(
provider=provider,
model=model,
call_type=call_type,
input_tokens=input_tokens,
output_tokens=output_tokens,
total_tokens=total_tokens,
duration_ms=duration_ms,
user_id=user_id,
client_id=client_id,
presentation_id=presentation_id,
)
)
async def _write_log(
provider: str,
model: str,
call_type: str,
input_tokens: Optional[int],
output_tokens: Optional[int],
total_tokens: Optional[int],
duration_ms: Optional[int],
user_id: Optional[uuid.UUID],
client_id: Optional[uuid.UUID],
presentation_id: Optional[uuid.UUID],
) -> None:
try:
from models.sql.ai_usage import AIUsageModel
async with async_session_maker() as session:
entry = AIUsageModel(
provider=provider,
model=model,
call_type=call_type,
input_tokens=input_tokens,
output_tokens=output_tokens,
total_tokens=total_tokens or ((input_tokens or 0) + (output_tokens or 0)),
duration_ms=duration_ms,
user_id=user_id,
client_id=client_id,
presentation_id=presentation_id,
)
session.add(entry)
await session.commit()
except Exception as e:
print(f"AI usage log error: {e}")