support-bot-saas/backend/app/routers/tickets.py
ed0ss f6d3ac05a5
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
fix: major bugfixes and stability improvements across backend & frontend
Backend:
- Fix TicketInput ticket_id type (int|str -> str) — Pydantic 500 on numeric IDs
- Fix LLM module: add missing logger import, handle 429 and non-200 responses
- Switch LLM model to poolside/laguna-m.1:free (avoid OpenRouter rate limits)
- Add GET /api/collections/{id} endpoint to fetch single collection
- Deduplicate tickets: replace randomized hash() with deterministic
  hashlib.md5 for Qdrant point IDs
- Add get_existing_ticket_ids() — scroll Qdrant before processing,
  skip already-imported tickets, avoid redundant embeddings
- Multi-tenant isolation: tenant_id + collection_id in all Qdrant filters
- Fix embedding: drop incompatible openai SDK, use raw httpx
- Fix reranking: access ScoredPoint.payload via .get(), not []
- Fix chunking: expand role detection (support/operator/agent + client/user/customer)
- Fix bcrypt: downgrade to 4.0.1 (incompatible 5.x with passlib)

Frontend:
- Register: add plan selection (free/starter/pro)
- CollectionView: fetch ticket_count from backend on mount
  (not only from upload response — survived page refresh)
- uploadFile: use getToken() instead of stale module-level var,
  show real HTTP status in error messages
- Nginx: increase client_max_body_size to 50M for file uploads
2026-06-20 19:51:08 +03:00

118 lines
3.8 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, get_existing_ticket_ids
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")
existing_ids = get_existing_ticket_ids(str(user.id), str(collection_id))
processed = 0
skipped = 0
chunks = []
for t in tickets:
if t.ticket_id in existing_ids:
skipped += 1
continue
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_current_user),
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)