File size: 7,265 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
209
210
211
212
213
214
215
"""
classify_and_extract.py β€” BERT Chapter Classifier + Topic Extractor

Orchestrates:
  1. BERT classifier  β†’ predicts which NCERT chapter a question belongs to
  2. Supabase retriever β†’ finds relevant chunks within that chapter
  3. Topic extractor  β†’ extracts unique topics/subtopics from retrieved chunks

Usage:
    from Classification.classify_and_extract import classify_question
    result = classify_question("What is the role of cofactors in enzyme activity?")
    print(result)

Returns:
    {
        "chapter": "Biomolecules",
        "chapter_id": 8,
        "chapter_confidence": 0.94,
        "topics": [
            {"section_code": "9.8", "section_title": "Enzymes", "similarity": 0.82},
            {"section_code": "9.8.6", "section_title": "Co-factors", "similarity": 0.71},
        ]
    }
"""

from Classification.predict import ClassifierPredictor
from rag.retriever import retrieve_chunks

# ==============================
# CONFIG
# ==============================
TOPIC_SIMILARITY_THRESHOLD = 0.45   # Minimum similarity to consider a topic relevant
MAX_TOPICS = 3                      # Maximum topics/subtopics to return
MIN_TOPICS = 1                      # Minimum topics to always return (top-1 fallback)
RETRIEVAL_TOP_K = 10                # Fetch more chunks to find diverse topics

# ==============================
# SINGLETON PREDICTOR
# ==============================
_predictor: ClassifierPredictor = None


def get_predictor() -> ClassifierPredictor:
    global _predictor
    if _predictor is None:
        _predictor = ClassifierPredictor()
    return _predictor


# ==============================
# MAIN FUNCTION
# ==============================
def classify_question(
    question: str,
    similarity_threshold: float = TOPIC_SIMILARITY_THRESHOLD,
    max_topics: int = MAX_TOPICS,
    min_topics: int = MIN_TOPICS,
) -> dict:
    """
    Classify a NEET Biology question into chapter + topic/subtopic.

    Steps:
      1. BERT predicts the chapter (91.7% F1 accuracy)
      2. Retriever fetches top-k chunks filtered to that chapter
      3. Unique (section_code, section_title) pairs are extracted
      4. Filtered by similarity threshold, capped at max_topics
      5. At least min_topics are always returned (fallback to top-1)

    Args:
        question             : The student's question.
        similarity_threshold : Min similarity score for a topic to qualify.
        max_topics           : Maximum number of topics to return.
        min_topics           : Minimum number of topics to always return.

    Returns:
        {
            "chapter": str,
            "chapter_id": int,
            "chapter_confidence": float,
            "topics": [
                {"section_code": str, "section_title": str, "similarity": float},
                ...
            ]
        }
    """
    # ── Step 1: BERT Chapter Prediction ──
    predictor = get_predictor()
    bert_result = predictor.predict(question)

    chapter_name = bert_result["chapter"]
    chapter_id = bert_result["chapter_id"]
    chapter_confidence = bert_result["confidence"]
    rejected = bert_result.get("rejected", False)
    rejection_reason = bert_result.get("rejection_reason", "")
    entropy = bert_result.get("entropy", 0.0)
    margin = bert_result.get("margin", 0.0)

    print(f"[Classify] BERT β†’ {chapter_name} (confidence: {chapter_confidence:.2%})")
    
    if rejected:
        print(f"[Classify] ❌ Rejected: {rejection_reason}")
        return {
            "chapter": chapter_name,
            "chapter_id": chapter_id,
            "chapter_confidence": chapter_confidence,
            "topics": [],
            "rejected": rejected,
            "rejection_reason": rejection_reason,
            "entropy": entropy,
            "margin": margin
        }

    # ── Step 2: Retrieve chunks filtered by predicted chapter ──
    chunks = retrieve_chunks(
        question,
        chapter_filter=chapter_name,
        top_k=RETRIEVAL_TOP_K,
    )

    if not chunks:
        print("[Classify] ⚠️  No chunks retrieved β€” returning chapter only.")
        return {
            "chapter": chapter_name,
            "chapter_id": chapter_id,
            "chapter_confidence": chapter_confidence,
            "topics": [],
            "rejected": False,
            "rejection_reason": "",
            "entropy": entropy,
            "margin": margin
        }

    # ── Step 3: Extract unique topics, keeping highest similarity per topic ──
    topic_map = {}  # key: (section_code, section_title) β†’ best similarity

    for chunk in chunks:
        section_code = chunk.get("section") or ""
        section_title = chunk.get("section_title") or ""
        similarity = chunk.get("similarity", 0.0)

        if not section_code and not section_title:
            continue

        key = (section_code, section_title)

        # Keep the highest similarity for each unique topic
        if key not in topic_map or similarity > topic_map[key]:
            topic_map[key] = similarity

    # Sort by similarity descending
    sorted_topics = sorted(topic_map.items(), key=lambda x: x[1], reverse=True)

    # ── Step 4: Apply threshold + min/max constraints ──
    filtered = [
        {
            "section_code": code,
            "section_title": title,
            "similarity": round(sim, 4),
        }
        for (code, title), sim in sorted_topics
        if sim >= similarity_threshold
    ]

    # Cap at max_topics
    filtered = filtered[:max_topics]

    # Fallback: if nothing passes threshold, always return top-1
    if len(filtered) < min_topics and sorted_topics:
        for i in range(min(min_topics, len(sorted_topics))):
            (code, title), sim = sorted_topics[i]
            entry = {
                "section_code": code,
                "section_title": title,
                "similarity": round(sim, 4),
            }
            if entry not in filtered:
                filtered.append(entry)
        filtered = filtered[:max_topics]

    print(f"[Classify] Topics found: {len(filtered)}")
    for t in filtered:
        print(f"  β†’ {t['section_code']} - {t['section_title']} (sim: {t['similarity']:.4f})")

    return {
        "chapter": chapter_name,
        "chapter_id": chapter_id,
        "chapter_confidence": chapter_confidence,
        "topics": filtered,
        "rejected": rejected,
        "rejection_reason": rejection_reason,
        "entropy": entropy,
        "margin": margin
    }


# ==============================
# QUICK TEST
# ==============================
if __name__ == "__main__":
    test_questions = [
        "What is the role of cofactors in enzyme activity?",
        "Explain the process of meiosis and its significance.",
        "What are the different types of immunity?",
    ]

    for q in test_questions:
        print(f"\n{'='*60}")
        print(f"Q: {q}")
        print(f"{'='*60}")
        result = classify_question(q)
        print(f"\nπŸ“— Chapter: {result['chapter']} (confidence: {result['chapter_confidence']:.2%})")
        print(f"πŸ“ Topics:")
        for t in result['topics']:
            print(f"   {t['section_code']} β€” {t['section_title']} (similarity: {t['similarity']:.4f})")