chuckfinca commited on
Commit
abf3963
·
1 Parent(s): 10a77fa

feat(ui): Overhaul Gradio app for interactivity and evaluation

Browse files

Transforms the basic Gradio interface into a polished, feature-rich web application designed for demonstration and evaluation. The new UI provides a significantly improved user experience with interactive examples and adds robust features for capturing and analyzing the RAG pipeline's output.

This work completes the transition from a back-end proof-of-concept to a user-facing demo.

Key Changes:

- **Gradio App Overhaul (`app.py`):**
- Rewrote the UI using `gr.Blocks` for a custom two-column layout.
- Added an "Example Scenarios" section with radio buttons to quickly populate the form with pre-defined student narratives.
- Implemented an "Evaluation Data" accordion that displays the full RAG output (inputs, retrieval results, LLM output) in a JSON viewer.
- Added a "Download JSON" button to allow users to save the complete evaluation data for offline analysis.
- Refactored the API call to be a generator (`yield`), enabling progressive updates like "Processing..." messages and interactive button states.

- **Refactored Utilities (`utils.py`):**
- Created a new `format_evidence_for_display` function to centralize the logic for creating rich, cited evidence snippets, now used by both the app and the notebook.
- Added `load_citations` and `create_evaluation_bundle` helpers to support the new UI's data requirements.

- **Notebook Improvements (`fot_recommender_poc.ipynb`):**
- Updated the notebook to use the new `format_evidence_for_display` utility, resulting in a cleaner and more professional presentation of the evidence base.

app.py CHANGED
@@ -3,68 +3,86 @@ import faiss
3
  import os
4
  import numpy as np
5
  import sys
 
 
 
6
  from pathlib import Path
7
  from dotenv import load_dotenv
8
 
 
 
9
  load_dotenv()
10
 
11
- sys.path.insert(0, str(Path(__file__).parent / "src"))
12
- from fot_recommender.rag_pipeline import ( # noqa: E402
 
 
13
  load_knowledge_base,
14
  initialize_embedding_model,
15
  generate_recommendation_summary,
16
  )
17
-
18
- # --- Define the project root based on this script's location ---
19
- APP_ROOT = Path(__file__).parent
20
 
21
  # --- Define ABSOLUTE paths to the data artifacts ---
22
  FAISS_INDEX_PATH = APP_ROOT / "data" / "processed" / "faiss_index.bin"
23
  KB_PATH = APP_ROOT / "data" / "processed" / "knowledge_base_final_chunks.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
 
 
25
  ACCESS_PASSWORD = os.environ.get("DEMO_PASSWORD", "")
26
- if ACCESS_PASSWORD and len(ACCESS_PASSWORD) > 10: # Check if it looks like a real key
27
- print("✅ DEMO_PASSWORD secret loaded successfully from the environment.")
28
- else:
29
- print("❌ FATAL: DEMO_PASSWORD secret was NOT FOUND in the environment.")
30
-
31
  FOT_GOOGLE_API_KEY = os.environ.get("FOT_GOOGLE_API_KEY", "")
32
- if FOT_GOOGLE_API_KEY and len(FOT_GOOGLE_API_KEY) > 10: # Check if it looks like a real key
33
- print("✅ FOT_GOOGLE_API_KEY secret loaded successfully from the environment.")
34
- else:
35
- print("❌ FATAL: FOT_GOOGLE_API_KEY secret was NOT FOUND in the environment.")
36
-
37
- print("--- Initializing API: Loading models and data... ---")
38
 
39
- # --- Load artifacts using the new absolute paths ---
 
40
  index = faiss.read_index(str(FAISS_INDEX_PATH))
41
  knowledge_base_chunks = load_knowledge_base(str(KB_PATH))
 
42
  embedding_model = initialize_embedding_model()
43
-
44
  print("✅ API initialized successfully.")
45
 
46
- # --- Define the core RAG function that the API exposes ---
47
-
48
-
49
  def get_recommendations_api(student_narrative, persona, password):
50
- """The main function that runs the RAG pipeline, protected by a password."""
51
  if password != ACCESS_PASSWORD:
52
- return "Authentication failed. Please check the access key."
 
 
53
  if not student_narrative:
54
- return "Please enter a student narrative."
 
 
 
55
 
56
  # 1. RETRIEVE
57
- query_embedding = np.asarray(embedding_model.encode([student_narrative])).astype(
58
- "float32"
59
- )
60
  scores, indices = index.search(query_embedding, k=3)
61
- retrieved_chunks_with_scores = [
62
- (knowledge_base_chunks[i], score)
63
- for i, score in zip(indices[0], scores[0])
64
- if score >= 0.4
65
- ]
66
  if not retrieved_chunks_with_scores:
67
- return "Could not find relevant interventions for this query."
 
68
 
69
  # 2. GENERATE
70
  synthesized_recommendation = generate_recommendation_summary(
@@ -74,33 +92,131 @@ def get_recommendations_api(student_narrative, persona, password):
74
  persona=persona,
75
  )
76
 
77
- # 3. Augment with evidence
78
- evidence_header = "\n\n---\n\n**Evidence Base:**"
79
- evidence_list = ""
80
- for chunk, score in retrieved_chunks_with_scores:
81
- evidence_list += f"\n- **{chunk['title']}** (Source: {chunk['source_document']}, Relevance: {score:.2f})"
82
- return synthesized_recommendation + evidence_header + evidence_list
83
-
84
-
85
- # --- Create and launch the Gradio Interface ---
86
- sample_narrative = "This student is struggling to keep up with coursework, having failed one core class and earning only 2.5 credits..."
87
- interface = gr.Interface(
88
- fn=get_recommendations_api,
89
- inputs=[
90
- gr.Textbox(lines=5, label="Student Narrative", value=sample_narrative),
91
- gr.Radio(
92
- ["teacher", "parent", "principal"],
93
- label="Who is this for?",
94
- value="teacher",
95
- ),
96
- gr.Textbox(
97
- label="Access Key",
98
- type="password",
99
- info="Enter the access key provided for the demo.",
100
- ),
101
- ],
102
- outputs=gr.Markdown(label="Synthesized Recommendation", show_copy_button=True),
103
- title="Freshman On-Track Intervention Recommender API",
104
- description="A live API demonstrating the FOT Recommender. Enter the provided access key to use.",
105
- theme=gr.themes.Soft(), # type: ignore
106
- ).launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import os
4
  import numpy as np
