Kerikim commited on
Commit
a57a470
Β·
1 Parent(s): 2085784
agentic-finlit-backend-output.md CHANGED
@@ -3,8 +3,8 @@
3
  ## πŸ“Š Project Information
4
 
5
  - **Project Name**: `agentic-finlit-backend`
6
- - **Generated On**: 2025-09-08 05:44:11 (Etc/GMT+5 / GMT-05:00)
7
- - **Total Files Processed**: 13
8
  - **Export Tool**: Easy Whole Project to Single Text File for LLMs v1.1.0
9
  - **Tool Author**: Jota / JosΓ© Guilherme Pandolfi
10
 
@@ -23,18 +23,19 @@
23
  β”œβ”€β”€ πŸ“ app/
24
  β”‚ β”œβ”€β”€ πŸ“ agents/
25
  β”‚ β”‚ β”œβ”€β”€ πŸ“„ graph.py (587 B)
26
- β”‚ β”‚ β”œβ”€β”€ πŸ“„ nodes.py (3.37 KB)
27
  β”‚ β”‚ └── πŸ“„ schemas.py (635 B)
28
  β”‚ β”œβ”€β”€ πŸ“„ db.py (2.42 KB)
29
  β”‚ β”œβ”€β”€ πŸ“„ embeddings.py (578 B)
30
  β”‚ β”œβ”€β”€ πŸ“„ hf_client.py (878 B)
31
  β”‚ β”œβ”€β”€ πŸ“„ ingest.py (845 B)
32
- β”‚ β”œβ”€β”€ πŸ“„ main.py (2.25 KB)
33
  β”‚ └── πŸ“„ README.md (107 B)
34
  β”œβ”€β”€ πŸ“„ Dockerfile (632 B)
35
  β”œβ”€β”€ πŸ“„ elkay.txt
 
36
  β”œβ”€β”€ πŸ“„ README.md (275 B)
37
- └── πŸ“„ requirements.txt (332 B)
38
  ```
39
 
40
  ## πŸ“‘ Table of Contents
@@ -60,11 +61,11 @@
60
 
61
  | Metric | Count |
62
  |--------|-------|
63
- | Total Files | 13 |
64
  | Total Directories | 2 |
65
  | Text Files | 12 |
66
- | Binary Files | 1 |
67
- | Total Size | 12.8 KB |
68
 
69
  ### πŸ“„ File Types Distribution
70
 
@@ -72,8 +73,8 @@
72
  |-----------|-------|
73
  | `.py` | 8 |
74
  | `.md` | 2 |
 
75
  | `.txt` | 2 |
76
- | `no extension` | 1 |
77
 
78
  ## πŸ’» File Code Contents
79
 
@@ -119,15 +120,15 @@ def build_graph():
119
  ### <a id="πŸ“„-app-agents-nodes-py"></a>πŸ“„ `app/agents/nodes.py`
120
 
121
  **File Info:**
122
- - **Size**: 3.37 KB
123
  - **Extension**: `.py`
124
  - **Language**: `python`
125
  - **Location**: `app/agents/nodes.py`
126
  - **Relative Path**: `app/agents`
127
  - **Created**: 2025-09-08 04:12:41 (Etc/GMT+5 / GMT-05:00)
128
- - **Modified**: 2025-09-08 04:17:35 (Etc/GMT+5 / GMT-05:00)
129
- - **MD5**: `01bf9b0ed40637415e3d43eb3c018d43`
130
- - **SHA256**: `c8f802e7ee48fe46e70bceef868091fd83123225c166ce72050b7aa3ea1f4f3f`
131
  - **Encoding**: ASCII
132
 
133
  **File code content:**
@@ -137,70 +138,71 @@ from .schemas import AgentState
137
  from ..db import search_chunks, record_submission
138
  from ..embeddings import embed_query
139
  from ..hf_client import chat
 
140
 
141
  def node_rag(state: AgentState) -> AgentState:
142
- qvec = embed_query("Key points of this lesson for a Jamaican primary student.")
 
143
  hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
144
  state.context_chunks = [h["content"] for h in hits]
145
- # Summarize context for the student
146
- sys = {"role":"system","content":"You are a Jamaican primary school tutor. Keep it simple and friendly."}
147
- usr = {"role":"user","content": "Summarize these notes in 5 short bullet points:\n" + "\n\n".join(state.context_chunks)}
148
  state.context_summary = chat([sys, usr], max_new_tokens=180)
149
  return state
150
 
151
  def node_quiz(state: AgentState) -> AgentState:
152
- sys = {"role":"system","content":"You are a Jamaican primary school teacher. Make kid-friendly multiple-choice questions (A-D) from the provided notes."}
153
- usr = {"role":"user","content": "Create 5 MCQs. JSON only: {\"items\":[{\"question\":\"...\",\"options\":[\"A\",\"B\",\"C\",\"D\"],\"answer_key\":\"A\"}...]}\nNotes:\n" + "\n\n".join(state.context_chunks)}
154
  raw = chat([sys, usr], max_new_tokens=350)
155
- import json
156
- data = {}
157
- try: data = json.loads(raw)
158
- except: data = {"items":[]}
159
- items = data.get("items", [])[:7] # 4–7 in your spec
160
- # Normalize
161
  fixed = []
162
  for it in items:
163
- q = it.get("question","").strip()
164
- opts = it.get("options", [])[:4]
165
- while len(opts)<4: opts.append("Option")
166
- key = str(it.get("answer_key","A")).strip()[:1].upper()
167
- if key not in "ABCD": key="A"
168
- if q: fixed.append({"question":q, "options":opts, "answer_key":key, "points":1})
169
  state.quiz_items = fixed
170
  return state
171
 
172
  def node_grade(state: AgentState) -> AgentState:
173
- # simple grading
174
  answers = state.student_answers
175
- score, total, details = 0, len(state.quiz_items), []
176
- for i,(it,ans) in enumerate(zip(state.quiz_items, answers)):
177
- correct = (ans.upper()==it["answer_key"])
 
178
  if correct: score += 1
179
- details.append({"qno":i+1,"user":ans,"key":it["answer_key"],"correct":correct})
180
- state.score, state.total = score, total
181
- # record
182
- if state.assignment_id is None:
183
- state.assignment_id = 0 # if not assigned formally
184
- record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id,
185
- score=score, total=total, details={"items":details})
 
 
 
 
 
186
  return state
187
 
188
  def node_coach_or_celebrate(state: AgentState) -> AgentState:
189
- if state.score == state.total and state.total is not None and state.total>0:
190
  state.route = "celebrate"
191
  return state
192
- # Coach for wrong answers
193
- wrong = []
194
- for it,ans in zip(state.quiz_items, state.student_answers):
195
- if ans.upper()!=it["answer_key"]:
196
- wrong.append({"q":it["question"],"your":ans,"key":it["answer_key"]})
197
- sys = {"role":"system","content":"You are a kind tutor. Explain simply where the student went wrong."}
198
- usr = {"role":"user","content": "Here are the mistakes:\n" + "\n".join([f"Q: {w['q']}\nYour answer: {w['your']}\nCorrect: {w['key']}" for w in wrong]) +
199
- "\nGive short, friendly explanations for each."}
200
- state.feedback = chat([sys,usr], max_new_tokens=220)
201
  state.route = "coach"
202
  return state
203
-
204
  ```
205
 
206
  ---
@@ -458,33 +460,26 @@ def ingest_lesson(lesson_id:int, level_slug:str, raw_text:str):
458
  ### <a id="πŸ“„-app-main-py"></a>πŸ“„ `app/main.py`
459
 
460
  **File Info:**
461
- - **Size**: 2.25 KB
462
  - **Extension**: `.py`
463
  - **Language**: `python`
464
  - **Location**: `app/main.py`
465
  - **Relative Path**: `app`
466
  - **Created**: 2025-09-08 03:31:10 (Etc/GMT+5 / GMT-05:00)
