File size: 6,112 Bytes
4d67ec7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | """
qa.py β Question Answering API router
Endpoints:
POST /qa β Full RAG pipeline (classify + retrieve + answer + verify)
POST /classify β Classification only (BERT chapter + topic extraction)
"""
import traceback
from fastapi import APIRouter, HTTPException
from Backend.schemas import (
QuestionRequest,
QAResponse,
ClassifyOnlyRequest,
ClassifyOnlyResponse,
ClassificationResult,
TopicInfo,
SourceChunk,
IsNeetBioRequest,
IsNeetBioResponse
)
from rag.pipeline import run_pipeline
from Classification.classify_and_extract import classify_question, get_predictor
router = APIRouter(prefix="/api", tags=["Question Answering"])
# ==============================
# POST /api/qa
# ==============================
@router.post(
"/qa",
response_model=QAResponse,
summary="Answer a NEET Biology question",
description=(
"Runs the full pipeline: BERT classifies the chapter, "
"retrieves relevant NCERT chunks, generates an answer with "
"justification, and verifies correctness."
),
)
async def answer_question(req: QuestionRequest):
"""
Full Q&A pipeline:
1. BERT β chapter prediction + topic extraction
2. Supabase β retrieve NCERT chunks (filtered by chapter)
3. Groq LLM β generate answer + justification
4. Groq LLM β verify answer
5. Compute confidence score
"""
try:
result = run_pipeline(
question=req.question,
chapter_filter=req.chapter_filter,
top_k=req.top_k,
use_classifier=req.use_classifier,
)
# Build classification model if present
classification = None
if result.get("classification"):
raw = result["classification"]
classification = ClassificationResult(
chapter=raw["chapter"],
chapter_id=raw["chapter_id"],
chapter_confidence=raw["chapter_confidence"],
rejected=raw.get("rejected", False),
rejection_reason=raw.get("rejection_reason", ""),
entropy=raw.get("entropy", 0.0),
margin=raw.get("margin", 0.0),
topics=[
TopicInfo(
section_code=t["section_code"],
section_title=t["section_title"],
similarity=t["similarity"],
)
for t in raw.get("topics", [])
],
)
# Build sources
sources = [
SourceChunk(
chapter=s.get("chapter"),
section=s.get("section"),
section_title=s.get("section_title"),
text_content=s.get("text_content"),
similarity=s.get("similarity", 0.0),
)
for s in result.get("sources", [])
]
return QAResponse(
question=result["question"],
classification=classification,
answer=result["answer"],
justification=result.get("justification", []),
sources=sources,
confidence=result.get("confidence", 0.0),
verified=result.get("verified", False),
verifier_note=result.get("verifier_note", ""),
)
except Exception as e:
traceback.print_exc()
raise HTTPException(
status_code=500,
detail=f"Pipeline error: {str(e)}",
)
# ==============================
# POST /api/classify
# ==============================
@router.post(
"/classify",
response_model=ClassifyOnlyResponse,
summary="Classify a question (chapter + topics only)",
description=(
"Runs only the BERT classifier + topic extraction. "
"Does NOT generate an answer. Useful for quick categorization."
),
)
async def classify_only(req: ClassifyOnlyRequest):
"""
Classification only (no RAG):
1. BERT β chapter prediction
2. Supabase retrieval β topic extraction
"""
try:
raw = classify_question(req.question)
classification = ClassificationResult(
chapter=raw["chapter"],
chapter_id=raw["chapter_id"],
chapter_confidence=raw["chapter_confidence"],
rejected=raw.get("rejected", False),
rejection_reason=raw.get("rejection_reason", ""),
entropy=raw.get("entropy", 0.0),
margin=raw.get("margin", 0.0),
topics=[
TopicInfo(
section_code=t["section_code"],
section_title=t["section_title"],
similarity=t["similarity"],
)
for t in raw.get("topics", [])
],
)
return ClassifyOnlyResponse(
question=req.question,
classification=classification,
)
except Exception as e:
traceback.print_exc()
raise HTTPException(
status_code=500,
detail=f"Classification error: {str(e)}",
)
# ==============================
# POST /api/is-neet-bio
# ==============================
@router.post(
"/is-neet-bio",
response_model=IsNeetBioResponse,
summary="Quickly check if a string is a valid NEET Biology question",
)
async def is_neet_bio(req: IsNeetBioRequest):
"""
Runs only the BERT classifier prediction to check OOD (Out-Of-Domain) logic.
Fast path validation for frontend to prevent garbage from triggering the RAG pipeline.
"""
try:
predictor = get_predictor()
bert_result = predictor.predict(req.question)
rejected = bert_result.get("rejected", False)
reason = bert_result.get("rejection_reason", "")
return IsNeetBioResponse(
is_valid=not rejected,
rejected=rejected,
reason=reason
)
except Exception as e:
traceback.print_exc()
raise HTTPException(
status_code=500,
detail=f"Validation error: {str(e)}",
)
|