5
  import sys
6
+ import json
7
+ import tempfile
8
+ import datetime
9
  from pathlib import Path
10
  from dotenv import load_dotenv
11
 
12
+ from fot_recommender.utils import load_citations
13
+
14
  load_dotenv()
15
 
16
+ # --- Define the project root and paths ---
17
+ APP_ROOT = Path(__file__).parent
18
+ sys.path.insert(0, str(APP_ROOT / "src"))
19
+ from fot_recommender.rag_pipeline import (
20
  load_knowledge_base,
21
  initialize_embedding_model,
22
  generate_recommendation_summary,
23
  )
24
+ from fot_recommender.utils import format_evidence_for_display
 
 
25
 
26
  # --- Define ABSOLUTE paths to the data artifacts ---
27
  FAISS_INDEX_PATH = APP_ROOT / "data" / "processed" / "faiss_index.bin"
28
  KB_PATH = APP_ROOT / "data" / "processed" / "knowledge_base_final_chunks.json"
29
+ CITATIONS_PATH = APP_ROOT / "data" / "processed" / "citations.json"
30
+
31
+ # --- Define Example Narratives for the UI (with new 'short_title') ---
32
+ EXAMPLE_NARRATIVES = [
33
+ {
34
+ "short_title": "Overwhelmed",
35
+ "title": "Overwhelmed Freshman (Academic & Attendance)",
36
+ "narrative": "A comprehensive support plan is urgently needed for this freshman. Academic performance is a critical concern, with failures in both Math and English leading to a credit deficiency of only 2 out of 4 expected credits. This academic struggle is compounded by a drop in attendance to 85% and a recent behavioral flag for an outburst in class, suggesting the student is significantly overwhelmed by the transition to high school."
37
+ },
38
+ {
39
+ "short_title": "Withdrawn",
40
+ "title": "Withdrawn Freshman (Social-Emotional)",
41
+ "narrative": "Academically, this freshman appears to be thriving, with a high GPA and perfect attendance. A closer look at classroom performance, however, reveals a student who is completely withdrawn. They do not participate in discussions or engage in any extracurricular activities, and teacher notes repeatedly describe them as 'isolated.' The lack of behavioral flags is a result of non-engagement, not positive conduct, pointing to a clear need for interventions focused on social-emotional learning and school connectedness."
42
+ },
43
+ {
44
+ "short_title": "Disruptive",
45
+ "title": "Disruptive Freshman (Behavioral)",
46
+ "narrative": "While this student's academics and credits earned are currently on track and attendance is acceptable at 92%, a significant pattern of disruptive behavior is jeopardizing their long-term success. An accumulation of five behavioral flags across multiple classes indicates a primary need for interventions in behavior management and positive conduct. Support should be focused on mentoring and strategies to foster appropriate classroom engagement before these behaviors begin to negatively impact their academic standing."
47
+ }
48
+ ]
49
+ # Use the short title for the UI, but map it to the full narrative
50
+ EXAMPLE_MAP = {ex["short_title"]: ex["narrative"] for ex in EXAMPLE_NARRATIVES}
51
+ EXAMPLE_TITLES = list(EXAMPLE_MAP.keys())
52
 
53
+
54
+ # --- Load Environment Variables and Secrets ---
55
  ACCESS_PASSWORD = os.environ.get("DEMO_PASSWORD", "")
 
 
 
 
 
56
  FOT_GOOGLE_API_KEY = os.environ.get("FOT_GOOGLE_API_KEY", "")
 
 
 
 
 
 
57
 
58
+ # --- Initialize models and data ---
59
+ print("--- Initializing API: Loading models and data... ---")
60
  index = faiss.read_index(str(FAISS_INDEX_PATH))
61
  knowledge_base_chunks = load_knowledge_base(str(KB_PATH))
62
+ citations_map = load_citations(str(CITATIONS_PATH))
63
  embedding_model = initialize_embedding_model()
 
64
  print("✅ API initialized successfully.")
65
 
 
 
 
66
  def get_recommendations_api(student_narrative, persona, password):
67
+ """The main function that runs the RAG pipeline and prepares data for export."""
68
  if password != ACCESS_PASSWORD:
69
+ yield "Authentication failed.", gr.update(interactive=True), gr.update(visible=False), None, gr.update(visible=False)
70
+ return
71
+
72
  if not student_narrative:
73
+ yield "Please enter a student narrative.", gr.update(interactive=True), gr.update(visible=False), None, gr.update(visible=False)
74
+ return
75
+
76
+ yield "Processing...", gr.update(interactive=False), gr.update(visible=False), None, gr.update(visible=False)
77
 
78
  # 1. RETRIEVE
79
+ query_embedding = np.asarray(embedding_model.encode([student_narrative])).astype("float32")
 
 
80
  scores, indices = index.search(query_embedding, k=3)
81
+ retrieved_chunks_with_scores = [(knowledge_base_chunks[i], score) for i, score in zip(indices[0], scores[0]) if score >= 0.4]
82
+
 
 
 
83
  if not retrieved_chunks_with_scores:
84
+ yield "Could not find relevant interventions.", gr.update(interactive=True), gr.update(visible=False), None, gr.update(visible=False)
85
+ return
86
 
87
  # 2. GENERATE
88
  synthesized_recommendation = generate_recommendation_summary(
 
92
  persona=persona,
93
  )
94
 
