Spaces:
Sleeping
Sleeping
feat: a
Browse files- agentic-finlit-backend-output.md +93 -87
- agentic-finlit-backend-output.txt +93 -87
- app/agents/nodes.py +42 -40
- app/main.py +28 -25
- git +0 -0
- requirements.txt +1 -0
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
|
| 7 |
-
- **Total Files Processed**:
|
| 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.
|
| 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.
|
| 33 |
β βββ π README.md (107 B)
|
| 34 |
βββ π Dockerfile (632 B)
|
| 35 |
βββ π elkay.txt
|
|
|
|
| 36 |
βββ π README.md (275 B)
|
| 37 |
-
βββ π requirements.txt (
|
| 38 |
```
|
| 39 |
|
| 40 |
## π Table of Contents
|
|
@@ -60,11 +61,11 @@
|
|
| 60 |
|
| 61 |
| Metric | Count |
|
| 62 |
|--------|-------|
|
| 63 |
-
| Total Files |
|
| 64 |
| Total Directories | 2 |
|
| 65 |
| Text Files | 12 |
|
| 66 |
-
| Binary Files |
|
| 67 |
-
| Total Size |
|
| 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.
|
| 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
|
| 129 |
-
- **MD5**: `
|
| 130 |
-
- **SHA256**: `
|
| 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 |
-
|
|
|
|
| 143 |
hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
|
| 144 |
state.context_chunks = [h["content"] for h in hits]
|
| 145 |
-
#
|
| 146 |
-
sys = {"role":"system","content":"You are a Jamaican primary school tutor. Keep
|
| 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.
|
| 153 |
-
usr = {"role":"user","content": "Create
|
| 154 |
raw = chat([sys, usr], max_new_tokens=350)
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
except:
|
| 159 |
-
|
| 160 |
-
# Normalize
|
| 161 |
fixed = []
|
| 162 |
for it in items:
|
| 163 |
-
q = it.get("question","").strip()
|
| 164 |
-
opts = it.get("options", [])[:4]
|
| 165 |
-
|
| 166 |
-
key
|
| 167 |
-
if
|
| 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
|
| 176 |
-
|
| 177 |
-
|
|
|
|
| 178 |
if correct: score += 1
|
| 179 |
-
details.append({"qno":i+1,"user":ans,"key":it["answer_key"],"correct":correct})
|
| 180 |
-
state.score
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id,
|
| 185 |
-
score=score, total=total, details=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
return state
|
| 187 |
|
| 188 |
def node_coach_or_celebrate(state: AgentState) -> AgentState:
|
| 189 |
-
if state.score == state.total and state.total
|
| 190 |
state.route = "celebrate"
|
| 191 |
return state
|
| 192 |
-
# Coach
|
| 193 |
-
wrong = []
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 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.
|
| 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:
|
| 468 |
-
- **MD5**: `
|
| 469 |
-
- **SHA256**: `
|
| 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
|
| 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
|
| 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,
|
| 519 |
return {"items": out.quiz_items}
|
| 520 |
|
| 521 |
@api.post("/agent/grade")
|
| 522 |
def agent_grade(payload: SubmitQuiz):
|
| 523 |
-
st = AgentState(
|
| 524 |
-
level_slug=payload.level_slug, assignment_id=payload.assignment_id)
|
| 525 |
st.student_answers = payload.answers
|
| 526 |
-
out = _graph.invoke(st,
|
| 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(
|
| 532 |
-
level_slug=payload.level_slug, assignment_id=payload.assignment_id)
|
| 533 |
st.student_answers = payload.answers
|
| 534 |
-
out = _graph.invoke(st,
|
| 535 |
data = {"route": out.route}
|
| 536 |
if out.route == "coach":
|
| 537 |
data["feedback"] = out.feedback
|
| 538 |
-
|
| 539 |
data["message"] = "Great job! Want to play Money Match?"
|
| 540 |
-
data["game_slug"] = "money_match"
|
| 541 |
return data
|
| 542 |
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
|
|
|
| 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**:
|
| 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
|
| 640 |
-
- **MD5**: `
|
| 641 |
-
- **SHA256**: `
|
| 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
|
| 9 |
-
Total Files Processed:
|
| 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.
|
| 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.
|
| 35 |
β βββ π README.md (107 B)
|
| 36 |
βββ π Dockerfile (632 B)
|
| 37 |
βββ π elkay.txt
|
|
|
|
| 38 |
βββ π README.md (275 B)
|
| 39 |
-
βββ π requirements.txt (
|
| 40 |
|
| 41 |
================================================================================
|
| 42 |
PROJECT STATISTICS
|
| 43 |
================================================================================
|
| 44 |
-
Total Files:
|
| 45 |
Total Directories: 2
|
| 46 |
Text Files: 12
|
| 47 |
-
Binary Files:
|
| 48 |
-
Total Size:
|
| 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.
|
| 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
|
| 115 |
-
MD5:
|
| 116 |
-
SHA256:
|
| 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 |
-
|
|
|
|
| 128 |
hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
|
| 129 |
state.context_chunks = [h["content"] for h in hits]
|
| 130 |
-
#
|
| 131 |
-
sys = {"role":"system","content":"You are a Jamaican primary school tutor. Keep
|
| 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.
|
| 138 |
-
usr = {"role":"user","content": "Create
|
| 139 |
raw = chat([sys, usr], max_new_tokens=350)
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
except:
|
| 144 |
-
|
| 145 |
-
# Normalize
|
| 146 |
fixed = []
|
| 147 |
for it in items:
|
| 148 |
-
q = it.get("question","").strip()
|
| 149 |
-
opts = it.get("options", [])[:4]
|
| 150 |
-
|
| 151 |
-
key
|
| 152 |
-
if
|
| 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
|
| 161 |
-
|
| 162 |
-
|
|
|
|
| 163 |
if correct: score += 1
|
| 164 |
-
details.append({"qno":i+1,"user":ans,"key":it["answer_key"],"correct":correct})
|
| 165 |
-
state.score
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id,
|
| 170 |
-
score=score, total=total, details=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
return state
|
| 172 |
|
| 173 |
def node_coach_or_celebrate(state: AgentState) -> AgentState:
|
| 174 |
-
if state.score == state.total and state.total
|
| 175 |
state.route = "celebrate"
|
| 176 |
return state
|
| 177 |
-
# Coach
|
| 178 |
-
wrong = []
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 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.
|
| 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:
|
| 460 |
-
MD5:
|
| 461 |
-
SHA256:
|
| 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
|
| 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
|
| 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,
|
| 510 |
return {"items": out.quiz_items}
|
| 511 |
|
| 512 |
@api.post("/agent/grade")
|
| 513 |
def agent_grade(payload: SubmitQuiz):
|
| 514 |
-
st = AgentState(
|
| 515 |
-
level_slug=payload.level_slug, assignment_id=payload.assignment_id)
|
| 516 |
st.student_answers = payload.answers
|
| 517 |
-
out = _graph.invoke(st,
|
| 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(
|
| 523 |
-
level_slug=payload.level_slug, assignment_id=payload.assignment_id)
|
| 524 |
st.student_answers = payload.answers
|
| 525 |
-
out = _graph.invoke(st,
|
| 526 |
data = {"route": out.route}
|
| 527 |
if out.route == "coach":
|
| 528 |
data["feedback"] = out.feedback
|
| 529 |
-
|
| 530 |
data["message"] = "Great job! Want to play Money Match?"
|
| 531 |
-
data["game_slug"] = "money_match"
|
| 532 |
return data
|
| 533 |
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
|
|
|
| 537 |
================================================================================
|
| 538 |
|
| 539 |
|
|
@@ -626,15 +631,15 @@ FILE: requirements.txt
|
|
| 626 |
|
| 627 |
FILE INFORMATION:
|
| 628 |
----------------------------------------
|
| 629 |
-
Size:
|
| 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
|
| 636 |
-
MD5:
|
| 637 |
-
SHA256:
|
| 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 |
-
|
|
|
|
| 8 |
hits = search_chunks(state.lesson_id, state.level_slug, qvec, k=6)
|
| 9 |
state.context_chunks = [h["content"] for h in hits]
|
| 10 |
-
#
|
| 11 |
-
sys = {"role":"system","content":"You are a Jamaican primary school tutor. Keep
|
| 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.
|
| 18 |
-
usr = {"role":"user","content": "Create
|
| 19 |
raw = chat([sys, usr], max_new_tokens=350)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
except:
|
| 24 |
-
|
| 25 |
-
# Normalize
|
| 26 |
fixed = []
|
| 27 |
for it in items:
|
| 28 |
-
q = it.get("question","").strip()
|
| 29 |
-
opts = it.get("options", [])[:4]
|
| 30 |
-
|
| 31 |
-
key
|
| 32 |
-
if
|
| 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
|
| 41 |
-
|
| 42 |
-
|
|
|
|
| 43 |
if correct: score += 1
|
| 44 |
-
details.append({"qno":i+1,"user":ans,"key":it["answer_key"],"correct":correct})
|
| 45 |
-
state.score
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
record_submission(state.student_id, quiz_id=0, assignment_id=state.assignment_id,
|
| 50 |
-
score=score, total=total, details=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
return state
|
| 52 |
|
| 53 |
def node_coach_or_celebrate(state: AgentState) -> AgentState:
|
| 54 |
-
if state.score == state.total and state.total
|
| 55 |
state.route = "celebrate"
|
| 56 |
return state
|
| 57 |
-
# Coach
|
| 58 |
-
wrong = []
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 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
|
| 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
|
| 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,
|
| 45 |
return {"items": out.quiz_items}
|
| 46 |
|
| 47 |
@api.post("/agent/grade")
|
| 48 |
def agent_grade(payload: SubmitQuiz):
|
| 49 |
-
st = AgentState(
|
| 50 |
-
level_slug=payload.level_slug, assignment_id=payload.assignment_id)
|
| 51 |
st.student_answers = payload.answers
|
| 52 |
-
out = _graph.invoke(st,
|
| 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(
|
| 58 |
-
level_slug=payload.level_slug, assignment_id=payload.assignment_id)
|
| 59 |
st.student_answers = payload.answers
|
| 60 |
-
out = _graph.invoke(st,
|
| 61 |
data = {"route": out.route}
|
| 62 |
if out.route == "coach":
|
| 63 |
data["feedback"] = out.feedback
|
| 64 |
-
|
| 65 |
data["message"] = "Great job! Want to play Money Match?"
|
| 66 |
-
data["game_slug"] = "money_match"
|
| 67 |
return data
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
| 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
|