from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from sqlalchemy.orm import Session from typing import List from app.database import get_db from app.models import UploadedRFP, Question from app.schemas import ProcessQuestionRequest, AIAnswerResponse, SimilarQuestion from app.services.pdf_service import pdf_service from app.services.similarity_service import similarity_service from app.services.groq_service import groq_service router = APIRouter(prefix="/api/rfp-processing", tags=["RFP Processing"]) @router.post("/upload") async def upload_rfp(file: UploadFile = File(...), db: Session = Depends(get_db)): """Upload a new RFP PDF and extract text from first 3 pages""" if not file.filename.endswith('.pdf'): raise HTTPException(status_code=400, detail="Only PDF files are allowed") uploaded_rfp = await pdf_service.save_and_extract_pdf(file, db, extract_pages=3) return { "id": uploaded_rfp.id, "filename": uploaded_rfp.filename, "uploaded_at": uploaded_rfp.uploaded_at, "text_preview": uploaded_rfp.text_content[:500] + "..." if len(uploaded_rfp.text_content) > 500 else uploaded_rfp.text_content } @router.get("/uploaded-rfps") def list_uploaded_rfps(skip: int = 0, limit: int = 50, db: Session = Depends(get_db)): """List all uploaded RFPs""" rfps = db.query(UploadedRFP).offset(skip).limit(limit).all() return [ { "id": rfp.id, "filename": rfp.filename, "rfp_name": rfp.rfp_name if hasattr(rfp, 'rfp_name') and rfp.rfp_name else rfp.filename.rsplit('.', 1)[0], "uploaded_at": rfp.uploaded_at } for rfp in rfps ] @router.delete("/uploaded-rfps/{rfp_id}") def delete_uploaded_rfp(rfp_id: int, db: Session = Depends(get_db)): """Delete an uploaded RFP""" uploaded_rfp = db.query(UploadedRFP).filter(UploadedRFP.id == rfp_id).first() if not uploaded_rfp: raise HTTPException(status_code=404, detail="Uploaded RFP not found") # Delete the file from disk if it exists import os if uploaded_rfp.file_path and os.path.exists(uploaded_rfp.file_path): try: os.remove(uploaded_rfp.file_path) except Exception as e: print(f"Error deleting file: {e}") db.delete(uploaded_rfp) db.commit() return {"message": "Uploaded RFP deleted successfully"} @router.post("/process-question", response_model=AIAnswerResponse) async def process_question(request: ProcessQuestionRequest, db: Session = Depends(get_db)): """ Process a question against a new RFP: 1. Find similar questions from database 2. Get new RFP content (if provided) 3. Generate AI answer combining both 4. Provide gap analysis """ # Get uploaded RFP (optional) uploaded_rfp = None new_rfp_content = "" if request.uploaded_rfp_id: uploaded_rfp = db.query(UploadedRFP).filter( UploadedRFP.id == request.uploaded_rfp_id ).first() if not uploaded_rfp: raise HTTPException(status_code=404, detail="Uploaded RFP not found") new_rfp_content = uploaded_rfp.text_content # Check for exact match first exact_match = similarity_service.check_exact_match(db, request.question_text) if exact_match: # Return exact match answer return AIAnswerResponse( ai_answer=exact_match.answer_text, gap_analysis="Exact match found in database. This is the stored answer from previous RFP." if not new_rfp_content else "Exact match found. No new information in current RFP.", similar_questions=[ SimilarQuestion( question_id=exact_match.id, question_text=exact_match.question_text, answer_text=exact_match.answer_text, page_start=exact_match.page_start, page_end=exact_match.page_end, rfp_title=exact_match.rfp.title, similarity_score=1.0 ) ], new_rfp_content=new_rfp_content[:1000] + "..." if new_rfp_content else "No new RFP uploaded" ) # Find similar questions - get more matches for AI to consider similar_questions = similarity_service.find_similar_questions( db, request.question_text, top_k=10, # Get more answers for AI to reference threshold=0.15 # Lower threshold to include more potential matches ) # Format similar questions for response similar_questions_list = [ SimilarQuestion( question_id=q.id, question_text=q.question_text, answer_text=q.answer_text, page_start=q.page_start, page_end=q.page_end, rfp_title=q.rfp.title, similarity_score=score ) for q, score in similar_questions ] print(f"Found {len(similar_questions_list)} similar questions") for sq in similar_questions_list: print(f" - {sq.question_text} (score: {sq.similarity_score:.2f})") # Prepare previous answers for AI previous_answers = [ { "question_text": q.question_text, "answer_text": q.answer_text, "page_start": q.page_start, "page_end": q.page_end, "rfp_title": q.rfp.title, "similarity_score": score } for q, score in similar_questions ] # Generate AI answer with HuggingFace prompt = f"""Question: {request.question_text} New RFP Content: {new_rfp_content if new_rfp_content else "No new RFP content provided"} Previous Similar Answers: {chr(10).join([f"- {ans['answer_text']} (from {ans['rfp_title']}, score: {ans['similarity_score']:.2f})" for ans in previous_answers[:5]])} {request.additional_context if request.additional_context else ""} Provide a comprehensive answer and gap analysis.""" ai_answer = groq_service.generate_chat_response(prompt) ai_response = { 'ai_answer': ai_answer, 'gap_analysis': "Generated using HuggingFace Mistral-7B model based on available context." } return AIAnswerResponse( ai_answer=ai_response['ai_answer'], gap_analysis=ai_response['gap_analysis'], similar_questions=similar_questions_list, new_rfp_content=new_rfp_content[:1000] + "..." if new_rfp_content else "No new RFP uploaded" ) @router.get("/uploaded-rfps/{rfp_id}/content") def get_rfp_content(rfp_id: int, db: Session = Depends(get_db)): """Get the full extracted content of an uploaded RFP""" uploaded_rfp = db.query(UploadedRFP).filter(UploadedRFP.id == rfp_id).first() if not uploaded_rfp: raise HTTPException(status_code=404, detail="Uploaded RFP not found") return { "id": uploaded_rfp.id, "filename": uploaded_rfp.filename, "content": uploaded_rfp.text_content }