File size: 4,908 Bytes
4dd6ca5
 
05bc05e
4dd6ca5
 
 
05bc05e
 
4dd6ca5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05bc05e
 
4dd6ca5
 
 
05bc05e
4dd6ca5
 
 
 
05bc05e
4dd6ca5
 
 
 
 
 
 
 
05bc05e
4dd6ca5
 
 
 
 
 
 
 
 
05bc05e
4dd6ca5
 
05bc05e
 
 
 
 
 
 
4dd6ca5
 
 
05bc05e
4dd6ca5
 
 
 
 
05bc05e
4dd6ca5
 
 
 
 
05bc05e
4dd6ca5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05bc05e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dd6ca5
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
from __future__ import annotations
from typing import Dict, List, Tuple
from smolagents import tool

# Import only the classifier API; DO NOT construct models here.
from level_classifier_tool_2 import classify_levels_phrases
from phrases import BLOOMS_PHRASES, DOK_PHRASES

# ------------------------ Injected state (set from app.py) ------------------------
_INDEX = None
_BACKEND = None
_BLOOM_INDEX = None
_DOK_INDEX = None

def set_retrieval_index(index) -> None:
    """Call this from app.py after loading your LlamaIndex index."""
    global _INDEX
    _INDEX = index

def set_classifier_state(backend, bloom_index, dok_index) -> None:
    """Call this from app.py after building the backend and prebuilt indices."""
    global _BACKEND, _BLOOM_INDEX, _DOK_INDEX
    _BACKEND = backend
    _BLOOM_INDEX = bloom_index
    _DOK_INDEX = dok_index

# ----------------------------- Tools -------------------------------------

@tool
def QuestionRetrieverTool(subject: str, topic: str, grade: str) -> dict:
    """
    Retrieve up to 5 closely-related example Q&A pairs from the source datasets.

    Args:
        subject: The subject area (e.g., "Math", "Science").
        topic: The specific topic within the subject (e.g., "Algebra", "Biology").
        grade: The grade level (e.g., "Grade 5", "Grade 8").

    Returns:
        {
          "closest questions found for": {"subject": ..., "topic": ..., "grade": ...},
          "questions": [{"text": "..."} * up to 5]
        }
    """
    if _INDEX is None:
        return {"error": "Retriever not initialized. Call set_retrieval_index(index) before using this tool."}

    query = f"{topic} question for {grade} of the {subject}"
    try:
        results = _INDEX.as_retriever(similarity_top_k=5).retrieve(query)
        question_texts = [r.node.text for r in results]
    except Exception as e:
        return {"error": f"Retriever error: {e}"}

    return {
        "closest questions found for": {"subject": subject, "topic": topic, "grade": grade},
        "questions": [{"text": q} for q in question_texts]
    }


@tool
def classify_and_score(
    question: str,
    target_bloom: str,
    target_dok: str,
    agg: str = "max"
) -> dict:
    """
    Classify a question against Bloom’s and DOK targets and return guidance.

    Args:
        question: Question text to evaluate.
        target_bloom: Target Bloom’s level (e.g., "Analyze" or "Apply+").
        target_dok: Target DOK level (e.g., "DOK3" or "DOK2-DOK3").
        agg: Aggregation over phrase sims ("mean", "max", "topk_mean").

    Returns:
        {
          "ok": bool,
          "measured": {"bloom_best": str, "bloom_scores": dict, "dok_best": str, "dok_scores": dict},
          "feedback": str
        }
    """
    if _BACKEND is None or _BLOOM_INDEX is None or _DOK_INDEX is None:
        return {"error": "Classifier not initialized. Call set_classifier_state(backend, bloom_index, dok_index) first."}

    try:
        res = classify_levels_phrases(
            question,
            BLOOMS_PHRASES,
            DOK_PHRASES,
            backend=_BACKEND,
            prebuilt_bloom_index=_BLOOM_INDEX,
            prebuilt_dok_index=_DOK_INDEX,
            agg=agg,
            return_phrase_matches=True
        )
    except Exception as e:
        return {"error": f"classify_levels_phrases failed: {e}"}

    def _parse_target_bloom(t: str):
        order = ["Remember","Understand","Apply","Analyze","Evaluate","Create"]
        if t.endswith("+"):
            base = t[:-1]
            return set(order[order.index(base):])
        return {t}

    def _parse_target_dok(t: str):
        order = ["DOK1","DOK2","DOK3","DOK4"]
        if "-" in t:
            lo, hi = t.split("-")
            return set(order[order.index(lo):order.index(hi)+1])
        return {t}

    bloom_target_set = _parse_target_bloom(target_bloom)
    dok_target_set = _parse_target_dok(target_dok)

    bloom_best = res["blooms"]["best_level"]
    dok_best = res["dok"]["best_level"]

    bloom_ok = bloom_best in bloom_target_set
    dok_ok = dok_best in dok_target_set

    feedback_parts = []
    if not bloom_ok:
        feedback_parts.append(
            f"Shift Bloom’s from {bloom_best} toward {sorted(bloom_target_set)}. "
            f"Top cues: {res['blooms']['top_phrases'].get(bloom_best, [])[:3]}"
        )
    if not dok_ok:
        feedback_parts.append(
            f"Shift DOK from {dok_best} toward {sorted(dok_target_set)}. "
            f"Top cues: {res['dok']['top_phrases'].get(dok_best, [])[:3]}"
        )

    return {
        "ok": bool(bloom_ok and dok_ok),
        "measured": {
            "bloom_best": bloom_best,
            "bloom_scores": res["blooms"]["scores"],
            "dok_best": dok_best,
            "dok_scores": res["dok"]["scores"],
        },
        "feedback": " ".join(feedback_parts) if feedback_parts else "On target.",
    }