467
- - **Modified**: 2025-09-08 05:33:58 (Etc/GMT+5 / GMT-05:00)
468
- - **MD5**: `8303a8d4c5e8e664e4ff8962623fec51`
469
- - **SHA256**: `c8cc9a31cf9c6ff78196de5648108fd37bad8c36995ab41310b46c90b68bb238`
470
  - **Encoding**: ASCII
471
 
472
  **File code content:**
473
 
474
  ```python
 
475
  from pydantic import BaseModel
476
  from .agents.schemas import AgentState
477
  from .agents.graph import build_graph
478
  from .ingest import ingest_lesson
479
-
480
-
481
- from fastapi import FastAPI
482
-
483
- app = FastAPI()
484
-
485
- @app.get("/health")
486
- def health():
487
- return {"ok": True, "service": "agentic-finlit-backend"}
488
 
489
  api = FastAPI(title="Agentic FinLit Backend")
490
  _graph = build_graph()
@@ -501,48 +496,58 @@ class SubmitQuiz(BaseModel):
501
  answers: list[str]
502
  assignment_id: int | None = None
503
 
 
 
 
 
 
 
 
 
 
 
 
504
  @api.post("/ingest")
505
- def ingest(lesson_id:int, level_slug:str, raw_text:str):
506
  ingest_lesson(lesson_id, level_slug, raw_text)
507
- return {"ok": True}
508
 
509
  @api.post("/agent/start")
510
  def agent_start(payload: StartRun):
511
  state = AgentState(**payload.model_dump())
512
- out = _graph.invoke(state, start_at="rag")
513
  return {"summary": out.context_summary, "ok": True}
514
 
515
  @api.post("/agent/quiz")
516
  def agent_quiz(payload: StartRun):
517
  state = AgentState(**payload.model_dump())
518
- out = _graph.invoke(state, start_at="quiz")
519
  return {"items": out.quiz_items}
520
 
521
  @api.post("/agent/grade")
522
  def agent_grade(payload: SubmitQuiz):
523
- st = AgentState(student_id=payload.student_id, lesson_id=payload.lesson_id,
524
- level_slug=payload.level_slug, assignment_id=payload.assignment_id)
525
  st.student_answers = payload.answers
526
- out = _graph.invoke(st, start_at="grade")
527
- return {"score": out.score, "total": out.total}
528
 
529
  @api.post("/agent/coach_or_celebrate")
530
  def agent_next(payload: SubmitQuiz):
531
- st = AgentState(student_id=payload.student_id, lesson_id=payload.lesson_id,
532
- level_slug=payload.level_slug, assignment_id=payload.assignment_id)
533
  st.student_answers = payload.answers
534
- out = _graph.invoke(st, start_at="coach_or_celebrate")
535
  data = {"route": out.route}
536
  if out.route == "coach":
537
  data["feedback"] = out.feedback
538
- else:
539
  data["message"] = "Great job! Want to play Money Match?"
540
- data["game_slug"] = "money_match"
541
  return data
542
 
543
- if __name__ == "__main__":
544
- import os, uvicorn
545
- uvicorn.run("app.main:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
 
546
  ```
547
 
548
  ---
@@ -630,15 +635,15 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
630
  ### <a id="πŸ“„-requirements-txt"></a>πŸ“„ `requirements.txt`
631
 
632
  **File Info:**
633
- - **Size**: 332 B
634
  - **Extension**: `.txt`
635
  - **Language**: `text`
636
  - **Location**: `requirements.txt`
637
  - **Relative Path**: `root`
638
  - **Created**: 2025-09-08 03:23:55 (Etc/GMT+5 / GMT-05:00)
639
- - **Modified**: 2025-09-08 05:23:44 (Etc/GMT+5 / GMT-05:00)
640
- - **MD5**: `011711e3a3a8373749286cd4918b9079`
641
- - **SHA256**: `6d05e356cd5b401fee707c7613b7b1b0623ef3d160ae0d54fdd43b1442bb69e8`
642
  - **Encoding**: ASCII
643
 
644
  **File code content:**
@@ -661,7 +666,7 @@ python-dotenv==1.0.1
661
  tenacity>=8.2.3
662
  certifi>=2024.6.2
663
  mysql-connector-python
664
-
665
  ```
666
 
667
  ---
@@ -671,4 +676,5 @@ mysql-connector-python
671
  The following files were not included in the text content:
672
 
673
  - `Dockerfile`
 
674
 
 
3
  ## πŸ“Š Project Information
4
 
5
  - **Project Name**: `agentic-finlit-backend`
6
+ - **Generated On**: 2025-09-08 06:00:37 (Etc/GMT+5 / GMT-05:00)
7
+ - **Total Files Processed**: 14
8
  - **Export Tool**: Easy Whole Project to Single Text File for LLMs v1.1.0
9
  - **Tool Author**: Jota / JosΓ© Guilherme Pandolfi
10
 
 
23
  β”œβ”€β”€ πŸ“ app/
24
  β”‚ β”œβ”€β”€ πŸ“ agents/
25
  β”‚ β”‚ β”œβ”€β”€ πŸ“„ graph.py (587 B)
26
+ β”‚ β”‚ β”œβ”€β”€ πŸ“„ nodes.py (3.82 KB)
27
  β”‚ β”‚ └── πŸ“„ schemas.py (635 B)
28
  β”‚ β”œβ”€β”€ πŸ“„ db.py (2.42 KB)
29
  β”‚ β”œβ”€β”€ πŸ“„ embeddings.py (578 B)
30
  β”‚ β”œβ”€β”€ πŸ“„ hf_client.py (878 B)
31
  β”‚ β”œβ”€β”€ πŸ“„ ingest.py (845 B)
32
+ β”‚ β”œβ”€β”€ πŸ“„ main.py (2.65 KB)
33
  β”‚ └── πŸ“„ README.md (107 B)
34
  β”œβ”€β”€ πŸ“„ Dockerfile (632 B)
35
  β”œβ”€β”€ πŸ“„ elkay.txt
36
+ β”œβ”€β”€ πŸ“„ git
37
  β”œβ”€β”€ πŸ“„ README.md (275 B)
38
+ └── πŸ“„ requirements.txt (345 B)
39
  ```
40
 
41
  ## πŸ“‘ Table of Contents
 
61
 
62
  | Metric | Count |
63
  |--------|-------|
64
+ | Total Files | 14 |
65
  | Total Directories | 2 |
66
  | Text Files | 12 |
67
+ | Binary Files | 2 |
68
+ | Total Size | 13.66 KB |
69
 
70
  ### πŸ“„ File Types Distribution
71
 
 
73
  |-----------|-------|
74
  | `.py` | 8 |
75
  | `.md` | 2 |
76
+ | `no extension` | 2 |
77
  | `.txt` | 2 |
 
78
 
79
  ## πŸ’» File Code Contents
80
 
 
120
  ### <a id="πŸ“„-app-agents-nodes-py"></a>πŸ“„ `app/agents/nodes.py`
121
 
122
  **File Info:**
123
+ - **Size**: 3.82 KB
124
  - **Extension**: `.py`
125
  - **Language**: `python`
126
  - **Location**: `app/agents/nodes.py`
127
  - **Relative Path**: `app/agents`
128
  - **Created**: 2025-09-08 04:12:41 (Etc/GMT+5 / GMT-05:00)
129
+ - **Modified**: 2025-09-08 05:55:38 (Etc/GMT+5 / GMT-05:00)
130
+ - **MD5**: `98caf6bbfebb550a4a3b9526a8446c4c`
131
+ - **SHA256**: `6fea8331aaef9cef25922369670e2b50c38df7050ad1287c0b865f688a9e4223`
132
  - **Encoding**: ASCII
133
 
134
  **File code content:**
 
138
  from ..db import search_chunks, record_submission
139
  from ..embeddings import embed_query
140
  from ..hf_client import chat
141
+ import json
142
 
