|
|
from typing import Optional, List, Dict, Any |
|
|
from contextlib import asynccontextmanager |
|
|
|
|
|
from fastapi import FastAPI, HTTPException, Query |
|
|
from pydantic import BaseModel |
|
|
|
|
|
from src import RAG |
|
|
from src.db_utils.history_utils import ( |
|
|
init_history_table, |
|
|
log_query, |
|
|
get_all_history, |
|
|
get_history_by_dialogue, |
|
|
search_history, |
|
|
get_history_stats, |
|
|
delete_history, |
|
|
get_recent_dialogues |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager |
|
|
async def lifespan(app: FastAPI): |
|
|
|
|
|
try: |
|
|
init_history_table() |
|
|
except Exception as e: |
|
|
print(f"⚠️ Не удалось инициализировать таблицу истории: {e}") |
|
|
yield |
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="RAG API", |
|
|
version="1.0.0", |
|
|
lifespan=lifespan, |
|
|
) |
|
|
|
|
|
|
|
|
rag = RAG( |
|
|
embed_model_name="deepvk/USER-bge-m3", |
|
|
embed_index_name="recursive_USER-bge-m3", |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QueryRequest(BaseModel): |
|
|
query: str |
|
|
dialogue_id: Optional[str] = None |
|
|
history: Optional[List[Dict[str, Any]]] = None |
|
|
|
|
|
|
|
|
class QueryResponse(BaseModel): |
|
|
answer: str |
|
|
reason: str |
|
|
query_id: Optional[int] = None |
|
|
|
|
|
|
|
|
class HistoryEntry(BaseModel): |
|
|
id: int |
|
|
timestamp: str |
|
|
dialogue_id: str |
|
|
query: str |
|
|
answer: str |
|
|
reason: Optional[str] = None |
|
|
search_period: Optional[Dict[str, Any]] = None |
|
|
metadata: Optional[Dict[str, Any]] = None |
|
|
|
|
|
|
|
|
class HistoryStats(BaseModel): |
|
|
total_queries: int |
|
|
unique_dialogues: int |
|
|
last_query_time: Optional[str] = None |
|
|
first_query_time: Optional[str] = None |
|
|
|
|
|
|
|
|
class DialogueInfo(BaseModel): |
|
|
dialogue_id: str |
|
|
message_count: int |
|
|
started_at: Optional[str] = None |
|
|
last_message_at: Optional[str] = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/rag", response_model=QueryResponse) |
|
|
def rag_query(request: QueryRequest): |
|
|
"""Основной endpoint для запросов к RAG. Логирует запрос после получения ответа.""" |
|
|
|
|
|
|
|
|
history = None |
|
|
if request.dialogue_id and not request.history: |
|
|
history = get_history_by_dialogue(request.dialogue_id) |
|
|
elif request.history: |
|
|
history = request.history |
|
|
|
|
|
|
|
|
result = rag.invoke(request.query, history=history) |
|
|
|
|
|
|
|
|
query_id = log_query( |
|
|
query=request.query, |
|
|
answer=result.get("answer", ""), |
|
|
reason=result.get("reason", ""), |
|
|
dialogue_id=request.dialogue_id |
|
|
) |
|
|
|
|
|
return QueryResponse( |
|
|
answer=result.get("answer", ""), |
|
|
reason=result.get("reason", ""), |
|
|
query_id=query_id |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/history", response_model=List[HistoryEntry]) |
|
|
def get_history( |
|
|
limit: int = Query(default=100, ge=1, le=1000), |
|
|
offset: int = Query(default=0, ge=0) |
|
|
): |
|
|
"""Получить историю запросов""" |
|
|
return get_all_history(limit=limit, offset=offset) |
|
|
|
|
|
|
|
|
@app.get("/history/stats", response_model=HistoryStats) |
|
|
def get_stats(): |
|
|
"""Получить статистику по истории""" |
|
|
stats = get_history_stats() |
|
|
return HistoryStats( |
|
|
total_queries=stats.get("total_queries", 0), |
|
|
unique_dialogues=stats.get("unique_dialogues", 0), |
|
|
last_query_time=stats.get("last_query_time"), |
|
|
first_query_time=stats.get("first_query_time") |
|
|
) |
|
|
|
|
|
|
|
|
@app.get("/history/search", response_model=List[HistoryEntry]) |
|
|
def search_in_history( |
|
|
q: str = Query(..., min_length=1, description="Текст для поиска"), |
|
|
limit: int = Query(default=50, ge=1, le=500) |
|
|
): |
|
|
"""Поиск по истории запросов""" |
|
|
return search_history(search_text=q, limit=limit) |
|
|
|
|
|
|
|
|
@app.get("/history/dialogues", response_model=List[DialogueInfo]) |
|
|
def get_dialogues( |
|
|
limit: int = Query(default=10, ge=1, le=100) |
|
|
): |
|
|
"""Получить список последних диалогов""" |
|
|
return get_recent_dialogues(limit=limit) |
|
|
|
|
|
|
|
|
@app.get("/history/dialogue/{dialogue_id}", response_model=List[HistoryEntry]) |
|
|
def get_dialogue(dialogue_id: str): |
|
|
"""Получить историю конкретного диалога""" |
|
|
return get_history_by_dialogue(dialogue_id) |
|
|
|
|
|
|
|
|
@app.delete("/history") |
|
|
def clear_history(dialogue_id: Optional[str] = None): |
|
|
"""Удалить историю (всю или конкретного диалога)""" |
|
|
try: |
|
|
delete_history(dialogue_id=dialogue_id) |
|
|
if dialogue_id: |
|
|
return {"message": f"История диалога {dialogue_id} удалена"} |
|
|
return {"message": "Вся история удалена"} |
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health") |
|
|
def health(): |
|
|
return {"status": "ok"} |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
|
|
|
uvicorn.run( |
|
|
"server:app", |
|
|
host="0.0.0.0", |
|
|
port=8000, |
|
|
reload=True, |
|
|
) |
|
|
|