support-bot-saas/backend/app/routers/tickets.py
ed0ss 0063df0a87
Some checks are pending
CI / lint-backend (push) Waiting to run
CI / lint-frontend (push) Waiting to run
CI / build-backend (push) Blocked by required conditions
CI / build-frontend (push) Blocked by required conditions
Реструктуризация проекта в SaaS: FastAPI бэкенд, React SPA фронтенд, Helm-чарт, CI/CD
- Бэкенд: FastAPI + SQLAlchemy async + PostgreSQL, JWT-аутентификация,
  Qdrant multi-tenant векторное хранилище, Celery воркер
- Фронтенд: React + Vite + TypeScript SPA (логин, регистрация,
  дашборд, поиск по коллекциям, настройки)
- Инфраструктура: docker-compose, Helm-чарт, GitHub Actions CI,
  Prometheus/Grafana мониторинг
- Embedding через OpenRouter (nvidia/llama-nemotron-embed-vl-1b-v2:free)
- LLM: google/gemma-4-31b-it:free через OpenRouter
2026-06-20 17:40:27 +03:00

113 lines
3.6 KiB
Python

import uuid
import json
import csv
import io
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.api_key import get_user_by_api_key
from app.auth.deps import get_current_user
from app.database import get_db
from app.models.collection import Collection
from app.models.schemas import TicketInput, TicketsUpload, TicketsUploadResponse
from app.models.user import User
from app.services.chunking import build_chunk
from app.services.embedding import embed_texts
from app.services.vector_store import upsert_chunks
router = APIRouter(prefix="/api/collections", tags=["tickets"])
async def process_tickets(
db: AsyncSession,
collection_id: uuid.UUID,
user: User,
tickets: list[TicketInput],
) -> TicketsUploadResponse:
result = await db.execute(
select(Collection).where(Collection.id == collection_id, Collection.user_id == user.id)
)
collection = result.scalar_one_or_none()
if not collection:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Collection not found")
processed = 0
skipped = 0
chunks = []
for t in tickets:
ticket_dict = {
"ticket_id": t.ticket_id,
"category": t.category,
"description": t.description,
"client": t.client,
"messages": [{"role": m.role, "text": m.text} for m in t.messages],
}
chunk = build_chunk(ticket_dict)
if chunk:
chunk["tenant_id"] = str(user.id)
chunk["collection_id"] = str(collection_id)
chunks.append(chunk)
processed += 1
else:
skipped += 1
if chunks:
search_texts = [c["search_text"] for c in chunks]
vectors = await embed_texts(search_texts)
await upsert_chunks(str(user.id), chunks, vectors)
collection.ticket_count += processed
await db.commit()
return TicketsUploadResponse(
processed=processed,
skipped=skipped,
collection_ticket_count=collection.ticket_count,
)
@router.post("/{collection_id}/tickets", response_model=TicketsUploadResponse)
async def upload_tickets_json(
collection_id: uuid.UUID,
body: TicketsUpload,
user: User = Depends(get_user_by_api_key),
db: AsyncSession = Depends(get_db),
):
return await process_tickets(db, collection_id, user, body.tickets)
@router.post("/{collection_id}/tickets/upload", response_model=TicketsUploadResponse)
async def upload_tickets_file(
collection_id: uuid.UUID,
file: UploadFile = File(...),
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
content = await file.read()
if file.filename.endswith(".json"):
data = json.loads(content)
tickets = [TicketInput(**t) for t in data] if isinstance(data, list) else [TicketInput(**data)]
elif file.filename.endswith(".csv"):
text = content.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
tickets = []
for row in reader:
tickets.append(
TicketInput(
ticket_id=row.get("ticket_id", ""),
category=row.get("category", ""),
description=row.get("description", ""),
client=row.get("client", ""),
)
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unsupported format. Use JSON or CSV.",
)
return await process_tickets(db, collection_id, user, tickets)