143
  def node_rag(state: AgentState) -> AgentState:
144
+ # Vector search for relevant chunks
145
+ qvec = embed_query(f"Key financial literacy points for Jamaican primary student level {state.level_slug} in lesson {state.lesson_id}.")
146
  hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
147
  state.context_chunks = [h["content"] for h in hits]
148
+ # LLM summarize
149
+ sys = {"role": "system", "content": "You are a friendly Jamaican primary school tutor. Keep explanations simple, use JA$ examples, and make it fun for kids aged 7-12."}
150
+ usr = {"role": "user", "content": f"Summarize these notes in 5 short, kid-friendly bullet points for {state.level_slug} level:\n" + "\n\n".join(state.context_chunks)}
151
  state.context_summary = chat([sys, usr], max_new_tokens=180)
152
  return state
153
 
154
  def node_quiz(state: AgentState) -> AgentState:
155
+ sys = {"role": "system", "content": "You are a Jamaican primary school teacher. Create 4-7 simple multiple-choice questions (A-D options) based on the notes. Use JA$ for money examples. Output JSON only."}
156
+ usr = {"role": "user", "content": f"Create {4 + len(state.context_chunks) % 3} MCQs for level {state.level_slug}. JSON: {{\"items\":[{{\"question\":\"...\",\"options\":[\"A. ...\",\"B. ...\",\"C. ...\",\"D. ...\"],\"answer_key\":\"A\"}}... ]}}\nNotes:\n" + "\n\n".join(state.context_chunks)}
157
  raw = chat([sys, usr], max_new_tokens=350)
158
+ try:
159
+ data = json.loads(raw)
160
+ items = data.get("items", [])[:7]
161
+ except:
162
+ items = [] # Fallback
163
+ # Normalize items
164
  fixed = []
165
  for it in items:
166
+ q = it.get("question", "").strip()
167
+ opts = it.get("options", ["A. Unknown", "B. Unknown", "C. Unknown", "D. Unknown"])[:4]
168
+ key = str(it.get("answer_key", "A")).strip()[:1].upper()
169
+ if key not in "ABCD": key = "A"
170
+ if q: fixed.append({"question": q, "options": opts, "answer_key": key, "points": 1})
 
171
  state.quiz_items = fixed
172
  return state
173
 
174
  def node_grade(state: AgentState) -> AgentState:
 
175
  answers = state.student_answers
176
+ score, total = 0, len(state.quiz_items)
177
+ details = []
178
+ for i, (it, ans) in enumerate(zip(state.quiz_items, answers)):
179
+ correct = (ans.upper() == it["answer_key"])
180
  if correct: score += 1
181
+ details.append({"qno": i+1, "user": ans, "key": it["answer_key"], "correct": correct})
182
+ state.score = score
183
+ state.total = total
184
+ state.details = details
185
+ # Record in TiDB
186
+ record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id or 0,
187
+ score=score, total=total, details=details)
188
+ # Update XP in users table (simple: +10 per correct)
189
+ with get_conn() as conn:
190
+ with conn.cursor() as cur:
191
+ cur.execute("UPDATE users SET total_xp = total_xp + %s WHERE user_id = %s", (score * 10, state.student_id))
192
+ conn.commit()
193
  return state
194
 
195
  def node_coach_or_celebrate(state: AgentState) -> AgentState:
196
+ if state.score == state.total and state.total > 0:
197
  state.route = "celebrate"
198
  return state
199
+ # Coach on wrongs
200
+ wrong = [it for it, ans in zip(state.quiz_items, state.student_answers) if ans.upper() != it["answer_key"]]
201
+ sys = {"role": "system", "content": "You are a kind Jamaican tutor. Explain mistakes simply and encouragingly for primary kids. Use JA$ examples."}
202
+ usr = {"role": "user", "content": f"Explain these wrong answers for level {state.level_slug}:\n" + "\n".join([f"Q: {w['question']}\nYour: {w['user_ans']}\nCorrect: {w['key']}" for w in wrong]) + "\nKeep it short and positive."}
203
+ state.feedback = chat([sys, usr], max_new_tokens=220)
 
 
 
 
204
  state.route = "coach"
205
  return state
 
206
  ```
207
 
208
  ---
 
460
  ### <a id="πŸ“„-app-main-py"></a>πŸ“„ `app/main.py`
461
 
462
  **File Info:**
463
+ - **Size**: 2.65 KB
464
  - **Extension**: `.py`
465
  - **Language**: `python`
466
  - **Location**: `app/main.py`
467
  - **Relative Path**: `app`
468
  - **Created**: 2025-09-08 03:31:10 (Etc/GMT+5 / GMT-05:00)
469
+ - **Modified**: 2025-09-08 05:55:14 (Etc/GMT+5 / GMT-05:00)
470
+ - **MD5**: `4e5e0615148bb8700abc8b8c85bf5176`
471
+ - **SHA256**: `8b7fc35e1728bc98d5792883a9c696830da1e39fac84af3c0410a4d036ee193f`
472
  - **Encoding**: ASCII
473
 
474
  **File code content:**
475
 
476
  ```python
477
+ from fastapi import FastAPI
478
  from pydantic import BaseModel
479
  from .agents.schemas import AgentState
480
  from .agents.graph import build_graph
481
  from .ingest import ingest_lesson
482
+ from .db import get_conn # For startup checks
 
 
 
 
 
 
 
 
483
 
484
  api = FastAPI(title="Agentic FinLit Backend")
485
  _graph = build_graph()
 
496
  answers: list[str]
497
  assignment_id: int | None = None
498
 
499
+ @api.on_event("startup")
500
+ async def startup_event():
501
+ # Ingest sample lesson on startup (from frontend's topic_1.txt; adjust path)
502
+ try:
503
+ with open("../../phase/Student_view/lessons/lesson_1/topic_1.txt", "r") as f:
504
+ raw_text = f.read()
505
+ ingest_lesson(1, "beginner", raw_text) # Sample: Lesson 1, Beginner
506
+ print("Sample lesson ingested.")
507
+ except Exception as e:
508
+ print(f"Ingestion failed: {e}")
509
+
510
  @api.post("/ingest")
511
+ def ingest_endpoint(lesson_id: int, level_slug: str, raw_text: str):
512
  ingest_lesson(lesson_id, level_slug, raw_text)
513
+ return {"ok": True, "message": f"Ingested lesson {lesson_id} for level {level_slug}"}
514
 
515
  @api.post("/agent/start")
516
  def agent_start(payload: StartRun):
517
  state = AgentState(**payload.model_dump())
518
+ out = _graph.invoke(state)
519
  return {"summary": out.context_summary, "ok": True}
520
 
521
  @api.post("/agent/quiz")
522
  def agent_quiz(payload: StartRun):
523
  state = AgentState(**payload.model_dump())
524
+ out = _graph.invoke(state, {"quiz": {}}) # Partial invoke for quiz gen
525
  return {"items": out.quiz_items}
526
 
527
  @api.post("/agent/grade")
528
  def agent_grade(payload: SubmitQuiz):
529
+ st = AgentState(**{k: v for k, v in payload.model_dump().items() if k != 'answers'})
 
530
  st.student_answers = payload.answers
531
+ out = _graph.invoke(st, {"grade": {}})
532
+ return {"score": out.score, "total": out.total, "details": out.details if hasattr(out, 'details') else {}}
533
 
534
  @api.post("/agent/coach_or_celebrate")
535
  def agent_next(payload: SubmitQuiz):
536
+ st = AgentState(**{k: v for k, v in payload.model_dump().items() if k != 'answers'})
 
537
  st.student_answers = payload.answers
538
+ out = _graph.invoke(st, {"coach_or_celebrate": {}})
539
  data = {"route": out.route}
540
  if out.route == "coach":
541
  data["feedback"] = out.feedback
542
+ elif out.route == "celebrate":
543
  data["message"] = "Great job! Want to play Money Match?"
