File size: 9,139 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
196
197
198
199
200
201
202
203
204
205
206
207
208
"""
pipeline.py  β€”  Main RAG orchestrator

Ties together:
  1. Retriever     β†’ get top-k NCERT chunks from Supabase
  2. PromptBuilder β†’ construct grounded LLM prompts
  3. Generator     β†’ call Groq to answer / justify / verify
  4. Confidence    β†’ produce a single confidence score

Usage:
    from rag.pipeline import run_pipeline
    result = run_pipeline("Which organelle is the powerhouse of the cell?")
    print(result)
"""

from rag.retriever      import retrieve_chunks
from rag.prompt_builder import (
    build_answer_prompt,
    build_justification_prompt,
    build_verifier_prompt,
)
from rag.generator  import generate_answer, generate_justification, verify_answer_llm
from rag.confidence import compute_confidence
from Classification.classify_and_extract import classify_question


def run_pipeline(
    question: str,
    chapter_filter: str = None,
    top_k: int = 5,
    use_classifier: bool = False,
) -> dict:
    """
    End-to-end RAG pipeline for a NEET Biology question.

    Args:
        question       : The student's question string.
        chapter_filter : Optional chapter name to restrict retrieval scope.
                         Auto-populated by BERT classifier if use_classifier=True.
        top_k          : Number of NCERT chunks to retrieve.
        use_classifier : If True, auto-classify question to get chapter + topics.

    Returns a structured dict:
    {
        "question"       : str,
        "classification" : {chapter, chapter_id, chapter_confidence, topics},
        "answer"         : str,
        "justification"  : list[str],
        "sources"        : list[{chapter, section, section_title, text_content, similarity}],
        "confidence"     : float,
        "verified"       : bool,
        "verifier_note"  : str,
    }
    """

    # ── Step 0: BERT Classification + Topic Extraction ──────────────────────
    classification = None
    if use_classifier:
        print("[Pipeline] Step 0: BERT classification + topic extraction...")
        classification = classify_question(question)
        predicted_chapter = classification["chapter"]
        print(f"[Pipeline] Predicted chapter: {predicted_chapter} (Not used as filter)")

        # ── Confidence gate: reject if BERT is too uncertain or OOD ──
        if classification.get("rejected") or classification["chapter_confidence"] < 0.45:
            reject_reason = classification.get("rejection_reason") or f"BERT confidence ({classification['chapter_confidence']:.2%}) is below the 45% threshold."
            print(f"[Pipeline] ⚠️ Classifier Rejected β€” {reject_reason}")
            return {
                "question"       : question,
                "classification" : classification,
                "answer"         : "This question does not appear to be related to NEET Biology.",
                "justification"  : [],
                "sources"        : [],
                "confidence"     : 0.0,
                "verified"       : False,
                "verifier_note"  : reject_reason,
                "rejected"       : True,
            }

    # ── Step 1: Retrieve relevant NCERT chunks ──────────────────────────────
    print("[Pipeline] Step 1: Retrieving NCERT chunks (Global DB Search)...")
    chunks = retrieve_chunks(question, chapter_filter=None, top_k=top_k)

    if not chunks:
        return {
            "question"       : question,
            "classification" : classification,
            "answer"         : "Insufficient context: no relevant NCERT passages found.",
            "justification"  : [],
            "sources"        : [],
            "confidence"     : 0.0,
            "verified"       : False,
            "verifier_note"  : "No chunks retrieved β€” cannot verify.",
        }

    # Extract top chapter/section for prompt context
    top_chunk   = chunks[0]
    top_chapter = top_chunk.get("chapter", "Unknown Chapter")
    top_section = top_chunk.get("section_title", "Unknown Topic")

    # ── Step 3: Generate answer + justification in a single Groq call ──────
    print("[Pipeline] Step 3: Generating answer + justification via Groq...")
    answer_msgs = build_answer_prompt(question, top_chapter, top_section, chunks)
    # Append instruction to also include justification in the same response
    answer_msgs[-1]["content"] += (
        "\n\nAfter the answer, on a new line write '---JUSTIFICATION---' "
        "and then provide a numbered step-by-step biological justification "
        "for why the answer is correct, citing only the NCERT context above."
    )
    raw_response = generate_answer(answer_msgs)
    
    # Parse answer and justification apart
    if "---JUSTIFICATION---" in raw_response:
        parts = raw_response.split("---JUSTIFICATION---", 1)
        answer = parts[0].strip()
        just_raw = parts[1].strip()
    else:
        answer = raw_response.strip()
        just_raw = ""
    
    # Parse justification into numbered steps
    if just_raw:
        lines = just_raw.strip().split("\n")
        justification = [l.strip() for l in lines if l.strip() and (
            l.strip()[0].isdigit() or l.strip().lower().startswith("step")
        )]
        if not justification:
            justification = [l.strip() for l in lines if l.strip()]
    else:
        justification = []

    # ── Step 5: Verify answer (LLM Judge) ───────────────────────────────────
    print("[Pipeline] Step 5: Verifying answer via LLM Judge...")
    verif_msgs      = build_verifier_prompt(question, answer, chunks)
    verif_result    = verify_answer_llm(verif_msgs)
    verified        = verif_result["verified"]
    verifier_note   = verif_result["explanation"]

    # ── Step 6: Compute confidence ──────────────────────────────────────────
    retrieval_scores = [c.get("similarity", 0.0) for c in chunks]
    confidence       = compute_confidence(retrieval_scores, verified)

    # ── Step 7: Build sources list (no raw embeddings) ─────────────────────
    sources = [
        {
            "chapter"      : c.get("chapter"),
            "section"      : c.get("section"),
            "section_title": c.get("section_title"),
            "text_content" : c.get("text_content"),
            "similarity"   : round(c.get("similarity", 0.0), 4),
        }
        for c in chunks
    ]

    return {
        "question"       : question,
        "classification" : classification,
        "answer"         : answer,
        "justification"  : justification,
        "sources"        : sources,
        "confidence"     : confidence,
        "verified"       : verified,
        "verifier_note"  : verifier_note,
    }