95
+ # 3. Augment with evidence for UI
96
+ formatted_evidence = format_evidence_for_display(retrieved_chunks_with_scores, citations_map)
97
+ evidence_header = "\n\n---\n\n### Evidence Base\n"
98
+ evidence_list_str = ""
99
+ for evidence in formatted_evidence:
100
+ evidence_list_str += f"\n- **{evidence['title']}**\n"
101
+ evidence_list_str += f" - **Source:** {evidence['source']}\n"
102
+ evidence_list_str += f" - **Page(s):** {evidence['pages']}\n"
103
+ evidence_list_str += f" - **Relevance Score:** {evidence['score']}\n"
104
+ evidence_list_str += f" - **Content Snippet:**\n > {evidence['content_snippet']}\n"
105
+
106
+ final_output = synthesized_recommendation + evidence_header + evidence_list_str
107
+
108
+ # 4. Assemble Evaluation Data
109
+ evaluation_data = {
110
+ "timestamp": datetime.datetime.now().isoformat(),
111
+ "inputs": {"student_narrative": student_narrative, "persona": persona},
112
+ "retrieval_results": [
113
+ {
114
+ "chunk_title": chunk['title'], "relevance_score": float(score),
115
+ "source_document": chunk['source_document'], "page_info": chunk.get('fot_pages', 'N/A'),
116
+ "original_content": chunk.get('original_content', ''), "citation_info": citations_map.get(chunk['source_document'], {})
117
+ } for chunk, score in retrieved_chunks_with_scores
118
+ ],
119
+ "llm_output": {"synthesized_recommendation": synthesized_recommendation},
120
+ "final_ui_output": final_output
121
+ }
122
+
123
+ # 5. Create a temporary file for download
124
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".json", encoding='utf-8') as f:
125
+ json.dump(evaluation_data, f, indent=4)
126
+ temp_file_path = f.name
127
+
128
+ yield final_output, gr.update(interactive=True), gr.update(visible=True), evaluation_data, gr.update(value=temp_file_path, visible=True)
129
+
130
+
131
+ # --- UI Helper Functions ---
132
+ def clear_all():
133
+ """Clears inputs, outputs, and hides the export section."""
134
+ return "", None, "", gr.update(visible=False), None, gr.update(visible=False, value=None)
135
+
136
+ def update_narrative_from_example(selection):
137
+ """Populates the narrative textbox when an example radio button is selected."""
138
+ return EXAMPLE_MAP.get(selection, "")
139
+
140
+ # --- Custom CSS for horizontal radio buttons ---
141
+ CUSTOM_CSS = """
142
+ /* Target the container of the radio buttons and make them horizontal */
143
+ .radio-horizontal .gr-form {
144
+ flex-direction: row;
145
+ flex-wrap: wrap;
146
+ gap: 0.5rem; /* Adjust spacing between buttons */
147
+ }
148
+ """
149
+
150
+ # --- Gradio Interface ---
151
+ with gr.Blocks(theme=gr.themes.Soft(), css=CUSTOM_CSS) as interface:
152
+ gr.Markdown(
153
+ """
154
+ # Freshman On-Track Intervention Recommender
155
+ *A live API demonstrating the FOT Recommender.*
156
+ """
157
+ )
158
+ with gr.Row(equal_height=False):
159
+ with gr.Column(scale=1):
160
+ with gr.Group():
161
+ narrative_input = gr.Textbox(
162
+ lines=8,
163
+ label="Student Narrative",
164
+ placeholder="Describe the student's situation here, or select an example below.",
165
+ )
166
+
167
+ # Use the new short titles and apply the custom CSS class
168
+ example_radio = gr.Radio(
169
+ EXAMPLE_TITLES,
170
+ label="Load an Example Scenario",
171
+ info="Select one to populate the narrative above. Typing a custom narrative will clear this selection.",
172
+ elem_classes=["radio-horizontal"]
173
+ )
174
+
175
+ persona_input = gr.Radio(
176
+ ["teacher", "parent", "principal"],
177
+ label="Who is this recommendation for?",
178
+ value="teacher",
179
+ elem_classes=["radio-horizontal"] # Apply same style here for consistency
180
+ )
181
+ password_input = gr.Textbox(
182
+ label="Access Key",
183
+ type="password",
184
+ info="Enter the access key for the demo."
185
+ )
186
+
187
+ with gr.Row():
188
+ clear_btn = gr.Button("Clear")
189
+ submit_btn = gr.Button("Submit", variant="primary")
190
+
191
+ with gr.Column(scale=2):
192
+ recommendation_output = gr.Markdown(label="Synthesized Recommendation", show_copy_button=True)
193
+ with gr.Accordion("Evaluation Data", open=False, visible=False) as eval_accordion:
194
+ json_viewer = gr.JSON(label="Evaluation JSON")
195
+ download_btn = gr.DownloadButton("Download JSON", visible=False)
196
+
197
+
198
+ # --- Event Handlers ---
199
+ example_radio.change(
200
+ fn=update_narrative_from_example,
201
+ inputs=example_radio,
202
+ outputs=narrative_input
203
+ )
204
+ narrative_input.input(
205
+ fn=lambda: None,
206
+ inputs=None,
207
+ outputs=example_radio
208
+ )
209
+ submit_btn.click(
210
+ fn=get_recommendations_api,
211
+ inputs=[narrative_input, persona_input, password_input],
212
+ outputs=[recommendation_output, submit_btn, eval_accordion, json_viewer, download_btn]
213
+ )
214
+ clear_btn.click(
215
+ fn=clear_all,
216
+ inputs=[],
217
+ outputs=[narrative_input, example_radio, recommendation_output, eval_accordion, json_viewer, download_btn]
218
+ )
219
+
220
+
221
+ if __name__ == "__main__":
222
+ interface.launch()
notebooks/fot_recommender_poc.ipynb CHANGED
@@ -6,11 +6,11 @@
6
  "metadata": {},
7
  "source": [
8
  "# Freshman On-Track (FOT) Intervention Recommender\n",
9
- "### A Standalone Proof-of-Concept\n",
10
  "\n",
11
- "**Goal:** To show, in a few simple steps, how we can turn a description of a struggling student into a set of clear, actionable, and evidence-based recommendations for an educator.\n",
12
  "\n",
13
- "This notebook demonstrates the core Retrieval-Augmented Generation (RAG) pipeline that powers our recommender."
14
  ]
15
  },
16
  {
@@ -27,7 +27,7 @@
27
  },
28
  {
29
  "cell_type": "code",
30
- "execution_count": 8,
31
  "id": "97f37783",
32
  "metadata": {},
33
  "outputs": [
@@ -42,15 +42,14 @@
42
  }
43
  ],