544
+ data["game_slug"] = "money_match" # Links to game in frontend
545
  return data
546
 
547
+ # Health check for frontend
548
+ @api.get("/")
549
+ def health():
550
+ return {"status": "Backend running", "agentic_flow": "ready"}
551
  ```
552
 
553
  ---
 
635
  ### <a id="πŸ“„-requirements-txt"></a>πŸ“„ `requirements.txt`
636
 
637
  **File Info:**
638
+ - **Size**: 345 B
639
  - **Extension**: `.txt`
640
  - **Language**: `text`
641
  - **Location**: `requirements.txt`
642
  - **Relative Path**: `root`
643
  - **Created**: 2025-09-08 03:23:55 (Etc/GMT+5 / GMT-05:00)
644
+ - **Modified**: 2025-09-08 06:00:37 (Etc/GMT+5 / GMT-05:00)
645
+ - **MD5**: `fa57386573787bc9628b5527758d3a3a`
646
+ - **SHA256**: `845818e5fa794614226cd9b08ad209c359843b0f485f8f3c5a6ac24c26be0609`
647
  - **Encoding**: ASCII
648
 
649
  **File code content:**
 
666
  tenacity>=8.2.3
667
  certifi>=2024.6.2
668
  mysql-connector-python
669
+ bcrypt==4.0.1
670
  ```
671
 
672
  ---
 
676
  The following files were not included in the text content:
677
 
678
  - `Dockerfile`
679
+ - `git`
680
 
agentic-finlit-backend-output.txt CHANGED
@@ -5,8 +5,8 @@ PROJECT EXPORT FOR LLMs
5
  PROJECT INFORMATION:
6
  --------------------------------------------------
7
  Project Name: agentic-finlit-backend
8
- Generated On: 2025-09-08 05:44:11 (Etc/GMT+5 / GMT-05:00)
9
- Total Files Processed: 13
10
  Export Tool: Easy Whole Project to Single Text File for LLMs v1.1.0
11
  Tool Author: Jota / JosΓ© Guilherme Pandolfi
12
 
@@ -25,34 +25,35 @@ PROJECT STRUCTURE
25
  β”œβ”€β”€ πŸ“ app/
26
  β”‚ β”œβ”€β”€ πŸ“ agents/
27
  β”‚ β”‚ β”œβ”€β”€ πŸ“„ graph.py (587 B)
28
- β”‚ β”‚ β”œβ”€β”€ πŸ“„ nodes.py (3.37 KB)
29
  β”‚ β”‚ └── πŸ“„ schemas.py (635 B)
30
  β”‚ β”œβ”€β”€ πŸ“„ db.py (2.42 KB)
31
  β”‚ β”œβ”€β”€ πŸ“„ embeddings.py (578 B)
32
  β”‚ β”œβ”€β”€ πŸ“„ hf_client.py (878 B)
33
  β”‚ β”œβ”€β”€ πŸ“„ ingest.py (845 B)
34
- β”‚ β”œβ”€β”€ πŸ“„ main.py (2.25 KB)
35
  β”‚ └── πŸ“„ README.md (107 B)
36
  β”œβ”€β”€ πŸ“„ Dockerfile (632 B)
37
  β”œβ”€β”€ πŸ“„ elkay.txt
 
38
  β”œβ”€β”€ πŸ“„ README.md (275 B)
39
- └── πŸ“„ requirements.txt (332 B)
40
 
41
  ================================================================================
42
  PROJECT STATISTICS
43
  ================================================================================
44
- Total Files: 13
45
  Total Directories: 2
46
  Text Files: 12
47
- Binary Files: 1
48
- Total Size: 12.8 KB
49
 
50
  FILE TYPES DISTRIBUTION:
51
  ------------------------------
52
  .py : 8
53
  .md : 2
 
54
  .txt : 2
55
- no extension : 1
56
 
57
  ================================================================================
58
  FILE CODE CONTENTS
@@ -105,15 +106,15 @@ FILE: app/agents/nodes.py
105
 
106
  FILE INFORMATION:
107
  ----------------------------------------
108
- Size: 3.37 KB
109
  Extension: .py
110
  Language: python
111
  Location: app/agents/nodes.py
112
  Relative Path: app/agents
113
  Created: 2025-09-08 04:12:41 (Etc/GMT+5 / GMT-05:00)
114
- Modified: 2025-09-08 04:17:35 (Etc/GMT+5 / GMT-05:00)
115
- MD5: 01bf9b0ed40637415e3d43eb3c018d43
116
- SHA256: c8f802e7ee48fe46e70bceef868091fd83123225c166ce72050b7aa3ea1f4f3f
117
  Encoding: ASCII
118
 
119
  FILE CONTENT:
@@ -122,70 +123,71 @@ from .schemas import AgentState
122
  from ..db import search_chunks, record_submission
123
  from ..embeddings import embed_query
124
  from ..hf_client import chat
 
125
 
126
  def node_rag(state: AgentState) -> AgentState:
127
- qvec = embed_query("Key points of this lesson for a Jamaican primary student.")
 
128
  hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
129
  state.context_chunks = [h["content"] for h in hits]
130
- # Summarize context for the student
131
- sys = {"role":"system","content":"You are a Jamaican primary school tutor. Keep it simple and friendly."}
132
- usr = {"role":"user","content": "Summarize these notes in 5 short bullet points:\n" + "\n\n".join(state.context_chunks)}
133
  state.context_summary = chat([sys, usr], max_new_tokens=180)
134
  return state
135
 
136
  def node_quiz(state: AgentState) -> AgentState:
137
- sys = {"role":"system","content":"You are a Jamaican primary school teacher. Make kid-friendly multiple-choice questions (A-D) from the provided notes."}
138
- usr = {"role":"user","content": "Create 5 MCQs. JSON only: {\"items\":[{\"question\":\"...\",\"options\":[\"A\",\"B\",\"C\",\"D\"],\"answer_key\":\"A\"}...]}\nNotes:\n" + "\n\n".join(state.context_chunks)}
139
  raw = chat([sys, usr], max_new_tokens=350)
140
- import json
141
- data = {}
142
- try: data = json.loads(raw)
143
- except: data = {"items":[]}
144
- items = data.get("items", [])[:7] # 4–7 in your spec
145
- # Normalize
146
  fixed = []
147
  for it in items:
148
- q = it.get("question","").strip()
149
- opts = it.get("options", [])[:4]
150
- while len(opts)<4: opts.append("Option")
151
- key = str(it.get("answer_key","A")).strip()[:1].upper()
152
- if key not in "ABCD": key="A"
153
- if q: fixed.append({"question":q, "options":opts, "answer_key":key, "points":1})
154
  state.quiz_items = fixed
155
  return state
156
 
157
  def node_grade(state: AgentState) -> AgentState:
158
- # simple grading
159
  answers = state.student_answers
160
- score, total, details = 0, len(state.quiz_items), []
161
- for i,(it,ans) in enumerate(zip(state.quiz_items, answers)):
162
- correct = (ans.upper()==it["answer_key"])
 
163
  if correct: score += 1
164
- details.append({"qno":i+1,"user":ans,"key":it["answer_key"],"correct":correct})
165
- state.score, state.total = score, total
166
- # record
167
- if state.assignment_id is None:
168
- state.assignment_id = 0 # if not assigned formally
169
- record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id,
170
- score=score, total=total, details={"items":details})
 
 
 
 
 
171
  return state
172
 
173
  def node_coach_or_celebrate(state: AgentState) -> AgentState:
174
- if state.score == state.total and state.total is not None and state.total>0:
175
  state.route = "celebrate"
176
  return state