if __name__ == "__main__":
    import json, sys

    # ── Chapters currently in your Supabase DB ──────────────────────────────
    # Kebo101 β†’ The Living World
    # Kebo109 β†’ Biomolecules
    TEST_QUESTIONS = [
        "What is the role of cofactors in enzyme activity?",                    # Biomolecules
        "What are primary and secondary metabolites in living organisms?",      # Biomolecules
        "How does carbonic anhydrase speed up chemical reactions?",             # Biomolecules
        "What is the difference between metabolism, anabolism and catabolism?", # Biomolecules
        "What is binomial nomenclature and who proposed it?",                  # The Living World
    ]

    question = TEST_QUESTIONS[0]   # ← change index (0-4) to test different questions
    debug    = "--debug" in sys.argv  # run with: python -m rag.pipeline --debug

    print(f"\n{'='*60}")
    print(f"QUESTION: {question}")
    print(f"{'='*60}\n")

    result = run_pipeline(question)

    if debug:
        # Show full retrieved context so you can verify what the LLM saw
        print("\n── RETRIEVED CONTEXT ──────────────────────────────────────")
        for i, src in enumerate(result["sources"], 1):
            print(f"\n[{i}] ({src['similarity']:.3f}) {src['chapter']} > {src['section_title']}")
            print(f"    {src['text_content'][:300]}...")
        print()

    print("── ANSWER ─────────────────────────────────────────────────")
    print(result["answer"])

    print("\n── JUSTIFICATION ──────────────────────────────────────────")
    for step in result["justification"]:
        print(f"  {step}")

    print(f"\n── CONFIDENCE: {result['confidence']}  |  VERIFIED: {result['verified']} ──")
    if result.get("verifier_note"):
        print(f"   Verifier note: {result['verifier_note']}")