Meet Patel commited on
Commit
bbd9cd6
·
1 Parent(s): d4df2a7

Core and Advanced Features is working with mock data.

Browse files
Files changed (5) hide show
  1. app.py +222 -56
  2. client.py +131 -19
  3. main.py +456 -56
  4. requirements.txt +3 -1
  5. 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.Markdown("## Adaptive Learning Engine")
74
-
75
- with gr.Row():
76
- with gr.Column():
77
- concept_id_input = gr.Dropdown(
78
- choices=["math_algebra_basics", "math_algebra_linear_equations", "math_algebra_quadratic_equations"],
79
- label="Select Concept",
80
- value="math_algebra_linear_equations"
81
- )
82
- assess_btn = gr.Button("Assess Skill")
83
-
84
- with gr.Column():
85
- assessment_output = gr.JSON(label="Skill Assessment")
86
- async def on_assess_click(concept_id):
87
- result = await api_request("assess_skill", "GET", {"student_id": "student_12345", "concept_id": concept_id})
88
- return result
 
 
 
 
 
 
 
 
 
89
 
90
- assess_btn.click(
91
- fn=on_assess_click,
92
- inputs=[concept_id_input],
93
- outputs=[assessment_output]
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
- concept_graph_btn.click(
105
- fn=on_concept_graph_click,
106
- inputs=[],
107
- outputs=[concept_graph_output]
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={"concept_ids": concepts, "difficulty": difficulty}
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=lambda x, y, z: asyncio.run(client.generate_lesson(x, y, z)),
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=["us", "uk"],
163
  label="Country",
164
- value="us"
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=lambda x: asyncio.run(client.get_curriculum_standards(x)),
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=lambda x: asyncio.run(client.text_interaction(x, "student_12345")),
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=lambda x: asyncio.run(client.handwriting_recognition(image_to_base64(x), "student_12345")),
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 Error Analysis",
227
  value="math_algebra_linear_equations"
228
  )
229
- error_btn = gr.Button("Analyze Errors")
230
- error_output = gr.JSON(label="Error Pattern Analysis")
231
 
 
 
 
232
  error_btn.click(
233
- fn=lambda x: asyncio.run(client.analyze_error_patterns("student_12345", x)),
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=lambda x, y: asyncio.run(client.check_submission_originality(x, [y])),
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
- async with self.session.get(url, params=params, timeout=30) as response:
55
- if response.status == 200:
56
- return await response.json()
57
- else:
58
- error = await response.text()
59
- return {
60
- "error": f"API error ({response.status}): {error}",
61
- "timestamp": datetime.now().isoformat()
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 level on a specific concept"""
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
- # Adaptive Learning Engine
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  @mcp.tool()
66
  async def assess_skill(student_id: str, concept_id: str) -> Dict[str, Any]:
67
- """
68
- Assess student's skill level on a specific concept
 
 
 
69
 
70
- Args:
71
- student_id: The unique identifier for the student
72
- concept_id: The concept to assess
 
 
 
 
73
 
74
- Returns:
75
- Dictionary containing skill level and recommendations
76
- """
77
- try:
78
- # Simulated skill assessment
79
- return {
80
- "student_id": student_id,
81
- "concept_id": concept_id,
82
- "skill_level": 0.75,
83
- "confidence": 0.85,
84
- "recommendations": [
85
- "Practice more complex problems",
86
- "Review related concept: algebra_linear_equations"
87
- ],
88
- "timestamp": datetime.now().isoformat()
89
- }
90
- except Exception as e:
91
- return {
92
- "error": str(e),
93
- "timestamp": datetime.now().isoformat()
94
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  @mcp.resource("concept-graph://")
97
- async def get_concept_graph() -> Dict[str, Any]:
98
  """Get the full knowledge concept graph"""
99
  return {
100
  "nodes": [
101
- {"id": "math_algebra_basics", "name": "Algebra Basics", "difficulty": 1},
102
- {"id": "math_algebra_linear_equations", "name": "Linear Equations", "difficulty": 2},
103
- {"id": "math_algebra_quadratic_equations", "name": "Quadratic Equations", "difficulty": 3},
 
 
 
 
 
 
104
  ],
105
  "edges": [
106
- {"from": "math_algebra_basics", "to": "math_algebra_linear_equations", "weight": 1.0},
107
- {"from": "math_algebra_linear_equations", "to": "math_algebra_quadratic_equations", "weight": 0.8},
 
 
 
 
 
 
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
- "recommended_next": ["math_algebra_quadratic_equations"],
118
- "mastered": ["math_algebra_basics"],
119
- "estimated_completion_time": "2 weeks"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "q12345",
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(student_id: str = Query(...), concept_id: str = Query(...)):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  try:
 
 
 
 
 
 
 
 
 
 
 
163
  result = await assess_skill(student_id, concept_id)
 
 
 
 
 
164
  return result
 
 
 
 
165
  except Exception as e:
166
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  @api_app.post("/api/generate_quiz")
169
- async def generate_quiz_api(concept_ids: List[str], difficulty: int = 2):
 
 
 
 
 
 
 
 
 
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="127.0.0.1", port=8000, transport="streamable-http"):
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="mcp",
66
  help="Run mode: 'mcp' for MCP server, 'gradio' for Gradio interface, 'both' for both"
67
  )
68
  parser.add_argument(
69
  "--host",
70
- default="127.0.0.1",
71
  help="Host address to use"
72
  )
73
  parser.add_argument(
74
  "--port",
75
  type=int,
76
- default=8000,
77
- help="Port to use"
 
 
 
 
 
 
78
  )
79
  parser.add_argument(
80
  "--transport",
81
- choices=["stdio", "streamable-http", "sse"],
82
- default="streamable-http",
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()