44
  "source": [
45
- "import sys, os, warnings\n",
46
  "from pathlib import Path\n",
47
- "from tqdm import TqdmWarning\n",
48
  "\n",
49
  "# This prevents common, harmless warnings from cluttering the output.\n",
50
  "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n",
51
- "warnings.filterwarnings(\"ignore\", category=TqdmWarning)\n",
52
  "\n",
53
- "# Clones the project from GitHub, but only if it doesn't already exist.\n",
54
  "PROJECT_DIR = \"fot-intervention-recommender\"\n",
55
  "if not Path(PROJECT_DIR).is_dir():\n",
56
  " print(\"🚀 Downloading project files...\")\n",
@@ -124,11 +123,13 @@
124
  "\n",
125
  "Now, we take the student's story and find the most relevant strategies from our **Knowledge Base**—a curated library of best practices and proven interventions.\n",
126
  "\n",
127
- "How do we do this? \n",
128
- "1. We've already converted our knowledge base documents into **vector embeddings** (unique digital fingerprints that capture meaning).\n",
129
- "2. We use a **FAISS vector database**—a super-fast search index—to instantly find the documents with fingerprints most similar to the student's situation.\n",
 
 
130
  "\n",
131
- "Let's see which top 3 strategies our system retrieves for this student."
132
  ]
133
  },
134
  {
@@ -158,9 +159,7 @@
158
  "name": "stderr",
159
  "output_type": "stream",
160
  "text": [
161
- "Batches: 0%| | 0/1 [00:00<?, ?it/s]/Users/charlesfeinn/Developer/job_applications/fot-intervention-recommender/.venv/lib/python3.12/site-packages/torch/nn/modules/module.py:1520: FutureWarning: `encoder_attention_mask` is deprecated and will be removed in version 4.55.0 for `BertSdpaSelfAttention.forward`.\n",
162
- " return forward_call(*args, **kwargs)\n",
163
- "Batches: 100%|████████████████████████████████████| 1/1 [00:02<00:00, 2.08s/it]\n"
164
  ]
165
  },
166
  {
@@ -172,13 +171,14 @@
172
  "FAISS index created with 27 vectors.\n",
173
  "\n",
174
  "Searching for top 3 interventions for query: 'This student is struggling to keep up with coursework, having failed one core cl...'\n",
175
- "Found 3 relevant interventions.\n"
 
176
  ]
177
  },