177
- # Coach for wrong answers
178
- wrong = []
179
- for it,ans in zip(state.quiz_items, state.student_answers):
180
- if ans.upper()!=it["answer_key"]:
181
- wrong.append({"q":it["question"],"your":ans,"key":it["answer_key"]})
182
- sys = {"role":"system","content":"You are a kind tutor. Explain simply where the student went wrong."}
183
- usr = {"role":"user","content": "Here are the mistakes:\n" + "\n".join([f"Q: {w['q']}\nYour answer: {w['your']}\nCorrect: {w['key']}" for w in wrong]) +
184
- "\nGive short, friendly explanations for each."}
185
- state.feedback = chat([sys,usr], max_new_tokens=220)
186
  state.route = "coach"
187
  return state
188
-
189
  ================================================================================
190
 
191
 
@@ -450,32 +452,25 @@ FILE: app/main.py
450
 
451
  FILE INFORMATION:
452
  ----------------------------------------
453
- Size: 2.25 KB
454
  Extension: .py
455
  Language: python
456
  Location: app/main.py
457
  Relative Path: app
458
  Created: 2025-09-08 03:31:10 (Etc/GMT+5 / GMT-05:00)
459
- Modified: 2025-09-08 05:33:58 (Etc/GMT+5 / GMT-05:00)
460
- MD5: 8303a8d4c5e8e664e4ff8962623fec51
461
- SHA256: c8cc9a31cf9c6ff78196de5648108fd37bad8c36995ab41310b46c90b68bb238
462
  Encoding: ASCII
463
 
464
  FILE CONTENT:
465
  ----------------------------------------
 
466
  from pydantic import BaseModel
467
  from .agents.schemas import AgentState
468
  from .agents.graph import build_graph
469
  from .ingest import ingest_lesson
470
-
471
-
472
- from fastapi import FastAPI
473
-
474
- app = FastAPI()
475
-
476
- @app.get("/health")
477
- def health():
478
- return {"ok": True, "service": "agentic-finlit-backend"}
479
 
480
  api = FastAPI(title="Agentic FinLit Backend")
481
  _graph = build_graph()
@@ -492,48 +487,58 @@ class SubmitQuiz(BaseModel):
492
  answers: list[str]
493
  assignment_id: int | None = None
494
 
 
 
 
 
 
 
 
 
 
 
 
495
  @api.post("/ingest")
496
- def ingest(lesson_id:int, level_slug:str, raw_text:str):
497
  ingest_lesson(lesson_id, level_slug, raw_text)
498
- return {"ok": True}
499
 
500
  @api.post("/agent/start")
501
  def agent_start(payload: StartRun):
502
  state = AgentState(**payload.model_dump())
503
- out = _graph.invoke(state, start_at="rag")
504
  return {"summary": out.context_summary, "ok": True}
505
 
506
  @api.post("/agent/quiz")
507
  def agent_quiz(payload: StartRun):
508
  state = AgentState(**payload.model_dump())
509
- out = _graph.invoke(state, start_at="quiz")
510
  return {"items": out.quiz_items}
511
 
512
  @api.post("/agent/grade")
513
  def agent_grade(payload: SubmitQuiz):
514
- st = AgentState(student_id=payload.student_id, lesson_id=payload.lesson_id,
515
- level_slug=payload.level_slug, assignment_id=payload.assignment_id)
516
  st.student_answers = payload.answers
517
- out = _graph.invoke(st, start_at="grade")
518
- return {"score": out.score, "total": out.total}
519
 
520
  @api.post("/agent/coach_or_celebrate")
521
  def agent_next(payload: SubmitQuiz):
522
- st = AgentState(student_id=payload.student_id, lesson_id=payload.lesson_id,
523
- level_slug=payload.level_slug, assignment_id=payload.assignment_id)
524
  st.student_answers = payload.answers
525
- out = _graph.invoke(st, start_at="coach_or_celebrate")
526
  data = {"route": out.route}
527
  if out.route == "coach":
528
  data["feedback"] = out.feedback
529
- else:
530
  data["message"] = "Great job! Want to play Money Match?"
531
- data["game_slug"] = "money_match"
532
  return data
533
 
534
- if __name__ == "__main__":
535
- import os, uvicorn
536
- uvicorn.run("app.main:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
 
537
  ================================================================================
538
 
539
 
@@ -626,15 +631,15 @@ FILE: requirements.txt
626
 
627
  FILE INFORMATION:
628
  ----------------------------------------
629
- Size: 332 B
630
  Extension: .txt
631
  Language: text
632
  Location: requirements.txt
633
  Relative Path: root
634
  Created: 2025-09-08 03:23:55 (Etc/GMT+5 / GMT-05:00)
635
- Modified: 2025-09-08 05:23:44 (Etc/GMT+5 / GMT-05:00)
636
- MD5: 011711e3a3a8373749286cd4918b9079
637
- SHA256: 6d05e356cd5b401fee707c7613b7b1b0623ef3d160ae0d54fdd43b1442bb69e8
638
  Encoding: ASCII
639
 
640
  FILE CONTENT:
@@ -656,7 +661,7 @@ python-dotenv==1.0.1
656
  tenacity>=8.2.3
657
  certifi>=2024.6.2
658
  mysql-connector-python
659
-
660
  ================================================================================
661
 
662
 
@@ -664,3 +669,4 @@ mysql-connector-python
664
  BINARY/EXCLUDED FILES (not included in text content)
665
  ================================================================================
666
  - Dockerfile
 
 
5
  PROJECT INFORMATION:
6
  --------------------------------------------------
7
  Project Name: agentic-finlit-backend
8
+ Generated On: 2025-09-08 06:00:37 (Etc/GMT+5 / GMT-05:00)
9
+ Total Files Processed: 14
10
  Export Tool: Easy Whole Project to Single Text File for LLMs v1.1.0
11
  Tool Author: Jota / JosΓ© Guilherme Pandolfi
12
 
 
25
  β”œβ”€β”€ πŸ“ app/
26
  β”‚ β”œβ”€β”€ πŸ“ agents/
27
  β”‚ β”‚ β”œβ”€β”€ πŸ“„ graph.py (587 B)
28
+ β”‚ β”‚ β”œβ”€β”€ πŸ“„ nodes.py (3.82 KB)
29
  β”‚ β”‚ └── πŸ“„ schemas.py (635 B)
30
  β”‚ β”œβ”€β”€ πŸ“„ db.py (2.42 KB)
31
  β”‚ β”œβ”€β”€ πŸ“„ embeddings.py (578 B)
32
  β”‚ β”œβ”€β”€ πŸ“„ hf_client.py (878 B)
33
  β”‚ β”œβ”€β”€ πŸ“„ ingest.py (845 B)
34
+ β”‚ β”œβ”€β”€ πŸ“„ main.py (2.65 KB)
35
  β”‚ └── πŸ“„ README.md (107 B)
36
  β”œβ”€β”€ πŸ“„ Dockerfile (632 B)
37
  β”œβ”€β”€ πŸ“„ elkay.txt
38
+ β”œβ”€β”€ πŸ“„ git
39
  β”œβ”€β”€ πŸ“„ README.md (275 B)
40
+ └── πŸ“„ requirements.txt (345 B)
41
 
42
  ================================================================================
43
  PROJECT STATISTICS
44
  ================================================================================
45
+ Total Files: 14
46
  Total Directories: 2
47
  Text Files: 12
48
+ Binary Files: 2
49
+ Total Size: 13.66 KB
50
 
51
  FILE TYPES DISTRIBUTION:
52
  ------------------------------
53
  .py : 8
54
  .md : 2
55
+ no extension : 2
56
  .txt : 2
 
57
 
58
  ================================================================================
59
  FILE CODE CONTENTS
 
106
 
107
  FILE INFORMATION:
108
  ----------------------------------------
109
+ Size: 3.82 KB
110
  Extension: .py
111
  Language: python
112
  Location: app/agents/nodes.py
113
  Relative Path: app/agents
114
  Created: 2025-09-08 04:12:41 (Etc/GMT+5 / GMT-05:00)
115
+ Modified: 2025-09-08 05:55:38 (Etc/GMT+5 / GMT-05:00)
116
+ MD5: 98caf6bbfebb550a4a3b9526a8446c4c
117
+ SHA256: 6fea8331aaef9cef25922369670e2b50c38df7050ad1287c0b865f688a9e4223
118
  Encoding: ASCII
119
 
120
  FILE CONTENT:
 
123
  from ..db import search_chunks, record_submission
124
  from ..embeddings import embed_query
125
  from ..hf_client import chat
126
+ import json
127
 
128
  def node_rag(state: AgentState) -> AgentState:
129
+ # Vector search for relevant chunks
130
+ qvec = embed_query(f"Key financial literacy points for Jamaican primary student level {state.level_slug} in lesson {state.lesson_id}.")
131
  hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
132
  state.context_chunks = [h["content"] for h in hits]
133
+ # LLM summarize
134
+ sys = {"role": "system", "content": "You are a friendly Jamaican primary school tutor. Keep explanations simple, use JA$ examples, and make it fun for kids aged 7-12."}
135
+ usr = {"role": "user", "content": f"Summarize these notes in 5 short, kid-friendly bullet points for {state.level_slug} level:\n" + "\n\n".join(state.context_chunks)}
136
  state.context_summary = chat([sys, usr], max_new_tokens=180)
137
  return state
138
 
139
  def node_quiz(state: AgentState) -> AgentState:
140
+ sys = {"role": "system", "content": "You are a Jamaican primary school teacher. Create 4-7 simple multiple-choice questions (A-D options) based on the notes. Use JA$ for money examples. Output JSON only."}
141
+ usr = {"role": "user", "content": f"Create {4 + len(state.context_chunks) % 3} MCQs for level {state.level_slug}. JSON: {{\"items\":[{{\"question\":\"...\",\"options\":[\"A. ...\",\"B. ...\",\"C. ...\",\"D. ...\"],\"answer_key\":\"A\"}}... ]}}\nNotes:\n" + "\n\n".join(state.context_chunks)}
142
  raw = chat([sys, usr], max_new_tokens=350)
143
+ try:
144
+ data = json.loads(raw)
145
+ items = data.get("items", [])[:7]
146
+ except:
147
+ items = [] # Fallback
148
+ # Normalize items
149
  fixed = []
150
  for it in items:
151
+ q = it.get("question", "").strip()
152
+ opts = it.get("options", ["A. Unknown", "B. Unknown", "C. Unknown", "D. Unknown"])[:4]
153
+ key = str(it.get("answer_key", "A")).strip()[:1].upper()
154
+ if key not in "ABCD": key = "A"
155
+ if q: fixed.append({"question": q, "options": opts, "answer_key": key, "points": 1})
 
156
  state.quiz_items = fixed
157
  return state
158
 
159
  def node_grade(state: AgentState) -> AgentState:
 
160
  answers = state.student_answers
161
+ score, total = 0, len(state.quiz_items)
162
+ details = []
163
+ for i, (it, ans) in enumerate(zip(state.quiz_items, answers)):
164
+ correct = (ans.upper() == it["answer_key"])
165
  if correct: score += 1
166
+ details.append({"qno": i+1, "user": ans, "key": it["answer_key"], "correct": correct})
167
+ state.score = score
168
+ state.total = total
169
+ state.details = details
170
+ # Record in TiDB
171
+ record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id or 0,
172
+ score=score, total=total, details=details)
173
+ # Update XP in users table (simple: +10 per correct)
174
+ with get_conn() as conn:
175
+ with conn.cursor() as cur:
176
+ cur.execute("UPDATE users SET total_xp = total_xp + %s WHERE user_id = %s", (score * 10, state.student_id))
177
+ conn.commit()
178
  return state
