from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import List, Optional from app.services.llm import llm_service from app.services.vector import vector_service from app.services.search import search_service from app.services.intent import IntentService from app.services.files import file_service from fastapi import UploadFile, File # Initialize Intent Service intent_service = IntentService(llm_service) router = APIRouter() # --- Pydantic Models --- class QueryRequest(BaseModel): query: str class Source(BaseModel): title: str url: str snippet: str class ChallengeRequest(BaseModel): original_query: str original_answer: str sources_text: str class QueryResponse(BaseModel): answer: str sources: List[Source] confidence: str search_queries: List[str] intent: Optional[str] = None thought_process: Optional[str] = None # --- Endpoints --- @router.post("/query", response_model=QueryResponse) async def process_query(request: QueryRequest): """ Main orchestration endpoint for the Trust-First Copilot. """ user_query = request.query print(f"Refining query: {user_query}") try: # --- PHASE 1: INTENT & RISK ANALYSIS --- print("🧠 Analyzing Intent...") try: intent = await intent_service.analyze(user_query) print(f" Category: {intent.category}") print(f" Reasoning: {intent.reasoning}") print(f" Risk: {intent.risk_level}") except Exception as e: print(f"Intent Error: {e}") from app.services.intent import IntentResponse intent = IntentResponse(category="SEARCH_REQUIRED", reasoning="Error", risk_level="LOW") # Risk Guard if intent.risk_level == "HIGH": return QueryResponse( answer="I cannot fulfill this request as it has been flagged as high risk/safety violation.", sources=[], confidence="Blocked", search_queries=[], intent="High Risk", thought_process=f"Blocked by Risk Analyzer. Reasoning: {intent.reasoning}" ) # --- PHASE 2: EXECUTION --- search_results = [] # Branch 1: Needs Search if intent.category == "SEARCH_REQUIRED" or intent.category == "DATA_ANALYSIS": print("🔍 Initiating Web Search...") search_results = await search_service.search(user_query) if not search_results: # Fallback if search finds nothing but intent was search pass # Branch 2: Coding (Skip Search usually, unless specific docs needed) elif intent.category == "CODING_TASK": print("💻 Coding Task - Focused Generation") # Potential future improvement: Search for docs if needed # Branch 3: Chat / General else: print("💬 Chat Mode - Direct Generation") # --- PHASE 3: CONTEXT & RAG --- context_text = "" final_sources = [] if search_results: # RAG Logic print("Indexing search results in Vector DB...") vector_service.create_index_from_results(search_results) print("Searching Vector DB for relevant context...") relevant_chunks = vector_service.search_similar(user_query, k=5) final_sources = relevant_chunks if relevant_chunks else search_results context_text = "\n\n".join([ f"Source {i+1}:\nTitle: {r.get('title')}\nURL: {r.get('url')}\nContent: {r.get('content')}" for i, r in enumerate(final_sources) ]) else: context_text = "No external sources used. Answering from internal knowledge." # --- PHASE 4: SYNTHESIS --- # Modify prompt based on intent? For now, standard synthesis but context aware. answer = await llm_service.synthesize_answer(user_query, context_text) # --- PHASE 5: VERIFICATION --- confidence_level = "Medium" if intent.category == "SEARCH_REQUIRED": confidence_assessment = await llm_service.verify_confidence(answer, context_text) if "High confidence" in confidence_assessment: confidence_level = "High" elif "Low confidence" in confidence_assessment: confidence_level = "Low" else: confidence_level = "N/A (Chat)" # Construct Response formatted_sources = [ Source(title=r.get('title', 'Unknown'), url=r.get('url', '#'), snippet=r.get('content', '')[:200]) for r in (search_results if search_results else []) ] return QueryResponse( answer=answer, sources=formatted_sources, confidence=confidence_level, search_queries=[user_query], intent=intent.category, thought_process=f"Intent: {intent.category}. Reasoning: {intent.reasoning}" ) except Exception as e: print(f"Error processing query: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/challenge", response_model=QueryResponse) async def challenge_answer(request: ChallengeRequest): """ 'Disagree-with-Me' Mode: Critiques the previous answer. """ try: from app.core import prompts # Construct the critique prompt messages = [ {"role": "system", "content": prompts.MASTER_PROMPT_CHALLENGE}, {"role": "user", "content": f"Query: {request.original_query}\n\nAnswer to critique: {request.original_answer}\n\nSources used:\n{request.sources_text}"} ] critique = await llm_service._generate(messages, temperature=0.7) # Return as a new message, but marked as a critique return QueryResponse( answer=critique, sources=[], confidence="High (Critique)", search_queries=[], intent="CRITIQUE", thought_process="Devil's Advocate Mode Activated." ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/upload") async def upload_file(file: UploadFile = File(...)): """ Parses an uploaded file and returns its text content for RAG. """ try: filename = file.filename print(f"📂 Processing file: {filename}") content = await file_service.process_file(file) return {"filename": filename, "content": content} except Exception as e: raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")