from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List from app.database import get_db from app.models import RFP, Question from app.schemas import ( RFPCreate, RFPUpdate, RFPResponse, QuestionCreate, QuestionUpdate, QuestionResponse, QuestionWithRFP ) from app.services.similarity_service import similarity_service router = APIRouter(prefix="/api/knowledge-base", tags=["Knowledge Base"]) # RFP Endpoints @router.post("/rfps", response_model=RFPResponse) def create_rfp(rfp: RFPCreate, db: Session = Depends(get_db)): """Create a new RFP""" db_rfp = RFP(**rfp.model_dump()) db.add(db_rfp) db.commit() db.refresh(db_rfp) return db_rfp @router.get("/rfps", response_model=List[RFPResponse]) def list_rfps(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): """List all RFPs""" rfps = db.query(RFP).offset(skip).limit(limit).all() return rfps @router.get("/rfps/{rfp_id}", response_model=RFPResponse) def get_rfp(rfp_id: int, db: Session = Depends(get_db)): """Get a specific RFP""" rfp = db.query(RFP).filter(RFP.id == rfp_id).first() if not rfp: raise HTTPException(status_code=404, detail="RFP not found") return rfp @router.put("/rfps/{rfp_id}", response_model=RFPResponse) def update_rfp(rfp_id: int, rfp_update: RFPUpdate, db: Session = Depends(get_db)): """Update an RFP""" db_rfp = db.query(RFP).filter(RFP.id == rfp_id).first() if not db_rfp: raise HTTPException(status_code=404, detail="RFP not found") for key, value in rfp_update.model_dump(exclude_unset=True).items(): setattr(db_rfp, key, value) db.commit() db.refresh(db_rfp) return db_rfp @router.delete("/rfps/{rfp_id}") def delete_rfp(rfp_id: int, db: Session = Depends(get_db)): """Delete an RFP and all its questions""" db_rfp = db.query(RFP).filter(RFP.id == rfp_id).first() if not db_rfp: raise HTTPException(status_code=404, detail="RFP not found") db.delete(db_rfp) db.commit() return {"message": "RFP deleted successfully"} # Question Endpoints @router.post("/questions", response_model=QuestionResponse) def create_question(question: QuestionCreate, db: Session = Depends(get_db)): """Create a new question and generate its embedding""" # Verify RFP exists rfp = db.query(RFP).filter(RFP.id == question.rfp_id).first() if not rfp: raise HTTPException(status_code=404, detail="RFP not found") # Create question db_question = Question(**question.model_dump()) db.add(db_question) db.commit() db.refresh(db_question) # Generate and save embedding immediately try: print(f"Generating embedding for question {db_question.id}...") similarity_service.save_question_embedding(db, db_question.id, db_question.question_text) print(f"Embedding generated successfully for question {db_question.id}") except Exception as e: print(f"ERROR: Failed to generate embedding for question {db_question.id}: {e}") # Don't fail the request, but log the error return db_question @router.get("/questions", response_model=List[QuestionWithRFP]) def list_questions( skip: int = 0, limit: int = 100, rfp_id: int = None, db: Session = Depends(get_db) ): """List all questions, optionally filtered by RFP""" query = db.query(Question) if rfp_id: query = query.filter(Question.rfp_id == rfp_id) questions = query.offset(skip).limit(limit).all() return questions @router.get("/questions/{question_id}", response_model=QuestionWithRFP) def get_question(question_id: int, db: Session = Depends(get_db)): """Get a specific question""" question = db.query(Question).filter(Question.id == question_id).first() if not question: raise HTTPException(status_code=404, detail="Question not found") return question @router.put("/questions/{question_id}", response_model=QuestionResponse) def update_question( question_id: int, question_update: QuestionUpdate, db: Session = Depends(get_db) ): """Update a question and regenerate its embedding if text changed""" db_question = db.query(Question).filter(Question.id == question_id).first() if not db_question: raise HTTPException(status_code=404, detail="Question not found") update_data = question_update.model_dump(exclude_unset=True) question_text_changed = "question_text" in update_data for key, value in update_data.items(): setattr(db_question, key, value) db.commit() db.refresh(db_question) # Regenerate embedding if question text changed if question_text_changed: similarity_service.save_question_embedding(db, db_question.id, db_question.question_text) return db_question @router.delete("/questions/{question_id}") def delete_question(question_id: int, db: Session = Depends(get_db)): """Delete a question""" question = db.query(Question).filter(Question.id == question_id).first() if not question: raise HTTPException(status_code=404, detail="Question not found") db.delete(question) db.commit() return {"message": "Question deleted successfully"} @router.post("/generate-embeddings") def generate_all_embeddings(db: Session = Depends(get_db)): """Generate embeddings for all questions that don't have them""" questions = db.query(Question).all() generated = 0 failed = 0 for question in questions: try: similarity_service.save_question_embedding(db, question.id, question.question_text) generated += 1 print(f"Generated embedding for question {question.id}") except Exception as e: failed += 1 print(f"Failed to generate embedding for question {question.id}: {e}") return { "message": "Embedding generation complete", "total_questions": len(questions), "generated": generated, "failed": failed }