179
 
180
  def node_coach_or_celebrate(state: AgentState) -> AgentState:
181
+ if state.score == state.total and state.total > 0:
182
  state.route = "celebrate"
183
  return state
184
+ # Coach on wrongs
185
+ wrong = [it for it, ans in zip(state.quiz_items, state.student_answers) if ans.upper() != it["answer_key"]]
186
+ sys = {"role": "system", "content": "You are a kind Jamaican tutor. Explain mistakes simply and encouragingly for primary kids. Use JA$ examples."}
187
+ usr = {"role": "user", "content": f"Explain these wrong answers for level {state.level_slug}:\n" + "\n".join([f"Q: {w['question']}\nYour: {w['user_ans']}\nCorrect: {w['key']}" for w in wrong]) + "\nKeep it short and positive."}
188
+ state.feedback = chat([sys, usr], max_new_tokens=220)
 
 
 
 
189
  state.route = "coach"
190
  return state
 
191
  ================================================================================
192
 
193
 
 
452
 
453
  FILE INFORMATION:
454
  ----------------------------------------
455
+ Size: 2.65 KB
456
  Extension: .py
457
  Language: python
458
  Location: app/main.py
459
  Relative Path: app
460
  Created: 2025-09-08 03:31:10 (Etc/GMT+5 / GMT-05:00)
461
+ Modified: 2025-09-08 05:55:14 (Etc/GMT+5 / GMT-05:00)
462
+ MD5: 4e5e0615148bb8700abc8b8c85bf5176
463
+ SHA256: 8b7fc35e1728bc98d5792883a9c696830da1e39fac84af3c0410a4d036ee193f
464
  Encoding: ASCII
465
 
466
  FILE CONTENT:
467
  ----------------------------------------
468
+ from fastapi import FastAPI
469
  from pydantic import BaseModel
470
  from .agents.schemas import AgentState
471
  from .agents.graph import build_graph
472
  from .ingest import ingest_lesson
473
+ from .db import get_conn # For startup checks
 
 
 
 
 
 
 
 
474
 
475
  api = FastAPI(title="Agentic FinLit Backend")
476
  _graph = build_graph()
 
487
  answers: list[str]
488
  assignment_id: int | None = None
489
 
490
+ @api.on_event("startup")
491
+ async def startup_event():
492
+ # Ingest sample lesson on startup (from frontend's topic_1.txt; adjust path)
493
+ try:
494
+ with open("../../phase/Student_view/lessons/lesson_1/topic_1.txt", "r") as f:
495
+ raw_text = f.read()
496
+ ingest_lesson(1, "beginner", raw_text) # Sample: Lesson 1, Beginner
497
+ print("Sample lesson ingested.")
498
+ except Exception as e:
499
+ print(f"Ingestion failed: {e}")
500
+
501
  @api.post("/ingest")
502
+ def ingest_endpoint(lesson_id: int, level_slug: str, raw_text: str):
503
  ingest_lesson(lesson_id, level_slug, raw_text)
504
+ return {"ok": True, "message": f"Ingested lesson {lesson_id} for level {level_slug}"}
505
 
506
  @api.post("/agent/start")
507
  def agent_start(payload: StartRun):
508
  state = AgentState(**payload.model_dump())
509
+ out = _graph.invoke(state)
510
  return {"summary": out.context_summary, "ok": True}
511
 
512
  @api.post("/agent/quiz")
513
  def agent_quiz(payload: StartRun):
514
  state = AgentState(**payload.model_dump())
515
+ out = _graph.invoke(state, {"quiz": {}}) # Partial invoke for quiz gen
516
  return {"items": out.quiz_items}
517
 
518
  @api.post("/agent/grade")
519
  def agent_grade(payload: SubmitQuiz):
520
+ st = AgentState(**{k: v for k, v in payload.model_dump().items() if k != 'answers'})
 
521
  st.student_answers = payload.answers
522
+ out = _graph.invoke(st, {"grade": {}})
523
+ return {"score": out.score, "total": out.total, "details": out.details if hasattr(out, 'details') else {}}
524
 
525
  @api.post("/agent/coach_or_celebrate")
526
  def agent_next(payload: SubmitQuiz):
527
+ st = AgentState(**{k: v for k, v in payload.model_dump().items() if k != 'answers'})
 
528
  st.student_answers = payload.answers