178
  {
179
  "data": {
180
  "text/markdown": [
181
- "**Top 3 Retrieved Strategies:**"
182
  ],
183
  "text/plain": [
184
  "<IPython.core.display.Markdown object>"
@@ -190,7 +190,15 @@
190
  {
191
  "data": {
192
  "text/markdown": [
193
- "- **Strategy: Differentiating Intervention Tiers** (Source: *NCS_OTToolkit_2ndEd_October_2017_updated.pdf*, Relevance: 0.57)"
 
 
 
 
 
 
 
 
194
  ],
195
  "text/plain": [
196
  "<IPython.core.display.Markdown object>"
@@ -202,7 +210,26 @@
202
  {
203
  "data": {
204
  "text/markdown": [
205
- "- **Tool: Intervention Tracking** (Source: *NCS_OTToolkit_2ndEd_October_2017_updated.pdf*, Relevance: 0.54)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  ],
207
  "text/plain": [
208
  "<IPython.core.display.Markdown object>"
@@ -214,7 +241,47 @@
214
  {
215
  "data": {
216
  "text/markdown": [
217
- "- **Tool: BAG Report (Example)** (Source: *NCS_OTToolkit_2ndEd_October_2017_updated.pdf*, Relevance: 0.53)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  ],
219
  "text/plain": [
220
  "<IPython.core.display.Markdown object>"
@@ -225,31 +292,28 @@
225
  }
226
  ],
227
  "source": [
228
- "# Import the necessary functions from our project's code\n",
229
  "from fot_recommender.rag_pipeline import (\n",
230
  " load_knowledge_base,\n",
231
  " initialize_embedding_model,\n",
232
  " create_embeddings,\n",
233
  " create_vector_db,\n",
234
- " search_interventions,\n",
235
- " generate_recommendation_summary\n",
236
  ")\n",
237
  "from fot_recommender.utils import display_recommendations\n",
238
  "\n",
239
- "# --- Load all the components of our RAG system ---\n",
240
- "\n",
241
- "# 1. Load the chunked knowledge base\n",
242
  "kb_path = project_path / \"data\" / \"processed\" / \"knowledge_base_final_chunks.json\"\n",
 
243
  "knowledge_base_chunks = load_knowledge_base(str(kb_path))\n",
 
 
244
  "\n",
245
- "# 2. Initialize the embedding model\n",
246
  "embedding_model = initialize_embedding_model()\n",
247
- "\n",
248
- "# 3. Create embeddings and the vector database\n",
249
  "embeddings = create_embeddings(knowledge_base_chunks, embedding_model)\n",
250
  "vector_db = create_vector_db(embeddings)\n",
251
  "\n",
252
- "# --- Perform the search! ---\n",
253
  "retrieved_interventions = search_interventions(\n",
254
  " query=student_query,\n",
255
  " model=embedding_model,\n",
@@ -259,10 +323,9 @@
259
  " min_similarity_score=0.4\n",
260
  ")\n",
261
  "\n",
262
- "# Display the titles of what we found\n",
263
- "display(Markdown(\"**Top 3 Retrieved Strategies:**\"))\n",
264
- "for chunk, score in retrieved_interventions:\n",
265
- " display(Markdown(f\"- **{chunk['title']}** (Source: *{chunk['source_document']}*, Relevance: {score:.2f})\"))"
266
  ]
267
  },
268
  {
@@ -270,178 +333,16 @@
270
  "id": "2202209d",
271
  "metadata": {},
272
  "source": [
273
- "## Step 4: Create the Recommendation (The \"Generation\" Step)\n",
274
- "\n",
275
- "Finding the right documents is only half the battle. Raw research isn't very helpful to a busy teacher. \n",
276
- "\n",
277
- "In this final step, we use a powerful Large Language Model (Google's Gemini API) to act as an expert instructional coach. We give it the student's story and the relevant strategies we just retrieved. The AI's job is to **synthesize** this information into a concise, practical recommendation tailored specifically for a teacher.\n",
278
- "\n",
279
- "This is the final output of our system."
280
- ]
281
- },
282
- {
283
- "cell_type": "code",
284
- "execution_count": 4,
285
- "id": "62ee35bc",
286
- "metadata": {},
287
- "outputs": [
288
- {
289
- "name": "stdout",
290
- "output_type": "stream",
291
- "text": [
292
- "\n",
293
- "Synthesizing recommendation for persona: 'teacher' using Gemini...\n",
294
- "Synthesis complete.\n"
295
- ]
296
- },
297
- {
298
- "data": {
299
- "text/markdown": [
300
- "### Final Synthesized Recommendation for the Teacher"
301
- ],
302
- "text/plain": [
303
- "<IPython.core.display.Markdown object>"
304
- ]
305
- },
306
- "metadata": {},
307
- "output_type": "display_data"
308
- },
309
- {
310
- "data": {
311
- "text/markdown": [
312
- "This student is experiencing academic difficulty, reflected in a 2.5 GPA and a failing grade in one core class, coupled with attendance concerns (88% attendance versus a 90% target) and one behavioral incident. To address these challenges and support the student's path to graduation, the following interventions are recommended:\n",
313
- "\n",
314
- "\n",
315
- "**1. Implement a Tiered Intervention Strategy:** Determine the extent to which attendance is contributing to the student's academic struggles. (\"Strategy: Differentiating Intervention Tiers\"). If attendance is a significant factor, refer the student to the appropriate support services (Success Team or Attendance Dean, as indicated by the BAG Report format) to address these issues directly. This allows more focused support from the teaching staff for academic interventions.\n",
316
- "\n",
317
- "**2. Utilize a Robust Intervention Tracking System:** Implement a system to monitor the student's progress, focusing on attendance, GPA, and behavior. (\"Tool: Intervention Tracking\"). This system should clearly document interventions (e.g., tutoring sessions, mentorship meetings), and track the student’s progress in each core course (GPA and attendance rates) at two checkpoints within a ten-week period. The \"BAG Report\" format provides a useful template to track behavior, attendance and grades. This data will inform adjustments to the support plan.\n",
318
- "\n",
319
- "**3. Regularly Review the Student's \"BAG Report\" (or Equivalent):** Use a reporting mechanism (such as the BAG report example) to regularly review the student's performance across all three key areas: Behavior, Attendance, and Grades. This visual representation highlights areas of strength and areas requiring immediate intervention, allowing for proactive adjustments to support strategies. This aligns with the recommendation to monitor multiple key performance indicators to improve student outcomes effectively.\n"
320
- ],
321
- "text/plain": [
322
- "<IPython.core.display.Markdown object>"
323
- ]
324
- },
325
- "metadata": {},
326
- "output_type": "display_data"
327
- }
328
- ],
329
- "source": [
330
- "from dotenv import load_dotenv\n",
331
- "\n",
332
- "# Load the API key from a .env file (if it exists)\n",
333
- "load_dotenv(project_path / '.env') \n",
334
- "api_key = os.getenv(\"FOT_GOOGLE_API_KEY\")\n",
335
- "\n",
336
- "if not api_key:\n",
337
- " print(\"✋ FOT_GOOGLE_API_KEY not found. Please provide your Google API key to generate the summary.\")\n",
338
- " final_recommendation = \"(API Key not provided - could not generate summary)\"\n",
339
- "else:\n",
340
- " final_recommendation = generate_recommendation_summary(\n",
341
- " retrieved_chunks=retrieved_interventions,\n",
342
- " student_narrative=student_query,\n",
343
- " api_key=api_key,\n",
344
- " persona=\"teacher\"\n",
345
- " )\n",
346
- "\n",
347
- "display(Markdown(\"### Final Synthesized Recommendation for the Teacher\"))\n",
348
- "display(Markdown(final_recommendation))"
349
- ]
350
- },
351
- {
352
- "cell_type": "markdown",
353
- "id": "d3718297",
354
- "metadata": {},
355
- "source": [
356
- "## Bonus: See the Evidence\n",
357
  "\n",
358
- "The recommendation above isn't just made up—it's directly grounded in the documents we retrieved. Here are the specific text snippets that the AI used to create its summary. This ensures our recommendations are always transparent and evidence-based."
359
- ]
360
- },
361
- {
362
- "cell_type": "code",
363
- "execution_count": 5,
364
- "id": "1b0cb720",
365
- "metadata": {},
366
- "outputs": [
367
- {
368
- "name": "stdout",
369
- "output_type": "stream",
370
- "text": [
371
- "\n",
372
- "--- Top Recommended Interventions ---\n",
373
- "\n",
374
- "--- Recommendation 1 (Similarity Score: 0.5735) ---\n",
375
- " Title: Strategy: Differentiating Intervention Tiers\n",
376
- " Source: NCS_OTToolkit_2ndEd_October_2017_updated.pdf (Pages: 46)\n",
377
- " \n",
378
- " Content Snippet:\n",
379
- " \"To what degree is attendance playing a role in student performance? To whom do you refer Tier 3 students who have serious attendance issues (inside and outside of the school) so that the Success Team can really concentrate on supporting Tier 2 students?...\"\n",
380
- "--------------------------------------------------\n",
381
- "\n",
382
- "--- Recommendation 2 (Similarity Score: 0.5416) ---\n",
383
- " Title: Tool: Intervention Tracking\n",
384
- " Source: NCS_OTToolkit_2ndEd_October_2017_updated.pdf (Pages: 49)\n",
385
- " \n",
386
- " Content Snippet:\n",
387
- " \"Features of Good Intervention Tracking Tools:\n",
388
- " • Name of the intervention and what key performance indicator it addresses (attendance, point-in-time On-Track rates, GPA, behavior metric, etc.)\n",
389
- " • Names of the targeted students\n",
390
- " ° If tracking grades, include each core course's average expressed as a percentage\n",
391
- " • Intervention contacts/implementation evidence\n",
392
- " ° Tutoring attendance\n",
393
- " ° Mentorship contact dates\n",
394
- " ° \"Office hours\" visits\n",
395
- " • Point-in-time progress on the key performance...\"\n",
396
- "--------------------------------------------------\n",
397
- "\n",
398
- "--- Recommendation 3 (Similarity Score: 0.5328) ---\n",
399
- " Title: Tool: BAG Report (Example)\n",
400
- " Source: NCS_OTToolkit_2ndEd_October_2017_updated.pdf (Pages: 61)\n",
401
- " \n",
402
- " Content Snippet:\n",
403
- " \"Student: Keith\n",
404
- " Grade Level: 9\n",
405
- " 8th Period Teacher: Donson\n",
406
- " The numbers below reflect totals through Semester 1\n",
407
- " \n",
408
- " BEHAVIOR - In what ways do I contribute to a Safe and Respectful school climate?\n",
409
- " • # of Infractions (# of Major Infractions): 5 (1)\n",
410
- " • # of Days of In-School-Suspension (ISS): 10\n",
411
- " • # of Days of Out-of-School-Suspension (OSS): 0\n",
412
- " If I have any questions regarding my misconducts, I should schedule an appointment with the Dean of Discipline.\n",
413
- " \n",
414
- " ATTENDANCE - Do my actions r...\"\n",
415
- "--------------------------------------------------\n"
416
- ]
417
- }
418
- ],
419
- "source": [
420
- "display_recommendations(retrieved_interventions)"
421
- ]
422
- },
423
- {
424
- "cell_type": "markdown",
425
- "id": "254d4cdf",
426
- "metadata": {},
427
- "source": [
428
- "## Explore the Live Demo!\n",
429
  "\n",
430
- "You've seen the step-by-step process of how our RAG system turns a student's story into an actionable, evidence-based plan. Now, it's time to try it yourself with any student scenario you can imagine!\n",
431
  "\n",
432
- "We have deployed this entire system as an interactive web application on Hugging Face Spaces. Click the link below to access the live demo—no setup or API key required.\n",
433
  "\n",
434
- "\n",
435
- "#### [👉 Click Here to Launch the Live FOT Recommender API](https://huggingface.co/spaces/chuckfinca/fot-recommender-api)\n"
436
  ]
437
- },
438
- {
439
- "cell_type": "code",
440
- "execution_count": null,
441
- "id": "64867bc5-2762-4c69-aa72-e4e7cf911019",
442
- "metadata": {},
443
- "outputs": [],
444
- "source": []
445
  }
446
  ],
447
  "metadata": {
 
6
  "metadata": {},
7
  "source": [
8
  "# Freshman On-Track (FOT) Intervention Recommender\n",
9
+ "### A Proof-of-Concept\n",
10
  "\n",
11
+ "**Goal:** To show, in just a few steps, how we can turn a description of a struggling student into a set of clear, actionable, and evidence-based strategies.\n",
12
  "\n",
13
+ "This notebook demonstrates the core **Retrieval** engine that powers our recommender. It shows how the system intelligently finds the most relevant documents from a knowledge base to match a student's needs."
14
  ]
15
  },
16
  {
 
27
  },
28
  {
29
  "cell_type": "code",
30
+ "execution_count": 1,
31
  "id": "97f37783",
32
  "metadata": {},
33
  "outputs": [
 
42
  }
43
  ],
44
  "source": [
45
+ "import sys, os, warnings, json\n",
46
  "from pathlib import Path\n",
 
47
  "\n",
48
  "# This prevents common, harmless warnings from cluttering the output.\n",
49
  "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n",
50
+ "warnings.filterwarnings(\"ignore\", category=FutureWarning) # Suppress specific torch warning\n",
51
  "\n",
52
+ "# Clones the project from GitHub if not already present.\n",
53
  "PROJECT_DIR = \"fot-intervention-recommender\"\n",
54
  "if not Path(PROJECT_DIR).is_dir():\n",
55
  " print(\"🚀 Downloading project files...\")\n",
 
123
  "\n",
124
  "Now, we take the student's story and find the most relevant strategies from our **Knowledge Base**—a curated library of best practices and proven interventions.\n",
125
  "\n",
126
+ "This next cell will perform the core retrieval logic:\n",
127
+ "1. Load the pre-processed knowledge base and citation data.\n",
128
+ "2. Initialize the text embedding model.\n",
129
+ "3. Create a searchable Facebook AI Similarity Search (FAISS) vector index.\n",
130
+ "4. Use the student query to find the top 3 most similar interventions.\n",
131
  "\n",
132
+ "The output will show the evidence-based strategies our system identified."
133
  ]
134
  },
135
  {
 
159
  "name": "stderr",
160
  "output_type": "stream",
161
  "text": [
162
+ "Batches: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:02<00:00, 2.24s/it]\n"
 
 
163
  ]
164
  },
165
  {
 
171
  "FAISS index created with 27 vectors.\n",
172
  "\n",
173
  "Searching for top 3 interventions for query: 'This student is struggling to keep up with coursework, having failed one core cl...'\n",
174
+ "Found 3 relevant interventions.\n",
175
+ "✅ Successfully loaded models and retrieved the top 3 most relevant interventions from the knowledge base.\n"
176
  ]
177
  },
178
  {
179
  "data": {
180
  "text/markdown": [
181
+ "### Evidence Base"
182
  ],
183
  "text/plain": [
184
  "<IPython.core.display.Markdown object>"
 
190
  {
191
  "data": {
192
  "text/markdown": [
193
+ "\n",
194
+ "**Strategy: Differentiating Intervention Tiers**\n",
195
+ "- **Source:** *Freshman On‑Track Toolkit (2nd Edition)* (Network for College Success, 2017).\n",
196
+ "- **Page(s):** Pages: 46\n",
197
+ "- **Relevance Score:** 0.57\n",
198
+ "- **Content Snippet:**\n",
199
+ "> To what degree is attendance playing a role in student performance? To whom do you refer Tier 3 students who have serious attendance issues (inside and outside of the school) so that the Success Team can really concentrate on supporting Tier 2 students?\n",
200
+ "\n",
201
+ "---\n"
202
  ],
203
  "text/plain": [
204
  "<IPython.core.display.Markdown object>"
 
210
  {
211
  "data": {
212
  "text/markdown": [
213
+ "\n",
214
+ "**Tool: Intervention Tracking**\n",
215
+ "- **Source:** *Freshman On‑Track Toolkit (2nd Edition)* (Network for College Success, 2017).\n",
216
+ "- **Page(s):** Pages: 49\n",
217
+ "- **Relevance Score:** 0.54\n",
218
+ "- **Content Snippet:**\n",
219
+ "> Features of Good Intervention Tracking Tools:\n",
220
+ "> • Name of the intervention and what key performance indicator it addresses (attendance, point-in-time On-Track rates, GPA, behavior metric, etc.)\n",
221
+ "> • Names of the targeted students\n",
222
+ "> ° If tracking grades, include each core course's average expressed as a percentage\n",
223
+ "> • Intervention contacts/implementation evidence\n",
224
+ "> ° Tutoring attendance\n",
225
+ "> ° Mentorship contact dates\n",
226
+ "> ° \"Office hours\" visits\n",
227
+ "> • Point-in-time progress on the key performance indicator impacted by the intervention\n",
228
+ "> ° Should include at least 2 checkpoints within a 10-week period\n",
229
+ "> ° If tracking grades, provide an average expressed as a percentage for each core course\n",
230
+ "> ° If tracking attendance, provide number of cumulative absences and/or tardies\n",
231
+ "\n",
232
+ "---\n"
233
  ],
234
  "text/plain": [
235
  "<IPython.core.display.Markdown object>"
 
241
  {
242
  "data": {
243
  "text/markdown": [
244
+ "\n",
245
+ "**Tool: BAG Report (Example)**\n",
246
+ "- **Source:** *Freshman On‑Track Toolkit (2nd Edition)* (Network for College Success, 2017).\n",
247
+ "- **Page(s):** Pages: 61\n",
248
+ "- **Relevance Score:** 0.53\n",
249
+ "- **Content Snippet:**\n",
250
+ "> Student: Keith\n",
251
+ "> Grade Level: 9\n",
252
+ "> 8th Period Teacher: Donson\n",
253
+ "> The numbers below reflect totals through Semester 1\n",
254
+ "> \n",
255
+ "> BEHAVIOR - In what ways do I contribute to a Safe and Respectful school climate?\n",
256
+ "> • # of Infractions (# of Major Infractions): 5 (1)\n",
257
+ "> • # of Days of In-School-Suspension (ISS): 10\n",
258
+ "> • # of Days of Out-of-School-Suspension (OSS): 0\n",
259
+ "> If I have any questions regarding my misconducts, I should schedule an appointment with the Dean of Discipline.\n",
260
+ "> \n",
261
+ "> ATTENDANCE - Do my actions reflect the real me?\n",
262
+ "> • Days Enrolled: 80\n",
263
+ "> • Days Present: 73\n",
264
+ "> • Days Absent: 7\n",
265
+ "> • My Year-to-Date Attendance Rate is 91%\n",
266
+ "> If I have any questions regarding my attendance, I should schedule an appointment with the Attendance Dean.\n",
267
+ "> \n",
268
+ "> GRADES - How am I doing academically in my classes? Do my grades represent my true ability?\n",
269
+ "> Period | Courses | Teacher | Grade\n",
270
+ "> P1 | Algebra 1 | Flint | D\n",
271
+ "> P2 | English 1 | Lemon | B\n",
272
+ "> P3 | World Studies | Moeller | C\n",
273
+ "> P4 | PE I-Health | Spann | A\n",
274
+ "> P5 | Lunch | | \n",
275
+ "> P6 | Science | Tyson | D\n",
276
+ "> P7 | Photography | McCain | B\n",
277
+ "> P8 | Intro to Comp | Penny | A\n",
278
+ "> \n",
279
+ "> My Estimated GPA is 2.57\n",
280
+ "> (this estimate does NOT include any previous semesters)\n",
281
+ "> \n",
282
+ "> If I have any questions regarding my grade in a course, I should schedule an appointment with my Teacher.\n",
283
+ "\n",
284
+ "---\n"
285
  ],
286
  "text/plain": [
287
  "<IPython.core.display.Markdown object>"
 
292
  }
293
  ],
294
  "source": [
 
295
  "from fot_recommender.rag_pipeline import (\n",
296
  " load_knowledge_base,\n",
297
  " initialize_embedding_model,\n",
298
  " create_embeddings,\n",
299
  " create_vector_db,\n",
300
+ " search_interventions\n",
 
301
  ")\n",
302
  "from fot_recommender.utils import display_recommendations\n",
303
  "\n",
304
+ "# 1. Load data\n",
 
 
305
  "kb_path = project_path / \"data\" / \"processed\" / \"knowledge_base_final_chunks.json\"\n",
306
+ "citations_path = project_path / \"data\" / \"processed\" / \"citations.json\"\n",
307
  "knowledge_base_chunks = load_knowledge_base(str(kb_path))\n",
308
+ "with open(citations_path, \"r\") as f:\n",
309
+ " citations_map = {item[\"source_document\"]: item for item in json.load(f)}\n",
310
  "\n",
311
+ "# 2. Initialize models and DB (quietly)\n",
312
  "embedding_model = initialize_embedding_model()\n",
 
 
313
  "embeddings = create_embeddings(knowledge_base_chunks, embedding_model)\n",
314
  "vector_db = create_vector_db(embeddings)\n",
315
  "\n",
316
+ "# 3. Perform search (quietly)\n",
317
  "retrieved_interventions = search_interventions(\n",
318
  " query=student_query,\n",
319
  " model=embedding_model,\n",
 
323
  " min_similarity_score=0.4\n",
324
  ")\n",
325
  "\n",
326
+ "# 4. Display a clean summary and the rich results\n",
327
+ "print(f\" Successfully loaded models and retrieved the top {len(retrieved_interventions)} most relevant interventions from the knowledge base.\")\n",
328
+ "display_recommendations(retrieved_interventions, citations_map)"
 
329
  ]
330
  },
331
  {
 
333
  "id": "2202209d",
334
  "metadata": {},
335
  "source": [
336
+ "## Step 4: See the Full System in the Live Demo!\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  "\n",
338
+ "You've just seen the core **Retrieval** engine at work. The system successfully took a student's story and identified the most relevant, evidence-based strategies from our knowledge base.\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  "\n",
340
+ "The final step in our RAG pipeline is **Generation**, where we use a Large Language Model to synthesize this evidence into a clear, actionable recommendation for an educator. This step requires a secure API key, so we've hosted it in an interactive web application.\n",
341
  "\n",
342
+ "Click the link below to see the full system in action. You can use the student narrative from this notebook or try your own!\n",
343
  "\n",
344
+ "### [👉 Click Here to Launch the Live FOT Recommender API](https://huggingface.co/spaces/chuckfinca/fot-recommender-api)"
 
345
  ]
 
 
 
 
 
 
 
 
346
  }
347
  ],
348
  "metadata": {
src/fot_recommender/utils.py CHANGED
@@ -1,27 +1,104 @@
1
- from typing import List, Dict, Any, Tuple
 
 
2
 
3
-
4
- def display_recommendations(results: List[Tuple[Dict[str, Any], float]]):
5
  """
6
- A helper function to neatly print the results of a semantic search.
7
- This function is designed to be called from a notebook or a command-line script.
8
-
9
- Args:
10
- results: A list of tuples, where each tuple contains a result chunk (dict)
11
- and its similarity score (float).
12
  """
13
  if not results:
14
- print("\nNo relevant interventions were found for this query.")
15
  return
16
 
17
- print("\n--- Top Recommended Interventions ---")
18
- for i, (chunk, score) in enumerate(results):
19
- print(f"\n--- Recommendation {i + 1} (Similarity Score: {score:.4f}) ---")
20
- print(f" Title: {chunk['title']}")
21
- print(f" Source: {chunk['source_document']} ({chunk['fot_pages']})")
22
-
23
- # Indent the content for better readability
24
- content = chunk["original_content"]
25
- indented_content = "\n ".join(content.splitlines())
26
- print(f' \n Content Snippet:\n "{indented_content[:500]}..."')
27
- print("-" * 50)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import json
3
+ from IPython.display import display, Markdown
4
 
5
+ def display_recommendations(results: list, citations_map: dict):
 
6
  """
7
+ Displays the retrieved recommendations in a rich, Markdown-formatted output
8
+ directly within a Jupyter/Colab notebook by using the shared formatter.
 
 
 
 
9
  """
10
  if not results:
11
+ display(Markdown("### No relevant interventions were found for this query."))
12
  return
13
 
14
+ # 1. Get the formatted data from the shared function
15
+ formatted_evidence = format_evidence_for_display(results, citations_map)
16
+
17
+ display(Markdown("### Evidence Base"))
18
+
19
+ # 2. Loop through the clean data and render it for the notebook
20
+ for evidence in formatted_evidence:
21
+ recommendation_md = f"""
22
+ **{evidence['title']}**
23
+ - **Source:** {evidence['source']}
24
+ - **Page(s):** {evidence['pages']}
25
+ - **Relevance Score:** {evidence['score']}
26
+ - **Content Snippet:**
27
+ > {evidence['content_snippet']}
28
+
29
+ ---
30
+ """
31
+ display(Markdown(recommendation_md))
32
+
33
+
34
+
35
+ def create_evaluation_bundle(
36
+ student_narrative: str,
37
+ persona: str,
38
+ retrieved_chunks_with_scores: list,
39
+ synthesized_recommendation: str,
40
+ citations_map: dict
41
+ ) -> dict:
42
+ """
43
+ Assembles a comprehensive dictionary for evaluation and logging purposes.
44
+ """
45
+ evaluation_data = {
46
+ "timestamp": datetime.datetime.now().isoformat(),
47
+ "inputs": {
48
+ "student_narrative": student_narrative,
49
+ "persona": persona,
50
+ },
51
+ "retrieval_results": [
52
+ {
53
+ "chunk_title": chunk['title'],
54
+ "relevance_score": float(score),
55
+ "source_document": chunk['source_document'],
56
+ "page_info": chunk.get('fot_pages', 'N/A'),
57
+ "original_content": chunk.get('original_content', ''),
58
+ "citation_info": citations_map.get(chunk['source_document'], {})
59
+ } for chunk, score in retrieved_chunks_with_scores
60
+ ],
61
+ "llm_output": {
62
+ "synthesized_recommendation": synthesized_recommendation
63
+ }
64
+ }
65
+ return evaluation_data
66
+
67
+ def format_evidence_for_display(results: list, citations_map: dict) -> list:
68
+ """
69
+ Takes raw search results and formats them into a structured list of dictionaries
70
+ ready for display in any environment.
71
+ """
72
+ evidence_list = []
73
+ for chunk, score in results:
74
+ source_doc = chunk.get('source_document', 'N/A')
75
+ citation_info = citations_map.get(source_doc, {})
76
+
77
+ # Consolidate all the formatting logic here
78
+ title = citation_info.get('title', 'N/A')
79
+ author = citation_info.get('author', 'N/A')
80
+ year = citation_info.get('year', 'N/A')
81
+ source_string = f"*{title}* ({author}, {year})."
82
+
83
+ page_info = chunk.get('fot_pages', 'N/A')
84
+
85
+ original_content = chunk.get("original_content", "Content not available.").strip()
86
+ blockquote_content = original_content.replace('\n', '\n> ')
87
+
88
+ evidence_list.append({
89
+ "title": chunk['title'],
90
+ "source": source_string,
91
+ "pages": page_info,
92
+ "score": f"{score:.2f}",
93
+ "content_snippet": blockquote_content
94
+ })
95
+
96
+ return evidence_list
97
+
98
+ def load_citations(path):
99
+ try:
100
+ with open(path, "r", encoding="utf-8") as f:
101
+ citations_list = json.load(f)
102
+ return {item["source_document"]: item for item in citations_list}
103
+ except (FileNotFoundError, json.JSONDecodeError):
104
+ return {}