Spaces:
Sleeping
Sleeping
Meet Patel
commited on
Commit
·
bbd9cd6
1
Parent(s):
d4df2a7
Core and Advanced Features is working with mock data.
Browse files- app.py +222 -56
- client.py +131 -19
- main.py +456 -56
- requirements.txt +3 -1
- run.py +15 -9
app.py
CHANGED
@@ -31,6 +31,126 @@ def image_to_base64(img):
|
|
31 |
img_str = base64.b64encode(buffered.getvalue()).decode()
|
32 |
return img_str
|
33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
async def api_request(endpoint, method="GET", params=None, json_data=None):
|
35 |
"""Make an API request to the server"""
|
36 |
url = f"{SERVER_URL}/api/{endpoint}"
|
@@ -70,42 +190,45 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
70 |
with gr.Tabs() as tabs:
|
71 |
# Tab 1: Core Features
|
72 |
with gr.Tab("Core Features"):
|
73 |
-
gr.
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
gr.Markdown("## Concept Graph")
|
97 |
-
concept_graph_btn = gr.Button("Show Concept Graph")
|
98 |
-
concept_graph_output = gr.JSON(label="Concept Graph")
|
99 |
-
|
100 |
-
async def on_concept_graph_click():
|
101 |
-
result = await api_request("concept_graph")
|
102 |
-
return result
|
103 |
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
|
|
109 |
|
110 |
gr.Markdown("## Assessment Generation")
|
111 |
with gr.Row():
|
@@ -122,10 +245,15 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
122 |
quiz_output = gr.JSON(label="Generated Quiz")
|
123 |
|
124 |
async def on_generate_quiz(concepts, difficulty):
|
|
|
|
|
|
|
|
|
|
|
125 |
result = await api_request(
|
126 |
"generate_quiz",
|
127 |
"POST",
|
128 |
-
json_data=
|
129 |
)
|
130 |
return result
|
131 |
|
@@ -148,8 +276,11 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
148 |
|
149 |
with gr.Column():
|
150 |
lesson_output = gr.JSON(label="Lesson Plan")
|
|
|
|
|
|
|
151 |
gen_lesson_btn.click(
|
152 |
-
fn=
|
153 |
inputs=[topic_input, grade_input, duration_input],
|
154 |
outputs=[lesson_output]
|
155 |
)
|
@@ -159,17 +290,49 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
159 |
with gr.Row():
|
160 |
with gr.Column():
|
161 |
country_input = gr.Dropdown(
|
162 |
-
choices=["
|
163 |
label="Country",
|
164 |
-
value="
|
165 |
)
|
166 |
standards_btn = gr.Button("Get Standards")
|
167 |
|
168 |
with gr.Column():
|
169 |
standards_output = gr.JSON(label="Curriculum Standards")
|
170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
standards_btn.click(
|
172 |
-
fn=
|
173 |
inputs=[country_input],
|
174 |
outputs=[standards_output]
|
175 |
)
|
@@ -185,8 +348,11 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
185 |
|
186 |
with gr.Column():
|
187 |
text_output = gr.JSON(label="Response")
|
|
|
|
|
|
|
188 |
text_btn.click(
|
189 |
-
fn=
|
190 |
inputs=[text_input],
|
191 |
outputs=[text_output]
|
192 |
)
|
@@ -201,8 +367,11 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
201 |
with gr.Column():
|
202 |
drawing_output = gr.JSON(label="Recognition Results")
|
203 |
|
|
|
|
|
|
|
204 |
drawing_btn.click(
|
205 |
-
fn=
|
206 |
inputs=[drawing_input],
|
207 |
outputs=[drawing_output]
|
208 |
)
|
@@ -210,27 +379,21 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
210 |
# Tab 4: Analytics
|
211 |
with gr.Tab("Analytics"):
|
212 |
gr.Markdown("## Student Performance")
|
213 |
-
analytics_btn = gr.Button("Generate Analytics Report")
|
214 |
-
timeframe = gr.Slider(minimum=7, maximum=90, value=30, step=1, label="Timeframe (days)")
|
215 |
-
analytics_output = gr.JSON(label="Performance Analytics")
|
216 |
-
analytics_btn.click(
|
217 |
-
fn=lambda x: asyncio.run(client.get_student_analytics("student_12345", x)),
|
218 |
-
inputs=[timeframe],
|
219 |
-
outputs=[analytics_output]
|
220 |
-
)
|
221 |
-
|
222 |
-
gr.Markdown("## Error Pattern Analysis")
|
223 |
|
|
|
224 |
error_concept = gr.Dropdown(
|
225 |
choices=["math_algebra_basics", "math_algebra_linear_equations", "math_algebra_quadratic_equations"],
|
226 |
-
label="Select Concept for
|
227 |
value="math_algebra_linear_equations"
|
228 |
)
|
229 |
-
error_btn = gr.Button("Analyze
|
230 |
-
error_output = gr.JSON(label="
|
231 |
|
|
|
|
|
|
|
232 |
error_btn.click(
|
233 |
-
fn=
|
234 |
inputs=[error_concept],
|
235 |
outputs=[error_output]
|
236 |
)
|
@@ -254,8 +417,11 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
|
|
254 |
with gr.Column():
|
255 |
plagiarism_output = gr.JSON(label="Originality Report")
|
256 |
|
|
|
|
|
|
|
257 |
plagiarism_btn.click(
|
258 |
-
fn=
|
259 |
inputs=[submission_input, reference_input],
|
260 |
outputs=[plagiarism_output]
|
261 |
)
|
|
|
31 |
img_str = base64.b64encode(buffered.getvalue()).decode()
|
32 |
return img_str
|
33 |
|
34 |
+
async def load_concept_graph(concept_id: str = None):
|
35 |
+
"""
|
36 |
+
Load and visualize the concept graph for a given concept ID.
|
37 |
+
If no concept_id is provided, returns the first available concept.
|
38 |
+
|
39 |
+
Returns:
|
40 |
+
tuple: (figure, concept_details, related_concepts) or (None, error_dict, [])
|
41 |
+
"""
|
42 |
+
try:
|
43 |
+
print(f"[DEBUG] Loading concept graph for concept_id: {concept_id}")
|
44 |
+
|
45 |
+
# Get concept graph data from the server
|
46 |
+
# First try direct API call, fall back to MCP tool if needed
|
47 |
+
result = await client.get_concept_graph(concept_id, use_mcp=False)
|
48 |
+
|
49 |
+
# If direct API call fails, try MCP tool
|
50 |
+
if "error" in result:
|
51 |
+
print(f"[DEBUG] Direct API call failed, trying MCP tool: {result}")
|
52 |
+
result = await client.get_concept_graph(concept_id, use_mcp=True)
|
53 |
+
print(f"[DEBUG] Server response: {result}")
|
54 |
+
|
55 |
+
if not result or not isinstance(result, dict):
|
56 |
+
error_msg = "Invalid server response"
|
57 |
+
print(f"[ERROR] {error_msg}")
|
58 |
+
return None, {"error": error_msg}, []
|
59 |
+
|
60 |
+
if "error" in result:
|
61 |
+
print(f"[ERROR] Server returned error: {result['error']}")
|
62 |
+
return None, {"error": result["error"]}, []
|
63 |
+
|
64 |
+
# Handle response when no specific concept_id was requested
|
65 |
+
if "concepts" in result and not concept_id:
|
66 |
+
if not result["concepts"]:
|
67 |
+
error_msg = "No concepts available"
|
68 |
+
print(f"[ERROR] {error_msg}")
|
69 |
+
return None, {"error": error_msg}, []
|
70 |
+
concept = result["concepts"][0]
|
71 |
+
print(f"[DEBUG] Using first concept from list: {concept.get('id')}")
|
72 |
+
else:
|
73 |
+
concept = result
|
74 |
+
print(f"[DEBUG] Using direct concept: {concept.get('id')}")
|
75 |
+
|
76 |
+
# Validate the concept structure
|
77 |
+
if not isinstance(concept, dict) or not concept.get('id'):
|
78 |
+
error_msg = "Invalid concept data structure"
|
79 |
+
print(f"[ERROR] {error_msg}: {concept}")
|
80 |
+
return None, {"error": error_msg}, []
|
81 |
+
|
82 |
+
# Create a simple visualization using matplotlib
|
83 |
+
import matplotlib.pyplot as plt
|
84 |
+
import networkx as nx
|
85 |
+
|
86 |
+
# Create a directed graph
|
87 |
+
G = nx.DiGraph()
|
88 |
+
|
89 |
+
# Add the main concept node
|
90 |
+
G.add_node(concept["id"], label=concept["name"], type="concept")
|
91 |
+
|
92 |
+
# Add related concepts
|
93 |
+
related_concepts = []
|
94 |
+
if "related" in concept:
|
95 |
+
for rel_id in concept["related"]:
|
96 |
+
rel_result = await client.get_concept_graph(rel_id)
|
97 |
+
if "error" not in rel_result:
|
98 |
+
G.add_node(rel_id, label=rel_result["name"], type="related")
|
99 |
+
G.add_edge(concept["id"], rel_id, relationship="related_to")
|
100 |
+
related_concepts.append([rel_id, rel_result.get("name", ""), rel_result.get("description", "")])
|
101 |
+
|
102 |
+
# Add prerequisites if any
|
103 |
+
if "prerequisites" in concept:
|
104 |
+
for prereq_id in concept["prerequisites"]:
|
105 |
+
prereq_result = await client.get_concept_graph(prereq_id)
|
106 |
+
if "error" not in prereq_result:
|
107 |
+
G.add_node(prereq_id, label=prereq_result["name"], type="prerequisite")
|
108 |
+
G.add_edge(prereq_id, concept["id"], relationship="prerequisite_for")
|
109 |
+
|
110 |
+
# Draw the graph
|
111 |
+
plt.figure(figsize=(10, 8))
|
112 |
+
pos = nx.spring_layout(G)
|
113 |
+
|
114 |
+
# Draw nodes with different colors based on type
|
115 |
+
node_colors = []
|
116 |
+
for node in G.nodes():
|
117 |
+
if G.nodes[node].get("type") == "concept":
|
118 |
+
node_colors.append("lightblue")
|
119 |
+
elif G.nodes[node].get("type") == "prerequisite":
|
120 |
+
node_colors.append("lightcoral")
|
121 |
+
else:
|
122 |
+
node_colors.append("lightgreen")
|
123 |
+
|
124 |
+
nx.draw_networkx_nodes(G, pos, node_size=2000, node_color=node_colors, alpha=0.8)
|
125 |
+
nx.draw_networkx_edges(G, pos, width=1.0, alpha=0.5)
|
126 |
+
|
127 |
+
# Add labels
|
128 |
+
labels = {node: G.nodes[node].get("label", node) for node in G.nodes()}
|
129 |
+
nx.draw_networkx_labels(G, pos, labels, font_size=10, font_weight="bold")
|
130 |
+
|
131 |
+
# Add edge labels
|
132 |
+
edge_labels = {(u, v): d["relationship"] for u, v, d in G.edges(data=True)}
|
133 |
+
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8)
|
134 |
+
|
135 |
+
plt.title(f"Concept Graph: {concept.get('name', concept_id)}")
|
136 |
+
plt.axis("off")
|
137 |
+
|
138 |
+
# Return the figure and concept details
|
139 |
+
concept_details = {
|
140 |
+
"id": concept.get("id", ""),
|
141 |
+
"name": concept.get("name", ""),
|
142 |
+
"description": concept.get("description", ""),
|
143 |
+
"related_concepts_count": len(concept.get("related", [])),
|
144 |
+
"prerequisites_count": len(concept.get("prerequisites", []))
|
145 |
+
}
|
146 |
+
|
147 |
+
return plt.gcf(), concept_details, related_concepts
|
148 |
+
|
149 |
+
except Exception as e:
|
150 |
+
import traceback
|
151 |
+
traceback.print_exc()
|
152 |
+
return None, {"error": f"Failed to load concept graph: {str(e)}"}, []
|
153 |
+
|
154 |
async def api_request(endpoint, method="GET", params=None, json_data=None):
|
155 |
"""Make an API request to the server"""
|
156 |
url = f"{SERVER_URL}/api/{endpoint}"
|
|
|
190 |
with gr.Tabs() as tabs:
|
191 |
# Tab 1: Core Features
|
192 |
with gr.Tab("Core Features"):
|
193 |
+
with gr.Blocks() as concept_graph_tab:
|
194 |
+
gr.Markdown("## Concept Graph Visualization")
|
195 |
+
with gr.Row():
|
196 |
+
with gr.Column(scale=3):
|
197 |
+
concept_id = gr.Dropdown(
|
198 |
+
label="Select a Concept",
|
199 |
+
choices=["python", "functions", "oop", "data_structures"],
|
200 |
+
value="python",
|
201 |
+
interactive=True
|
202 |
+
)
|
203 |
+
load_concept_btn = gr.Button("Load Concept Graph", variant="primary")
|
204 |
+
|
205 |
+
# Concept details
|
206 |
+
concept_details = gr.JSON(label="Concept Details")
|
207 |
+
|
208 |
+
# Related concepts
|
209 |
+
related_concepts = gr.Dataframe(
|
210 |
+
headers=["ID", "Name", "Description"],
|
211 |
+
datatype=["str", "str", "str"],
|
212 |
+
label="Related Concepts"
|
213 |
+
)
|
214 |
+
|
215 |
+
# Graph visualization
|
216 |
+
with gr.Column(scale=7):
|
217 |
+
graph_output = gr.Plot(label="Concept Graph")
|
218 |
|
219 |
+
# Button click handler
|
220 |
+
load_concept_btn.click(
|
221 |
+
fn=load_concept_graph,
|
222 |
+
inputs=[concept_id],
|
223 |
+
outputs=[graph_output, concept_details, related_concepts]
|
224 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
225 |
|
226 |
+
# Load default concept on tab click
|
227 |
+
concept_graph_tab.load(
|
228 |
+
fn=load_concept_graph,
|
229 |
+
inputs=[concept_id],
|
230 |
+
outputs=[graph_output, concept_details, related_concepts]
|
231 |
+
)
|
232 |
|
233 |
gr.Markdown("## Assessment Generation")
|
234 |
with gr.Row():
|
|
|
245 |
quiz_output = gr.JSON(label="Generated Quiz")
|
246 |
|
247 |
async def on_generate_quiz(concepts, difficulty):
|
248 |
+
# Convert the request to match the expected format
|
249 |
+
request_data = {
|
250 |
+
"concept_ids": concepts if isinstance(concepts, list) else [concepts],
|
251 |
+
"difficulty": int(difficulty)
|
252 |
+
}
|
253 |
result = await api_request(
|
254 |
"generate_quiz",
|
255 |
"POST",
|
256 |
+
json_data=request_data
|
257 |
)
|
258 |
return result
|
259 |
|
|
|
276 |
|
277 |
with gr.Column():
|
278 |
lesson_output = gr.JSON(label="Lesson Plan")
|
279 |
+
async def generate_lesson_async(topic, grade, duration):
|
280 |
+
return await client.generate_lesson(topic, grade, duration)
|
281 |
+
|
282 |
gen_lesson_btn.click(
|
283 |
+
fn=generate_lesson_async,
|
284 |
inputs=[topic_input, grade_input, duration_input],
|
285 |
outputs=[lesson_output]
|
286 |
)
|
|
|
290 |
with gr.Row():
|
291 |
with gr.Column():
|
292 |
country_input = gr.Dropdown(
|
293 |
+
choices=["US", "UK"],
|
294 |
label="Country",
|
295 |
+
value="US"
|
296 |
)
|
297 |
standards_btn = gr.Button("Get Standards")
|
298 |
|
299 |
with gr.Column():
|
300 |
standards_output = gr.JSON(label="Curriculum Standards")
|
301 |
|
302 |
+
async def get_standards_async(country):
|
303 |
+
try:
|
304 |
+
# Convert display text to lowercase for the API
|
305 |
+
country_code = country.lower()
|
306 |
+
response = await client.get_curriculum_standards(country_code)
|
307 |
+
|
308 |
+
# Format the response for better display
|
309 |
+
if "standards" in response:
|
310 |
+
formatted = {
|
311 |
+
"country": response["standards"]["name"],
|
312 |
+
"subjects": {},
|
313 |
+
"website": response["standards"].get("website", "")
|
314 |
+
}
|
315 |
+
|
316 |
+
# Format subjects and domains
|
317 |
+
for subj_key, subj_info in response["standards"]["subjects"].items():
|
318 |
+
formatted["subjects"][subj_key] = {
|
319 |
+
"description": subj_info["description"],
|
320 |
+
"domains": subj_info["domains"]
|
321 |
+
}
|
322 |
+
|
323 |
+
# Add grade levels or key stages if available
|
324 |
+
if "grade_levels" in response["standards"]:
|
325 |
+
formatted["grade_levels"] = response["standards"]["grade_levels"]
|
326 |
+
elif "key_stages" in response["standards"]:
|
327 |
+
formatted["key_stages"] = response["standards"]["key_stages"]
|
328 |
+
|
329 |
+
return formatted
|
330 |
+
return response
|
331 |
+
except Exception as e:
|
332 |
+
return {"error": f"Failed to fetch standards: {str(e)}"}
|
333 |
+
|
334 |
standards_btn.click(
|
335 |
+
fn=get_standards_async,
|
336 |
inputs=[country_input],
|
337 |
outputs=[standards_output]
|
338 |
)
|
|
|
348 |
|
349 |
with gr.Column():
|
350 |
text_output = gr.JSON(label="Response")
|
351 |
+
async def text_interaction_async(text):
|
352 |
+
return await client.text_interaction(text, "student_12345")
|
353 |
+
|
354 |
text_btn.click(
|
355 |
+
fn=text_interaction_async,
|
356 |
inputs=[text_input],
|
357 |
outputs=[text_output]
|
358 |
)
|
|
|
367 |
with gr.Column():
|
368 |
drawing_output = gr.JSON(label="Recognition Results")
|
369 |
|
370 |
+
async def handwriting_async(drawing):
|
371 |
+
return await client.handwriting_recognition(image_to_base64(drawing), "student_12345")
|
372 |
+
|
373 |
drawing_btn.click(
|
374 |
+
fn=handwriting_async,
|
375 |
inputs=[drawing_input],
|
376 |
outputs=[drawing_output]
|
377 |
)
|
|
|
379 |
# Tab 4: Analytics
|
380 |
with gr.Tab("Analytics"):
|
381 |
gr.Markdown("## Student Performance")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
|
383 |
+
# Error Pattern Analysis
|
384 |
error_concept = gr.Dropdown(
|
385 |
choices=["math_algebra_basics", "math_algebra_linear_equations", "math_algebra_quadratic_equations"],
|
386 |
+
label="Select Concept for Analysis",
|
387 |
value="math_algebra_linear_equations"
|
388 |
)
|
389 |
+
error_btn = gr.Button("Analyze Concept")
|
390 |
+
error_output = gr.JSON(label="Analysis Results")
|
391 |
|
392 |
+
async def analyze_errors_async(concept):
|
393 |
+
return await client.analyze_error_patterns("student_12345", concept)
|
394 |
+
|
395 |
error_btn.click(
|
396 |
+
fn=analyze_errors_async,
|
397 |
inputs=[error_concept],
|
398 |
outputs=[error_output]
|
399 |
)
|
|
|
417 |
with gr.Column():
|
418 |
plagiarism_output = gr.JSON(label="Originality Report")
|
419 |
|
420 |
+
async def check_plagiarism_async(submission, reference):
|
421 |
+
return await client.check_submission_originality(submission, [reference])
|
422 |
+
|
423 |
plagiarism_btn.click(
|
424 |
+
fn=check_plagiarism_async,
|
425 |
inputs=[submission_input, reference_input],
|
426 |
outputs=[plagiarism_output]
|
427 |
)
|
client.py
CHANGED
@@ -37,13 +37,14 @@ class TutorXClient:
|
|
37 |
}
|
38 |
)
|
39 |
|
40 |
-
async def _call_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
41 |
"""
|
42 |
Call an MCP tool on the server
|
43 |
|
44 |
Args:
|
45 |
tool_name: Name of the tool to call
|
46 |
params: Parameters to pass to the tool
|
|
|
47 |
|
48 |
Returns:
|
49 |
Tool response
|
@@ -51,21 +52,36 @@ class TutorXClient:
|
|
51 |
await self._ensure_session()
|
52 |
try:
|
53 |
url = f"{self.server_url}{API_PREFIX}/{tool_name}"
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
|
|
|
|
|
|
|
|
63 |
except Exception as e:
|
64 |
return {
|
65 |
"error": f"Failed to call tool: {str(e)}",
|
66 |
"timestamp": datetime.now().isoformat()
|
67 |
}
|
68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
async def _get_resource(self, resource_uri: str) -> Dict[str, Any]:
|
70 |
"""
|
71 |
Get an MCP resource from the server
|
@@ -117,16 +133,74 @@ class TutorXClient:
|
|
117 |
|
118 |
# ------------ Core Features ------------
|
119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
async def assess_skill(self, student_id: str, concept_id: str) -> Dict[str, Any]:
|
121 |
-
"""Assess student's skill
|
122 |
-
return await self._call_tool("assess_skill", {
|
123 |
-
"student_id": student_id,
|
124 |
-
"concept_id": concept_id
|
125 |
-
})
|
126 |
-
|
127 |
-
async def get_concept_graph(self) -> Dict[str, Any]:
|
128 |
-
"""Get the full knowledge concept graph"""
|
129 |
-
return await self._get_resource("concept-graph://")
|
130 |
|
131 |
async def get_learning_path(self, student_id: str) -> Dict[str, Any]:
|
132 |
"""Get personalized learning path for a student"""
|
@@ -247,11 +321,49 @@ class TutorXClient:
|
|
247 |
"reference_sources": reference_sources
|
248 |
})
|
249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
250 |
async def close(self):
|
251 |
"""Close the aiohttp session"""
|
252 |
if self.session:
|
253 |
await self.session.close()
|
254 |
self.session = None
|
255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
256 |
# Create a default client instance for easy import
|
257 |
client = TutorXClient()
|
|
|
37 |
}
|
38 |
)
|
39 |
|
40 |
+
async def _call_tool(self, tool_name: str, params: Dict[str, Any], method: str = "POST") -> Dict[str, Any]:
|
41 |
"""
|
42 |
Call an MCP tool on the server
|
43 |
|
44 |
Args:
|
45 |
tool_name: Name of the tool to call
|
46 |
params: Parameters to pass to the tool
|
47 |
+
method: HTTP method to use (GET or POST)
|
48 |
|
49 |
Returns:
|
50 |
Tool response
|
|
|
52 |
await self._ensure_session()
|
53 |
try:
|
54 |
url = f"{self.server_url}{API_PREFIX}/{tool_name}"
|
55 |
+
|
56 |
+
# Convert params to query string for GET requests
|
57 |
+
if method.upper() == "GET":
|
58 |
+
from urllib.parse import urlencode
|
59 |
+
if params:
|
60 |
+
query_string = urlencode(params, doseq=True)
|
61 |
+
url = f"{url}?{query_string}"
|
62 |
+
async with self.session.get(url, timeout=30) as response:
|
63 |
+
return await self._handle_response(response)
|
64 |
+
else:
|
65 |
+
async with self.session.post(url, json=params, timeout=30) as response:
|
66 |
+
return await self._handle_response(response)
|
67 |
+
|
68 |
except Exception as e:
|
69 |
return {
|
70 |
"error": f"Failed to call tool: {str(e)}",
|
71 |
"timestamp": datetime.now().isoformat()
|
72 |
}
|
73 |
|
74 |
+
async def _handle_response(self, response) -> Dict[str, Any]:
|
75 |
+
"""Handle the HTTP response"""
|
76 |
+
if response.status == 200:
|
77 |
+
return await response.json()
|
78 |
+
else:
|
79 |
+
error = await response.text()
|
80 |
+
return {
|
81 |
+
"error": f"API error ({response.status}): {error}",
|
82 |
+
"timestamp": datetime.now().isoformat()
|
83 |
+
}
|
84 |
+
|
85 |
async def _get_resource(self, resource_uri: str) -> Dict[str, Any]:
|
86 |
"""
|
87 |
Get an MCP resource from the server
|
|
|
133 |
|
134 |
# ------------ Core Features ------------
|
135 |
|
136 |
+
async def get_concept_graph(self, concept_id: str = None, use_mcp: bool = False) -> Dict[str, Any]:
|
137 |
+
"""
|
138 |
+
Get the concept graph for a specific concept or all concepts.
|
139 |
+
|
140 |
+
Args:
|
141 |
+
concept_id: Optional ID of the concept to fetch. If None, returns all concepts.
|
142 |
+
use_mcp: If True, uses the MCP tool interface instead of direct API call.
|
143 |
+
|
144 |
+
Returns:
|
145 |
+
Dict containing concept data or error information.
|
146 |
+
"""
|
147 |
+
try:
|
148 |
+
# Ensure we have a session
|
149 |
+
await self._ensure_session()
|
150 |
+
|
151 |
+
if use_mcp:
|
152 |
+
# Use MCP tool interface
|
153 |
+
print(f"[CLIENT] Using MCP tool to get concept graph for: {concept_id}")
|
154 |
+
return await self._call_tool("get_concept_graph", {"concept_id": concept_id} if concept_id else {})
|
155 |
+
|
156 |
+
# Use direct API call (default)
|
157 |
+
url = f"{self.server_url}/api/concept_graph"
|
158 |
+
params = {}
|
159 |
+
if concept_id:
|
160 |
+
params["concept_id"] = concept_id
|
161 |
+
|
162 |
+
print(f"[CLIENT] Fetching concept graph from {url} with params: {params}")
|
163 |
+
|
164 |
+
async with self.session.get(
|
165 |
+
url,
|
166 |
+
params=params,
|
167 |
+
timeout=30
|
168 |
+
) as response:
|
169 |
+
print(f"[CLIENT] Response status: {response.status}")
|
170 |
+
|
171 |
+
if response.status == 404:
|
172 |
+
error_msg = f"Concept {concept_id} not found"
|
173 |
+
print(f"[CLIENT] {error_msg}")
|
174 |
+
return {"error": error_msg}
|
175 |
+
|
176 |
+
response.raise_for_status()
|
177 |
+
|
178 |
+
# Parse the JSON response
|
179 |
+
result = await response.json()
|
180 |
+
print(f"[CLIENT] Received response: {result}")
|
181 |
+
|
182 |
+
return result
|
183 |
+
|
184 |
+
except asyncio.TimeoutError:
|
185 |
+
error_msg = "Request timed out"
|
186 |
+
print(f"[CLIENT] {error_msg}")
|
187 |
+
return {"error": error_msg}
|
188 |
+
|
189 |
+
except aiohttp.ClientError as e:
|
190 |
+
error_msg = f"HTTP client error: {str(e)}"
|
191 |
+
print(f"[CLIENT] {error_msg}")
|
192 |
+
return {"error": error_msg}
|
193 |
+
|
194 |
+
except Exception as e:
|
195 |
+
error_msg = f"Unexpected error: {str(e)}"
|
196 |
+
print(f"[CLIENT] {error_msg}")
|
197 |
+
import traceback
|
198 |
+
traceback.print_exc()
|
199 |
+
return {"error": error_msg}
|
200 |
+
|
201 |
async def assess_skill(self, student_id: str, concept_id: str) -> Dict[str, Any]:
|
202 |
+
"""Assess a student's skill on a specific concept"""
|
203 |
+
return await self._call_tool("assess_skill", {"student_id": student_id, "concept_id": concept_id})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
|
205 |
async def get_learning_path(self, student_id: str) -> Dict[str, Any]:
|
206 |
"""Get personalized learning path for a student"""
|
|
|
321 |
"reference_sources": reference_sources
|
322 |
})
|
323 |
|
324 |
+
|
325 |
+
async def get_curriculum_standards(self, country_code: str = "us") -> Dict[str, Any]:
|
326 |
+
"""
|
327 |
+
Get curriculum standards for a specific country
|
328 |
+
|
329 |
+
Args:
|
330 |
+
country_code: ISO country code (e.g., 'us', 'uk')
|
331 |
+
|
332 |
+
Returns:
|
333 |
+
Dictionary containing curriculum standards
|
334 |
+
"""
|
335 |
+
return await self._call_tool(
|
336 |
+
"curriculum-standards", # Note the endpoint name matches the API route
|
337 |
+
{"country": country_code.lower()}, # Note the parameter name matches the API
|
338 |
+
method="GET" # Use GET for this endpoint
|
339 |
+
)
|
340 |
+
|
341 |
async def close(self):
|
342 |
"""Close the aiohttp session"""
|
343 |
if self.session:
|
344 |
await self.session.close()
|
345 |
self.session = None
|
346 |
|
347 |
+
async def generate_lesson(self, topic: str, grade_level: int, duration_minutes: int) -> Dict[str, Any]:
|
348 |
+
"""
|
349 |
+
Generate a lesson plan for the given topic, grade level, and duration
|
350 |
+
|
351 |
+
Args:
|
352 |
+
topic: The topic for the lesson
|
353 |
+
grade_level: The grade level (1-12)
|
354 |
+
duration_minutes: Duration of the lesson in minutes
|
355 |
+
|
356 |
+
Returns:
|
357 |
+
Dictionary containing the generated lesson plan
|
358 |
+
"""
|
359 |
+
return await self._call_tool(
|
360 |
+
"generate_lesson",
|
361 |
+
{
|
362 |
+
"topic": topic,
|
363 |
+
"grade_level": grade_level,
|
364 |
+
"duration_minutes": duration_minutes
|
365 |
+
}
|
366 |
+
)
|
367 |
+
|
368 |
# Create a default client instance for easy import
|
369 |
client = TutorXClient()
|
main.py
CHANGED
@@ -28,6 +28,9 @@ from utils.assessment import (
|
|
28 |
generate_performance_analytics,
|
29 |
detect_plagiarism
|
30 |
)
|
|
|
|
|
|
|
31 |
|
32 |
# Get server configuration from environment variables with defaults
|
33 |
SERVER_HOST = os.getenv("MCP_HOST", "0.0.0.0") # Allow connections from any IP
|
@@ -61,50 +64,171 @@ mcp = FastMCP(
|
|
61 |
|
62 |
# ------------------ Core Features ------------------
|
63 |
|
64 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
@mcp.tool()
|
66 |
async def assess_skill(student_id: str, concept_id: str) -> Dict[str, Any]:
|
67 |
-
"""
|
68 |
-
|
|
|
|
|
|
|
69 |
|
70 |
-
|
71 |
-
|
72 |
-
|
|
|
|
|
|
|
|
|
73 |
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
|
96 |
@mcp.resource("concept-graph://")
|
97 |
-
async def
|
98 |
"""Get the full knowledge concept graph"""
|
99 |
return {
|
100 |
"nodes": [
|
101 |
-
{"id": "
|
102 |
-
{"id": "
|
103 |
-
{"id": "
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
],
|
105 |
"edges": [
|
106 |
-
{"from": "
|
107 |
-
{"from": "
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
]
|
109 |
}
|
110 |
|
@@ -113,10 +237,63 @@ async def get_learning_path(student_id: str) -> Dict[str, Any]:
|
|
113 |
"""Get personalized learning path for a student"""
|
114 |
return {
|
115 |
"student_id": student_id,
|
116 |
-
"current_concepts": ["math_algebra_linear_equations"]
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
}
|
121 |
|
122 |
# Assessment Suite
|
@@ -132,46 +309,269 @@ async def generate_quiz(concept_ids: List[str], difficulty: int = 2) -> Dict[str
|
|
132 |
Returns:
|
133 |
Quiz object with questions and answers
|
134 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
return {
|
136 |
-
"quiz_id": "
|
137 |
"concept_ids": concept_ids,
|
138 |
"difficulty": difficulty,
|
139 |
-
"questions":
|
140 |
-
|
141 |
-
"id": "q1",
|
142 |
-
"text": "Solve for x: 2x + 3 = 7",
|
143 |
-
"type": "algebraic_equation",
|
144 |
-
"answer": "x = 2",
|
145 |
-
"solution_steps": [
|
146 |
-
"2x + 3 = 7",
|
147 |
-
"2x = 7 - 3",
|
148 |
-
"2x = 4",
|
149 |
-
"x = 4/2 = 2"
|
150 |
-
]
|
151 |
-
}
|
152 |
-
]
|
153 |
}
|
154 |
|
|
|
|
|
155 |
# API Endpoints
|
156 |
@api_app.get("/api/health")
|
157 |
async def health_check():
|
158 |
return {"status": "ok", "timestamp": datetime.now().isoformat()}
|
159 |
|
160 |
@api_app.get("/api/assess_skill")
|
161 |
-
async def assess_skill_api(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
result = await assess_skill(student_id, concept_id)
|
|
|
|
|
|
|
|
|
|
|
164 |
return result
|
|
|
|
|
|
|
|
|
165 |
except Exception as e:
|
166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
|
168 |
@api_app.post("/api/generate_quiz")
|
169 |
-
async def generate_quiz_api(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
result = await generate_quiz(concept_ids, difficulty)
|
172 |
return result
|
|
|
|
|
|
|
173 |
except Exception as e:
|
174 |
-
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
|
176 |
# Mount MCP app to /mcp path
|
177 |
mcp.app = api_app
|
|
|
28 |
generate_performance_analytics,
|
29 |
detect_plagiarism
|
30 |
)
|
31 |
+
from typing import List, Dict, Any, Optional, Union
|
32 |
+
import random
|
33 |
+
from datetime import datetime, timedelta, timezone
|
34 |
|
35 |
# Get server configuration from environment variables with defaults
|
36 |
SERVER_HOST = os.getenv("MCP_HOST", "0.0.0.0") # Allow connections from any IP
|
|
|
64 |
|
65 |
# ------------------ Core Features ------------------
|
66 |
|
67 |
+
# Store the concept graph data in memory
|
68 |
+
CONCEPT_GRAPH = {
|
69 |
+
"python": {
|
70 |
+
"id": "python",
|
71 |
+
"name": "Python Programming",
|
72 |
+
"description": "Fundamentals of Python programming language",
|
73 |
+
"prerequisites": [],
|
74 |
+
"related": ["functions", "oop", "data_structures"]
|
75 |
+
},
|
76 |
+
"functions": {
|
77 |
+
"id": "functions",
|
78 |
+
"name": "Python Functions",
|
79 |
+
"description": "Creating and using functions in Python",
|
80 |
+
"prerequisites": ["python"],
|
81 |
+
"related": ["decorators", "lambdas"]
|
82 |
+
},
|
83 |
+
"oop": {
|
84 |
+
"id": "oop",
|
85 |
+
"name": "Object-Oriented Programming",
|
86 |
+
"description": "Classes and objects in Python",
|
87 |
+
"prerequisites": ["python"],
|
88 |
+
"related": ["inheritance", "polymorphism"]
|
89 |
+
},
|
90 |
+
"data_structures": {
|
91 |
+
"id": "data_structures",
|
92 |
+
"name": "Data Structures",
|
93 |
+
"description": "Built-in data structures in Python",
|
94 |
+
"prerequisites": ["python"],
|
95 |
+
"related": ["algorithms"]
|
96 |
+
},
|
97 |
+
"decorators": {
|
98 |
+
"id": "decorators",
|
99 |
+
"name": "Python Decorators",
|
100 |
+
"description": "Function decorators in Python",
|
101 |
+
"prerequisites": ["functions"],
|
102 |
+
"related": ["python", "functions"]
|
103 |
+
},
|
104 |
+
"lambdas": {
|
105 |
+
"id": "lambdas",
|
106 |
+
"name": "Lambda Functions",
|
107 |
+
"description": "Anonymous functions in Python",
|
108 |
+
"prerequisites": ["functions"],
|
109 |
+
"related": ["python", "functions"]
|
110 |
+
},
|
111 |
+
"inheritance": {
|
112 |
+
"id": "inheritance",
|
113 |
+
"name": "Inheritance in OOP",
|
114 |
+
"description": "Creating class hierarchies in Python",
|
115 |
+
"prerequisites": ["oop"],
|
116 |
+
"related": ["python", "oop"]
|
117 |
+
},
|
118 |
+
"polymorphism": {
|
119 |
+
"id": "polymorphism",
|
120 |
+
"name": "Polymorphism in OOP",
|
121 |
+
"description": "Multiple forms of methods in Python",
|
122 |
+
"prerequisites": ["oop"],
|
123 |
+
"related": ["python", "oop"]
|
124 |
+
},
|
125 |
+
"algorithms": {
|
126 |
+
"id": "algorithms",
|
127 |
+
"name": "Basic Algorithms",
|
128 |
+
"description": "Common algorithms in Python",
|
129 |
+
"prerequisites": ["data_structures"],
|
130 |
+
"related": ["python", "data_structures"]
|
131 |
+
}
|
132 |
+
}
|
133 |
+
|
134 |
+
@api_app.get("/api/concept_graph")
|
135 |
+
async def api_get_concept_graph(concept_id: str = None):
|
136 |
+
"""API endpoint to get concept graph data for a specific concept or all concepts"""
|
137 |
+
if concept_id:
|
138 |
+
concept = CONCEPT_GRAPH.get(concept_id)
|
139 |
+
if not concept:
|
140 |
+
return JSONResponse(
|
141 |
+
status_code=404,
|
142 |
+
content={"error": f"Concept {concept_id} not found"}
|
143 |
+
)
|
144 |
+
return JSONResponse(content=concept)
|
145 |
+
return JSONResponse(content={"concepts": list(CONCEPT_GRAPH.values())})
|
146 |
+
|
147 |
+
@mcp.tool()
|
148 |
+
async def get_concept(concept_id: str = None) -> Dict[str, Any]:
|
149 |
+
"""MCP tool to get a specific concept or all concepts"""
|
150 |
+
if concept_id:
|
151 |
+
concept = CONCEPT_GRAPH.get(concept_id)
|
152 |
+
if not concept:
|
153 |
+
return {"error": f"Concept {concept_id} not found"}
|
154 |
+
return {"concept": concept}
|
155 |
+
return {"concepts": list(CONCEPT_GRAPH.values())}
|
156 |
+
|
157 |
@mcp.tool()
|
158 |
async def assess_skill(student_id: str, concept_id: str) -> Dict[str, Any]:
|
159 |
+
"""Assess a student's understanding of a specific concept"""
|
160 |
+
# Check if concept exists in our concept graph
|
161 |
+
concept_data = await get_concept(concept_id)
|
162 |
+
if isinstance(concept_data, dict) and "error" in concept_data:
|
163 |
+
return {"error": f"Cannot assess skill: {concept_data['error']}"}
|
164 |
|
165 |
+
# Get concept name, handling both direct dict and concept graph response
|
166 |
+
if isinstance(concept_data, dict) and "concept" in concept_data:
|
167 |
+
concept_name = concept_data["concept"].get("name", concept_id)
|
168 |
+
elif isinstance(concept_data, dict) and "name" in concept_data:
|
169 |
+
concept_name = concept_data["name"]
|
170 |
+
else:
|
171 |
+
concept_name = concept_id
|
172 |
|
173 |
+
# Generate a score based on concept difficulty or random
|
174 |
+
score = random.uniform(0.2, 1.0) # Random score between 0.2 and 1.0
|
175 |
+
|
176 |
+
# Set timestamp with timezone
|
177 |
+
timestamp = datetime.now(timezone.utc).isoformat()
|
178 |
+
|
179 |
+
# Generate feedback based on score
|
180 |
+
feedback = {
|
181 |
+
"strengths": [f"Good understanding of {concept_name} fundamentals"],
|
182 |
+
"areas_for_improvement": [f"Could work on advanced applications of {concept_name}"],
|
183 |
+
"recommendations": [
|
184 |
+
f"Review {concept_name} practice problems",
|
185 |
+
f"Watch tutorial videos on {concept_name}"
|
186 |
+
]
|
187 |
+
}
|
188 |
+
|
189 |
+
# Adjust feedback based on score
|
190 |
+
if score < 0.5:
|
191 |
+
feedback["strengths"] = [f"Basic understanding of {concept_name}"]
|
192 |
+
feedback["areas_for_improvement"].append("Needs to review fundamental concepts")
|
193 |
+
elif score > 0.8:
|
194 |
+
feedback["strengths"].append(f"Excellent grasp of {concept_name} concepts")
|
195 |
+
feedback["recommendations"].append("Try more advanced problems")
|
196 |
+
|
197 |
+
# Create assessment response
|
198 |
+
assessment = {
|
199 |
+
"student_id": student_id,
|
200 |
+
"concept_id": concept_id,
|
201 |
+
"concept_name": concept_name,
|
202 |
+
"score": round(score, 2), # Round to 2 decimal places
|
203 |
+
"timestamp": timestamp,
|
204 |
+
"feedback": feedback
|
205 |
+
}
|
206 |
+
return assessment
|
207 |
|
208 |
@mcp.resource("concept-graph://")
|
209 |
+
async def get_concept_graph_resource() -> Dict[str, Any]:
|
210 |
"""Get the full knowledge concept graph"""
|
211 |
return {
|
212 |
"nodes": [
|
213 |
+
{"id": "python", "name": "Python Basics", "difficulty": 1, "type": "foundation"},
|
214 |
+
{"id": "functions", "name": "Functions", "difficulty": 2, "type": "concept"},
|
215 |
+
{"id": "oop", "name": "OOP in Python", "difficulty": 3, "type": "paradigm"},
|
216 |
+
{"id": "data_structures", "name": "Data Structures", "difficulty": 2, "type": "concept"},
|
217 |
+
{"id": "decorators", "name": "Decorators", "difficulty": 4, "type": "advanced"},
|
218 |
+
{"id": "lambdas", "name": "Lambda Functions", "difficulty": 2, "type": "concept"},
|
219 |
+
{"id": "inheritance", "name": "Inheritance", "difficulty": 3, "type": "oop"},
|
220 |
+
{"id": "polymorphism", "name": "Polymorphism", "difficulty": 3, "type": "oop"},
|
221 |
+
{"id": "algorithms", "name": "Algorithms", "difficulty": 3, "type": "concept"}
|
222 |
],
|
223 |
"edges": [
|
224 |
+
{"from": "python", "to": "functions", "weight": 0.9},
|
225 |
+
{"from": "python", "to": "oop", "weight": 0.8},
|
226 |
+
{"from": "python", "to": "data_structures", "weight": 0.9},
|
227 |
+
{"from": "functions", "to": "decorators", "weight": 0.8},
|
228 |
+
{"from": "functions", "to": "lambdas", "weight": 0.7},
|
229 |
+
{"from": "oop", "to": "inheritance", "weight": 0.9},
|
230 |
+
{"from": "oop", "to": "polymorphism", "weight": 0.8},
|
231 |
+
{"from": "data_structures", "to": "algorithms", "weight": 0.9}
|
232 |
]
|
233 |
}
|
234 |
|
|
|
237 |
"""Get personalized learning path for a student"""
|
238 |
return {
|
239 |
"student_id": student_id,
|
240 |
+
"current_concepts": ["math_algebra_linear_equations"]
|
241 |
+
}
|
242 |
+
|
243 |
+
# Lesson Generation
|
244 |
+
@mcp.tool()
|
245 |
+
async def generate_lesson(topic: str, grade_level: int, duration_minutes: int) -> Dict[str, Any]:
|
246 |
+
"""
|
247 |
+
Generate a lesson plan for the given topic, grade level, and duration
|
248 |
+
|
249 |
+
Args:
|
250 |
+
topic: The topic for the lesson
|
251 |
+
grade_level: The grade level (1-12)
|
252 |
+
duration_minutes: Duration of the lesson in minutes
|
253 |
+
|
254 |
+
Returns:
|
255 |
+
Dictionary containing the generated lesson plan
|
256 |
+
"""
|
257 |
+
# In a real implementation, this would generate a lesson plan using an LLM
|
258 |
+
# For now, we'll return a mock lesson plan
|
259 |
+
return {
|
260 |
+
"lesson_id": f"lesson_{int(datetime.utcnow().timestamp())}",
|
261 |
+
"topic": topic,
|
262 |
+
"grade_level": grade_level,
|
263 |
+
"duration_minutes": duration_minutes,
|
264 |
+
"objectives": [
|
265 |
+
f"Understand the key concepts of {topic}",
|
266 |
+
f"Apply {topic} to solve problems",
|
267 |
+
f"Analyze examples of {topic} in real-world contexts"
|
268 |
+
],
|
269 |
+
"materials": ["Whiteboard", "Markers", "Printed worksheets"],
|
270 |
+
"activities": [
|
271 |
+
{
|
272 |
+
"name": "Introduction",
|
273 |
+
"duration": 5,
|
274 |
+
"description": f"Brief introduction to {topic} and its importance"
|
275 |
+
},
|
276 |
+
{
|
277 |
+
"name": "Direct Instruction",
|
278 |
+
"duration": 15,
|
279 |
+
"description": f"Explain the main concepts of {topic} with examples"
|
280 |
+
},
|
281 |
+
{
|
282 |
+
"name": "Guided Practice",
|
283 |
+
"duration": 15,
|
284 |
+
"description": "Work through example problems together"
|
285 |
+
},
|
286 |
+
{
|
287 |
+
"name": "Independent Practice",
|
288 |
+
"duration": 10,
|
289 |
+
"description": "Students work on problems independently"
|
290 |
+
}
|
291 |
+
],
|
292 |
+
"assessment": {
|
293 |
+
"type": "formative",
|
294 |
+
"description": "Exit ticket with 2-3 problems related to the lesson"
|
295 |
+
},
|
296 |
+
"timestamp": datetime.utcnow().isoformat()
|
297 |
}
|
298 |
|
299 |
# Assessment Suite
|
|
|
309 |
Returns:
|
310 |
Quiz object with questions and answers
|
311 |
"""
|
312 |
+
# In a real implementation, this would generate questions based on the concepts
|
313 |
+
# For now, we'll return a mock quiz
|
314 |
+
questions = []
|
315 |
+
for i, concept_id in enumerate(concept_ids[:5]): # Limit to 5 questions max
|
316 |
+
concept = CONCEPT_GRAPH.get(concept_id, {"name": f"Concept {concept_id}"})
|
317 |
+
questions.append({
|
318 |
+
"id": f"q{i+1}",
|
319 |
+
"concept_id": concept_id,
|
320 |
+
"concept_name": concept.get("name", f"Concept {concept_id}"),
|
321 |
+
"question": f"Sample question about {concept.get('name', concept_id)}?",
|
322 |
+
"options": ["Option 1", "Option 2", "Option 3", "Option 4"],
|
323 |
+
"correct_answer": random.randint(0, 3), # Random correct answer index
|
324 |
+
"difficulty": min(max(1, difficulty), 5), # Clamp difficulty between 1-5
|
325 |
+
"explanation": f"This is an explanation for the question about {concept.get('name', concept_id)}"
|
326 |
+
})
|
327 |
+
|
328 |
return {
|
329 |
+
"quiz_id": f"quiz_{int(datetime.utcnow().timestamp())}",
|
330 |
"concept_ids": concept_ids,
|
331 |
"difficulty": difficulty,
|
332 |
+
"questions": questions,
|
333 |
+
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
}
|
335 |
|
336 |
+
|
337 |
+
|
338 |
# API Endpoints
|
339 |
@api_app.get("/api/health")
|
340 |
async def health_check():
|
341 |
return {"status": "ok", "timestamp": datetime.now().isoformat()}
|
342 |
|
343 |
@api_app.get("/api/assess_skill")
|
344 |
+
async def assess_skill_api(
|
345 |
+
request: Request,
|
346 |
+
student_id: Optional[str] = Query(None, description="Student ID"),
|
347 |
+
concept_id: Optional[str] = Query(None, description="Concept ID to assess")
|
348 |
+
):
|
349 |
+
"""
|
350 |
+
Assess a student's understanding of a specific concept
|
351 |
+
|
352 |
+
Args:
|
353 |
+
student_id: Student's unique identifier
|
354 |
+
concept_id: Concept ID to assess
|
355 |
+
|
356 |
+
Returns:
|
357 |
+
Assessment results with score and feedback
|
358 |
+
"""
|
359 |
try:
|
360 |
+
# Get query parameters
|
361 |
+
params = dict(request.query_params)
|
362 |
+
|
363 |
+
# Check for required parameters
|
364 |
+
if not student_id or not concept_id:
|
365 |
+
raise HTTPException(
|
366 |
+
status_code=400,
|
367 |
+
detail="Both student_id and concept_id are required parameters"
|
368 |
+
)
|
369 |
+
|
370 |
+
# Call the assess_skill function
|
371 |
result = await assess_skill(student_id, concept_id)
|
372 |
+
|
373 |
+
# Handle error responses
|
374 |
+
if isinstance(result, dict) and "error" in result:
|
375 |
+
raise HTTPException(status_code=404, detail=result["error"])
|
376 |
+
|
377 |
return result
|
378 |
+
|
379 |
+
except HTTPException as http_err:
|
380 |
+
# Re-raise HTTP exceptions as is
|
381 |
+
raise http_err
|
382 |
except Exception as e:
|
383 |
+
# Log the error for debugging
|
384 |
+
print(f"Error in assess_skill_api: {str(e)}")
|
385 |
+
import traceback
|
386 |
+
traceback.print_exc()
|
387 |
+
|
388 |
+
# Return a user-friendly error message
|
389 |
+
raise HTTPException(
|
390 |
+
status_code=500,
|
391 |
+
detail=f"An error occurred while processing your request: {str(e)}"
|
392 |
+
)
|
393 |
+
|
394 |
+
@api_app.post("/api/generate_lesson")
|
395 |
+
async def generate_lesson_api(request: Dict[str, Any]):
|
396 |
+
"""
|
397 |
+
Generate a lesson plan based on the provided parameters
|
398 |
+
|
399 |
+
Expected request format:
|
400 |
+
{
|
401 |
+
"topic": "Lesson Topic",
|
402 |
+
"grade_level": 9, # 1-12
|
403 |
+
"duration_minutes": 45
|
404 |
+
}
|
405 |
+
"""
|
406 |
+
try:
|
407 |
+
# Validate request
|
408 |
+
if not isinstance(request, dict):
|
409 |
+
raise HTTPException(
|
410 |
+
status_code=400,
|
411 |
+
detail="Request must be a JSON object"
|
412 |
+
)
|
413 |
+
|
414 |
+
# Get parameters with validation
|
415 |
+
topic = request.get("topic")
|
416 |
+
if not topic or not isinstance(topic, str):
|
417 |
+
raise HTTPException(
|
418 |
+
status_code=400,
|
419 |
+
detail="Topic is required and must be a string"
|
420 |
+
)
|
421 |
+
|
422 |
+
grade_level = request.get("grade_level")
|
423 |
+
if not isinstance(grade_level, int) or not (1 <= grade_level <= 12):
|
424 |
+
raise HTTPException(
|
425 |
+
status_code=400,
|
426 |
+
detail="Grade level must be an integer between 1 and 12"
|
427 |
+
)
|
428 |
+
|
429 |
+
duration_minutes = request.get("duration_minutes")
|
430 |
+
if not isinstance(duration_minutes, (int, float)) or duration_minutes <= 0:
|
431 |
+
raise HTTPException(
|
432 |
+
status_code=400,
|
433 |
+
detail="Duration must be a positive number"
|
434 |
+
)
|
435 |
+
|
436 |
+
# Generate the lesson plan
|
437 |
+
result = await generate_lesson(topic, grade_level, int(duration_minutes))
|
438 |
+
return result
|
439 |
+
|
440 |
+
except HTTPException:
|
441 |
+
raise
|
442 |
+
except Exception as e:
|
443 |
+
raise HTTPException(status_code=500, detail=f"Failed to generate lesson: {str(e)}")
|
444 |
|
445 |
@api_app.post("/api/generate_quiz")
|
446 |
+
async def generate_quiz_api(request: Dict[str, Any]):
|
447 |
+
"""
|
448 |
+
Generate a quiz based on specified concepts and difficulty
|
449 |
+
|
450 |
+
Expected request format:
|
451 |
+
{
|
452 |
+
"concept_ids": ["concept1", "concept2", ...],
|
453 |
+
"difficulty": 2 # Optional, default is 2
|
454 |
+
}
|
455 |
+
"""
|
456 |
try:
|
457 |
+
# Validate request
|
458 |
+
if not isinstance(request, dict) or "concept_ids" not in request:
|
459 |
+
raise HTTPException(
|
460 |
+
status_code=400,
|
461 |
+
detail="Request must be a JSON object with 'concept_ids' key"
|
462 |
+
)
|
463 |
+
|
464 |
+
# Get parameters with defaults
|
465 |
+
concept_ids = request.get("concept_ids", [])
|
466 |
+
difficulty = request.get("difficulty", 2)
|
467 |
+
|
468 |
+
# Validate types
|
469 |
+
if not isinstance(concept_ids, list):
|
470 |
+
concept_ids = [concept_ids] # Convert single concept to list
|
471 |
+
|
472 |
+
if not all(isinstance(cid, str) for cid in concept_ids):
|
473 |
+
raise HTTPException(
|
474 |
+
status_code=400,
|
475 |
+
detail="All concept IDs must be strings"
|
476 |
+
)
|
477 |
+
|
478 |
+
difficulty = int(difficulty) # Ensure difficulty is an integer
|
479 |
+
|
480 |
+
# Generate the quiz
|
481 |
result = await generate_quiz(concept_ids, difficulty)
|
482 |
return result
|
483 |
+
|
484 |
+
except HTTPException:
|
485 |
+
raise
|
486 |
except Exception as e:
|
487 |
+
raise HTTPException(status_code=500, detail=f"Failed to generate quiz: {str(e)}")
|
488 |
+
|
489 |
+
@mcp.tool()
|
490 |
+
async def get_curriculum_standards(country_code: str = "us") -> Dict[str, Any]:
|
491 |
+
"""
|
492 |
+
Get curriculum standards for a specific country
|
493 |
+
|
494 |
+
Args:
|
495 |
+
country_code: ISO country code (e.g., 'us', 'uk')
|
496 |
+
|
497 |
+
Returns:
|
498 |
+
Dictionary containing curriculum standards
|
499 |
+
"""
|
500 |
+
# Mock data - in a real implementation, this would come from a database or external API
|
501 |
+
standards = {
|
502 |
+
"us": {
|
503 |
+
"name": "Common Core State Standards (US)",
|
504 |
+
"subjects": {
|
505 |
+
"math": {
|
506 |
+
"description": "Mathematics standards focusing on conceptual understanding, procedural skills, and problem solving",
|
507 |
+
"domains": ["Number & Operations", "Algebra", "Geometry", "Statistics & Probability"]
|
508 |
+
},
|
509 |
+
"ela": {
|
510 |
+
"description": "English Language Arts standards for reading, writing, speaking, and listening",
|
511 |
+
"domains": ["Reading", "Writing", "Speaking & Listening", "Language"]
|
512 |
+
}
|
513 |
+
},
|
514 |
+
"grade_levels": list(range(1, 13)),
|
515 |
+
"website": "http://www.corestandards.org"
|
516 |
+
},
|
517 |
+
"uk": {
|
518 |
+
"name": "National Curriculum (UK)",
|
519 |
+
"subjects": {
|
520 |
+
"maths": {
|
521 |
+
"description": "Mathematics programme of study for key stages 1-4",
|
522 |
+
"domains": ["Number", "Algebra", "Ratio & Proportion", "Geometry", "Statistics"]
|
523 |
+
},
|
524 |
+
"english": {
|
525 |
+
"description": "English programme of study for key stages 1-4",
|
526 |
+
"domains": ["Reading", "Writing", "Grammar & Vocabulary", "Spoken English"]
|
527 |
+
}
|
528 |
+
},
|
529 |
+
"key_stages": ["KS1 (5-7)", "KS2 (7-11)", "KS3 (11-14)", "KS4 (14-16)"],
|
530 |
+
"website": "https://www.gov.uk/government/collections/national-curriculum"
|
531 |
+
}
|
532 |
+
}
|
533 |
+
|
534 |
+
# Default to US standards if country not found
|
535 |
+
country_code = country_code.lower()
|
536 |
+
if country_code not in standards:
|
537 |
+
country_code = "us"
|
538 |
+
|
539 |
+
return {
|
540 |
+
"country_code": country_code,
|
541 |
+
"standards": standards[country_code],
|
542 |
+
"timestamp": datetime.utcnow().isoformat()
|
543 |
+
}
|
544 |
+
|
545 |
+
@api_app.get("/api/curriculum-standards")
|
546 |
+
async def get_curriculum_standards_api(country: str = "us"):
|
547 |
+
"""
|
548 |
+
Get curriculum standards for a specific country
|
549 |
+
|
550 |
+
Args:
|
551 |
+
country: ISO country code (e.g., 'us', 'uk')
|
552 |
+
|
553 |
+
Returns:
|
554 |
+
Dictionary containing curriculum standards
|
555 |
+
"""
|
556 |
+
try:
|
557 |
+
# Validate country code
|
558 |
+
if not isinstance(country, str) or len(country) != 2:
|
559 |
+
raise HTTPException(
|
560 |
+
status_code=400,
|
561 |
+
detail="Country code must be a 2-letter ISO code"
|
562 |
+
)
|
563 |
+
|
564 |
+
# Get the standards
|
565 |
+
result = await get_curriculum_standards(country)
|
566 |
+
return result
|
567 |
+
|
568 |
+
except HTTPException:
|
569 |
+
raise
|
570 |
+
except Exception as e:
|
571 |
+
raise HTTPException(
|
572 |
+
status_code=500,
|
573 |
+
detail=f"Failed to fetch curriculum standards: {str(e)}"
|
574 |
+
)
|
575 |
|
576 |
# Mount MCP app to /mcp path
|
577 |
mcp.app = api_app
|
requirements.txt
CHANGED
@@ -16,4 +16,6 @@ pytest-cov>=3.0.0
|
|
16 |
black>=22.0.0
|
17 |
isort>=5.10.0
|
18 |
mypy>=0.910
|
19 |
-
ruff>=0.0.262
|
|
|
|
|
|
16 |
black>=22.0.0
|
17 |
isort>=5.10.0
|
18 |
mypy>=0.910
|
19 |
+
ruff>=0.0.262
|
20 |
+
networkx>=3.0
|
21 |
+
matplotlib>=3.5.0
|
run.py
CHANGED
@@ -16,7 +16,7 @@ def load_module(name, path):
|
|
16 |
spec.loader.exec_module(module)
|
17 |
return module
|
18 |
|
19 |
-
def run_mcp_server(host="
|
20 |
"""Run the MCP server with specified configuration"""
|
21 |
print(f"Starting TutorX MCP Server on {host}:{port} using {transport} transport...")
|
22 |
|
@@ -25,6 +25,7 @@ def run_mcp_server(host="127.0.0.1", port=8000, transport="streamable-http"):
|
|
25 |
os.environ["MCP_PORT"] = str(port)
|
26 |
os.environ["MCP_TRANSPORT"] = transport
|
27 |
|
|
|
28 |
main_module = load_module("main", "main.py")
|
29 |
|
30 |
# Access the mcp instance and run it
|
@@ -41,7 +42,7 @@ def run_gradio_interface():
|
|
41 |
|
42 |
# Run the Gradio demo
|
43 |
if hasattr(app_module, "demo"):
|
44 |
-
app_module.demo.launch()
|
45 |
else:
|
46 |
print("Error: Gradio demo not found in app.py")
|
47 |
sys.exit(1)
|
@@ -62,25 +63,30 @@ if __name__ == "__main__":
|
|
62 |
parser.add_argument(
|
63 |
"--mode",
|
64 |
choices=["mcp", "gradio", "both"],
|
65 |
-
default="
|
66 |
help="Run mode: 'mcp' for MCP server, 'gradio' for Gradio interface, 'both' for both"
|
67 |
)
|
68 |
parser.add_argument(
|
69 |
"--host",
|
70 |
-
default="
|
71 |
help="Host address to use"
|
72 |
)
|
73 |
parser.add_argument(
|
74 |
"--port",
|
75 |
type=int,
|
76 |
-
default=
|
77 |
-
help="Port to use"
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
)
|
79 |
parser.add_argument(
|
80 |
"--transport",
|
81 |
-
|
82 |
-
|
83 |
-
help="Transport protocol to use"
|
84 |
)
|
85 |
|
86 |
args = parser.parse_args()
|
|
|
16 |
spec.loader.exec_module(module)
|
17 |
return module
|
18 |
|
19 |
+
def run_mcp_server(host="0.0.0.0", port=8001, transport="http"):
|
20 |
"""Run the MCP server with specified configuration"""
|
21 |
print(f"Starting TutorX MCP Server on {host}:{port} using {transport} transport...")
|
22 |
|
|
|
25 |
os.environ["MCP_PORT"] = str(port)
|
26 |
os.environ["MCP_TRANSPORT"] = transport
|
27 |
|
28 |
+
# Import and run the main module
|
29 |
main_module = load_module("main", "main.py")
|
30 |
|
31 |
# Access the mcp instance and run it
|
|
|
42 |
|
43 |
# Run the Gradio demo
|
44 |
if hasattr(app_module, "demo"):
|
45 |
+
app_module.demo.launch(server_name="0.0.0.0", server_port=7860)
|
46 |
else:
|
47 |
print("Error: Gradio demo not found in app.py")
|
48 |
sys.exit(1)
|
|
|
63 |
parser.add_argument(
|
64 |
"--mode",
|
65 |
choices=["mcp", "gradio", "both"],
|
66 |
+
default="both",
|
67 |
help="Run mode: 'mcp' for MCP server, 'gradio' for Gradio interface, 'both' for both"
|
68 |
)
|
69 |
parser.add_argument(
|
70 |
"--host",
|
71 |
+
default="0.0.0.0",
|
72 |
help="Host address to use"
|
73 |
)
|
74 |
parser.add_argument(
|
75 |
"--port",
|
76 |
type=int,
|
77 |
+
default=8001,
|
78 |
+
help="Port to use for MCP server (default: 8001)"
|
79 |
+
)
|
80 |
+
parser.add_argument(
|
81 |
+
"--gradio-port",
|
82 |
+
type=int,
|
83 |
+
default=7860,
|
84 |
+
help="Port to use for Gradio interface (default: 7860)"
|
85 |
)
|
86 |
parser.add_argument(
|
87 |
"--transport",
|
88 |
+
default="http",
|
89 |
+
help="Transport protocol to use (default: http)"
|
|
|
90 |
)
|
91 |
|
92 |
args = parser.parse_args()
|