import os import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from fastapi.security import OAuth2PasswordBearer from src.core.domain.schemas import ChatRequest, ChatResponse, SearchResponse from src.core.use_cases.search_use_case import SearchUseCase from src.core.use_cases.rag_chat_use_case import RagChatUseCase from src.core.use_cases.agent_router_use_case import AgentRouterUseCase from src.api.dependencies import get_search_use_case, get_rag_chat_use_case, get_agent_router_use_case from src.core.security import get_current_user from src.core.domain.db_models import User from jose import jwt, JWTError from src.core.config import settings from src.infrastructure.database import get_db from sqlalchemy.orm import Session router = APIRouter() # Optional bearer โ€” doesn't raise if token is missing _optional_bearer = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login", auto_error=False) def get_optional_user( token: Optional[str] = Depends(_optional_bearer), db: Session = Depends(get_db) ) -> Optional[User]: """Returns the authenticated user or None for guests.""" if not token: return None try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) if payload.get("type") != "access": return None email = payload.get("sub") if not email: return None user = db.query(User).filter(User.email == email).first() return user if user and user.is_active else None except JWTError: return None @router.post("/search") def direct_search( request: ChatRequest, search_use_case: SearchUseCase = Depends(get_search_use_case), current_user: User = Depends(get_current_user) ): """Lightning-fast hybrid search bypassing the LLM.""" try: results = search_use_case.execute( query=request.query, limit=request.top_k, source_filter=request.source_filter, language_filter=request.language_filter, days_back=getattr(request, 'days_back', None) ) hits = [{"content": r.content, "metadata": r.metadata, "score": r.score, "doc_id": r.doc_id} for r in results] return {"results": hits, "query": request.query} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/chat/test", response_model=ChatResponse) async def chat_test( request: ChatRequest, agent_router_use_case: AgentRouterUseCase = Depends(get_agent_router_use_case) ): """Test RAG chat endpoint without authentication for debugging""" # Get endpoint from environment variable or use default os.getenv("RAG_ENDPOINT", "/rag/chat/test") result = await agent_router_use_case.execute_chat(request) return result @router.post("/chat/stream") async def chat_with_rag_stream( request: ChatRequest, router_use_case: AgentRouterUseCase = Depends(get_agent_router_use_case), current_user: Optional[User] = Depends(get_optional_user) ): """Streaming RAG chat. Works for both authenticated users and guests.""" try: if current_user is None and not request.session_id: request.session_id = f"guest_{uuid.uuid4().hex[:12]}" user_id = current_user.id if current_user else None # X-Accel-Buffering: no tells HF nginx NOT to buffer โ€” fixes ERR_HTTP2_PROTOCOL_ERROR # NOTE: Do NOT set Transfer-Encoding: chunked โ€” it is prohibited in HTTP/2 (RFC 7540 ยง8.1.2.2) headers = { "X-Accel-Buffering": "no", "Cache-Control": "no-cache, no-transform", } return StreamingResponse( router_use_case.execute_stream(request, is_guest=(current_user is None), user_id=user_id), media_type="text/event-stream", headers=headers, ) except Exception as e: raise HTTPException(status_code=500, detail=str(e))