529
+ out = _graph.invoke(st, {"coach_or_celebrate": {}})
530
  data = {"route": out.route}
531
  if out.route == "coach":
532
  data["feedback"] = out.feedback
533
+ elif out.route == "celebrate":
534
  data["message"] = "Great job! Want to play Money Match?"
535
+ data["game_slug"] = "money_match" # Links to game in frontend
536
  return data
537
 
538
+ # Health check for frontend
539
+ @api.get("/")
540
+ def health():
541
+ return {"status": "Backend running", "agentic_flow": "ready"}
542
  ================================================================================
543
 
544
 
 
631
 
632
  FILE INFORMATION:
633
  ----------------------------------------
634
+ Size: 345 B
635
  Extension: .txt
636
  Language: text
637
  Location: requirements.txt
638
  Relative Path: root
639
  Created: 2025-09-08 03:23:55 (Etc/GMT+5 / GMT-05:00)
640
+ Modified: 2025-09-08 06:00:37 (Etc/GMT+5 / GMT-05:00)
641
+ MD5: fa57386573787bc9628b5527758d3a3a
642
+ SHA256: 845818e5fa794614226cd9b08ad209c359843b0f485f8f3c5a6ac24c26be0609
643
  Encoding: ASCII
644
 
645
  FILE CONTENT:
 
661
  tenacity>=8.2.3
662
  certifi>=2024.6.2
663
  mysql-connector-python
664
+ bcrypt==4.0.1
665
  ================================================================================
666
 
667
 
 
669
  BINARY/EXCLUDED FILES (not included in text content)
670
  ================================================================================
671
  - Dockerfile
672
+ - git
app/agents/nodes.py CHANGED
@@ -2,66 +2,68 @@ from .schemas import AgentState
2
  from ..db import search_chunks, record_submission
3
  from ..embeddings import embed_query
4
  from ..hf_client import chat
 
5
 
6
  def node_rag(state: AgentState) -> AgentState:
7
- qvec = embed_query("Key points of this lesson for a Jamaican primary student.")
 
8
  hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
9
  state.context_chunks = [h["content"] for h in hits]
10
- # Summarize context for the student
11
- sys = {"role":"system","content":"You are a Jamaican primary school tutor. Keep it simple and friendly."}
12
- usr = {"role":"user","content": "Summarize these notes in 5 short bullet points:\n" + "\n\n".join(state.context_chunks)}
13
  state.context_summary = chat([sys, usr], max_new_tokens=180)
14
  return state
15
 
16
  def node_quiz(state: AgentState) -> AgentState:
17
- sys = {"role":"system","content":"You are a Jamaican primary school teacher. Make kid-friendly multiple-choice questions (A-D) from the provided notes."}
18
- usr = {"role":"user","content": "Create 5 MCQs. JSON only: {\"items\":[{\"question\":\"...\",\"options\":[\"A\",\"B\",\"C\",\"D\"],\"answer_key\":\"A\"}...]}\nNotes:\n" + "\n\n".join(state.context_chunks)}
19
  raw = chat([sys, usr], max_new_tokens=350)
20
- import json
21
- data = {}
22
- try: data = json.loads(raw)
23
- except: data = {"items":[]}
24
- items = data.get("items", [])[:7] # 4–7 in your spec
25
- # Normalize
26
  fixed = []
27
  for it in items:
28
- q = it.get("question","").strip()
29
- opts = it.get("options", [])[:4]
30
- while len(opts)<4: opts.append("Option")
31
- key = str(it.get("answer_key","A")).strip()[:1].upper()
32
- if key not in "ABCD": key="A"
33
- if q: fixed.append({"question":q, "options":opts, "answer_key":key, "points":1})
34
  state.quiz_items = fixed
35
  return state
36
 
37
  def node_grade(state: AgentState) -> AgentState:
38
- # simple grading
39
  answers = state.student_answers
40
- score, total, details = 0, len(state.quiz_items), []
41
- for i,(it,ans) in enumerate(zip(state.quiz_items, answers)):
42
- correct = (ans.upper()==it["answer_key"])
 
43
  if correct: score += 1
44
- details.append({"qno":i+1,"user":ans,"key":it["answer_key"],"correct":correct})
45
- state.score, state.total = score, total
46
- # record
47
- if state.assignment_id is None:
48
- state.assignment_id = 0 # if not assigned formally
49
- record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id,
50
- score=score, total=total, details={"items":details})
 
 
 
 
 
51
  return state
52
 
53
  def node_coach_or_celebrate(state: AgentState) -> AgentState:
54
- if state.score == state.total and state.total is not None and state.total>0:
55
  state.route = "celebrate"
56
  return state
57
- # Coach for wrong answers
58
- wrong = []
59
- for it,ans in zip(state.quiz_items, state.student_answers):
60
- if ans.upper()!=it["answer_key"]:
61
- wrong.append({"q":it["question"],"your":ans,"key":it["answer_key"]})
62
- sys = {"role":"system","content":"You are a kind tutor. Explain simply where the student went wrong."}
63
- usr = {"role":"user","content": "Here are the mistakes:\n" + "\n".join([f"Q: {w['q']}\nYour answer: {w['your']}\nCorrect: {w['key']}" for w in wrong]) +
64
- "\nGive short, friendly explanations for each."}
65
- state.feedback = chat([sys,usr], max_new_tokens=220)
66
  state.route = "coach"
67
- return state
 
2
  from ..db import search_chunks, record_submission
3
  from ..embeddings import embed_query
4
  from ..hf_client import chat
5
+ import json
6
 
7
  def node_rag(state: AgentState) -> AgentState:
8
+ # Vector search for relevant chunks
9
+ qvec = embed_query(f"Key financial literacy points for Jamaican primary student level {state.level_slug} in lesson {state.lesson_id}.")
10
  hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
11
  state.context_chunks = [h["content"] for h in hits]
12
+ # LLM summarize
13
+ sys = {"role": "system", "content": "You are a friendly Jamaican primary school tutor. Keep explanations simple, use JA$ examples, and make it fun for kids aged 7-12."}
14
+ usr = {"role": "user", "content": f"Summarize these notes in 5 short, kid-friendly bullet points for {state.level_slug} level:\n" + "\n\n".join(state.context_chunks)}
15
  state.context_summary = chat([sys, usr], max_new_tokens=180)
16
  return state
17
 
18
  def node_quiz(state: AgentState) -> AgentState:
19
+ sys = {"role": "system", "content": "You are a Jamaican primary school teacher. Create 4-7 simple multiple-choice questions (A-D options) based on the notes. Use JA$ for money examples. Output JSON only."}
20
+ usr = {"role": "user", "content": f"Create {4 + len(state.context_chunks) % 3} MCQs for level {state.level_slug}. JSON: {{\"items\":[{{\"question\":\"...\",\"options\":[\"A. ...\",\"B. ...\",\"C. ...\",\"D. ...\"],\"answer_key\":\"A\"}}... ]}}\nNotes:\n" + "\n\n".join(state.context_chunks)}
21
  raw = chat([sys, usr], max_new_tokens=350)
22
+ try:
23
+ data = json.loads(raw)
24
+ items = data.get("items", [])[:7]
25
+ except:
26
+ items = [] # Fallback
27
+ # Normalize items
28
  fixed = []
29
  for it in items:
30
+ q = it.get("question", "").strip()
31
+ opts = it.get("options", ["A. Unknown", "B. Unknown", "C. Unknown", "D. Unknown"])[:4]
32
+ key = str(it.get("answer_key", "A")).strip()[:1].upper()
33
+ if key not in "ABCD": key = "A"
34
+ if q: fixed.append({"question": q, "options": opts, "answer_key": key, "points": 1})
 
35
  state.quiz_items = fixed
36
  return state
37
 
38
  def node_grade(state: AgentState) -> AgentState:
 
39
  answers = state.student_answers
