Spaces:
Running
Running
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()
|