import uuid from fastapi import APIRouter, Depends, 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.database import get_db from app.models.collection import Collection from app.models.schemas import TicketInput, TicketsUpload, TicketsUploadResponse, UserResponse, CollectionResponse from app.models.search_schemas import SearchRequest, SearchResponse, SourceItem from app.models.user import User from app.services.embedding import embed_query from app.services.vector_store import search as qdrant_search, get_existing_ticket_ids from app.services.reranking import rerank from app.services.llm import generate_answer 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/v1", tags=["API v1"]) FETCH_K = 20 FINAL_K = 5 @router.get("/me", response_model=UserResponse) async def get_me(user: User = Depends(get_user_by_api_key)): return user @router.patch("/me/api-key") async def rotate_api_key( user: User = Depends(get_user_by_api_key), db: AsyncSession = Depends(get_db), ): user.api_key = str(uuid.uuid4().hex[:32]) await db.commit() return {"api_key": user.api_key} @router.get("/collections", response_model=list[CollectionResponse]) async def list_collections( user: User = Depends(get_user_by_api_key), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Collection).where(Collection.user_id == user.id).order_by(Collection.created_at.desc()) ) return result.scalars().all() @router.post("/tickets", response_model=TicketsUploadResponse) async def upload_tickets( body: TicketsUpload, collection_id: uuid.UUID, user: User = Depends(get_user_by_api_key), db: AsyncSession = Depends(get_db), ): 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 body.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("/search", response_model=SearchResponse) async def search( body: SearchRequest, user: User = Depends(get_user_by_api_key), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Collection).where( Collection.id == uuid.UUID(body.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") query_vector = await embed_query(body.query) qdrant_results = await qdrant_search( tenant_id=str(user.id), query_vector=query_vector, collection_id=body.collection_id, k=FETCH_K, ) if not qdrant_results: return SearchResponse( answer="Недостаточно информации в истории обращений" if body.generate_answer else None, sources=[], ) reranked = rerank(body.query, qdrant_results, k=FINAL_K) sources = [ SourceItem(ticket_id=r["ticket_id"], score=r["score"], category=r["category"], full_text=r["full_text"]) for r in reranked ] answer = None if body.generate_answer: context = "\n\n".join( f"Тикет №{r['ticket_id']} ({r['category']}):\n{r['full_text']}" for r in reranked ) answer = await generate_answer(body.query, context) return SearchResponse(answer=answer, sources=sources)