40
+ score, total = 0, len(state.quiz_items)
41
+ details = []
42
+ for i, (it, ans) in enumerate(zip(state.quiz_items, answers)):
43
+ correct = (ans.upper() == it["answer_key"])
44
  if correct: score += 1
45
+ details.append({"qno": i+1, "user": ans, "key": it["answer_key"], "correct": correct})
46
+ state.score = score
47
+ state.total = total
48
+ state.details = details
49
+ # Record in TiDB
50
+ record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id or 0,
51
+ score=score, total=total, details=details)
52
+ # Update XP in users table (simple: +10 per correct)
53
+ with get_conn() as conn:
54
+ with conn.cursor() as cur:
55
+ cur.execute("UPDATE users SET total_xp = total_xp + %s WHERE user_id = %s", (score * 10, state.student_id))
56
+ conn.commit()
57
  return state
58
 
59
  def node_coach_or_celebrate(state: AgentState) -> AgentState:
60
+ if state.score == state.total and state.total > 0:
61
  state.route = "celebrate"
62
  return state
63
+ # Coach on wrongs
64
+ wrong = [it for it, ans in zip(state.quiz_items, state.student_answers) if ans.upper() != it["answer_key"]]
65
+ sys = {"role": "system", "content": "You are a kind Jamaican tutor. Explain mistakes simply and encouragingly for primary kids. Use JA$ examples."}
66
+ usr = {"role": "user", "content": f"Explain these wrong answers for level {state.level_slug}:\n" + "\n".join([f"Q: {w['question']}\nYour: {w['user_ans']}\nCorrect: {w['key']}" for w in wrong]) + "\nKeep it short and positive."}
67
+ state.feedback = chat([sys, usr], max_new_tokens=220)
 
 
 
 
68
  state.route = "coach"
69
+ return state
app/main.py CHANGED
@@ -1,16 +1,9 @@
 
1
  from pydantic import BaseModel
2
  from .agents.schemas import AgentState
3
  from .agents.graph import build_graph
4
  from .ingest import ingest_lesson
5
-
6
-
7
- from fastapi import FastAPI
8
-
9
- app = FastAPI()
10
-
11
- @app.get("/health")
12
- def health():
13
- return {"ok": True, "service": "agentic-finlit-backend"}
14
 
15
  api = FastAPI(title="Agentic FinLit Backend")
16
  _graph = build_graph()
@@ -27,45 +20,55 @@ class SubmitQuiz(BaseModel):
27
  answers: list[str]
28
  assignment_id: int | None = None
29
 
 
 
 
 
 
 
 
 
 
 
 
30
  @api.post("/ingest")
31
- def ingest(lesson_id:int, level_slug:str, raw_text:str):
32
  ingest_lesson(lesson_id, level_slug, raw_text)
33
- return {"ok": True}
34
 
35
  @api.post("/agent/start")
36
  def agent_start(payload: StartRun):
37
  state = AgentState(**payload.model_dump())
38
- out = _graph.invoke(state, start_at="rag")
39
  return {"summary": out.context_summary, "ok": True}
40
 
41
  @api.post("/agent/quiz")
42
  def agent_quiz(payload: StartRun):
43
  state = AgentState(**payload.model_dump())
44
- out = _graph.invoke(state, start_at="quiz")
45
  return {"items": out.quiz_items}
46
 
47
  @api.post("/agent/grade")
48
  def agent_grade(payload: SubmitQuiz):
49
- st = AgentState(student_id=payload.student_id, lesson_id=payload.lesson_id,
50
- level_slug=payload.level_slug, assignment_id=payload.assignment_id)
51
  st.student_answers = payload.answers
52
- out = _graph.invoke(st, start_at="grade")
53
- return {"score": out.score, "total": out.total}
54
 
55
  @api.post("/agent/coach_or_celebrate")
56
  def agent_next(payload: SubmitQuiz):
57
- st = AgentState(student_id=payload.student_id, lesson_id=payload.lesson_id,
58
- level_slug=payload.level_slug, assignment_id=payload.assignment_id)
59
  st.student_answers = payload.answers
60
- out = _graph.invoke(st, start_at="coach_or_celebrate")
61
  data = {"route": out.route}
62
  if out.route == "coach":
63
  data["feedback"] = out.feedback
64
- else:
65
  data["message"] = "Great job! Want to play Money Match?"
66
- data["game_slug"] = "money_match"
67
  return data
68
 
69
- if __name__ == "__main__":
70
- import os, uvicorn
71
- uvicorn.run("app.main:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
 
 
1
+ from fastapi import FastAPI
2
  from pydantic import BaseModel
3
  from .agents.schemas import AgentState
4
  from .agents.graph import build_graph
5
  from .ingest import ingest_lesson
6
+ from .db import get_conn # For startup checks
 
 
 
 
 
 
 
 
7
 
8
  api = FastAPI(title="Agentic FinLit Backend")
9
  _graph = build_graph()
 
20
  answers: list[str]
21
  assignment_id: int | None = None
22
 
23
+ @api.on_event("startup")
24
+ async def startup_event():
25
+ # Ingest sample lesson on startup (from frontend's topic_1.txt; adjust path)
26
+ try:
27
+ with open("../../phase/Student_view/lessons/lesson_1/topic_1.txt", "r") as f:
28
+ raw_text = f.read()
29
+ ingest_lesson(1, "beginner", raw_text) # Sample: Lesson 1, Beginner
30
+ print("Sample lesson ingested.")
31
+ except Exception as e:
32
+ print(f"Ingestion failed: {e}")
33
+
34
  @api.post("/ingest")
35
+ def ingest_endpoint(lesson_id: int, level_slug: str, raw_text: str):
36
  ingest_lesson(lesson_id, level_slug, raw_text)
37
+ return {"ok": True, "message": f"Ingested lesson {lesson_id} for level {level_slug}"}
38
 
39
  @api.post("/agent/start")
40
  def agent_start(payload: StartRun):
41
  state = AgentState(**payload.model_dump())
42
+ out = _graph.invoke(state)
43
  return {"summary": out.context_summary, "ok": True}
44
 
45
  @api.post("/agent/quiz")
46
  def agent_quiz(payload: StartRun):
47
  state = AgentState(**payload.model_dump())
48
+ out = _graph.invoke(state, {"quiz": {}}) # Partial invoke for quiz gen
49
  return {"items": out.quiz_items}
50
 
51
  @api.post("/agent/grade")
52
  def agent_grade(payload: SubmitQuiz):
53
+ st = AgentState(**{k: v for k, v in payload.model_dump().items() if k != 'answers'})
 
54
  st.student_answers = payload.answers
55
+ out = _graph.invoke(st, {"grade": {}})
56
+ return {"score": out.score, "total": out.total, "details": out.details if hasattr(out, 'details') else {}}
57
 
58
  @api.post("/agent/coach_or_celebrate")
59
  def agent_next(payload: SubmitQuiz):
60
+ st = AgentState(**{k: v for k, v in payload.model_dump().items() if k != 'answers'})
 
61
  st.student_answers = payload.answers
62
+ out = _graph.invoke(st, {"coach_or_celebrate": {}})
63
  data = {"route": out.route}
64
  if out.route == "coach":
65
  data["feedback"] = out.feedback
66
+ elif out.route == "celebrate":
67
  data["message"] = "Great job! Want to play Money Match?"
68
+ data["game_slug"] = "money_match" # Links to game in frontend
69
  return data
70
 
71
+ # Health check for frontend
72
+ @api.get("/")
73
+ def health():
74
+ return {"status": "Backend running", "agentic_flow": "ready"}
git ADDED
File without changes
requirements.txt CHANGED
@@ -15,3 +15,4 @@ python-dotenv==1.0.1
15
  tenacity>=8.2.3
16
  certifi>=2024.6.2
17
  mysql-connector-python
 
 
15
  tenacity>=8.2.3
16
  certifi>=2024.6.2
17
  mysql-connector-python
18
+ bcrypt==4.0.1