Meet Patel commited on
Commit
411f252
·
1 Parent(s): f9f5b1d

Added external tool integration,also with test files and documentation

Browse files
README.md CHANGED
@@ -127,10 +127,15 @@ tutorx-mcp/
127
  ├── client.py # MCP client for calling server tools
128
  ├── app.py # Gradio web interface
129
  ├── run.py # Runner script for different modes
 
 
 
 
130
  ├── utils/ # Utility modules
131
  │ ├── multimodal.py # Multi-modal processing utilities
132
  │ └── assessment.py # Assessment and analytics functions
133
  ├── pyproject.toml # Project dependencies
 
134
  └── README.md # Project documentation
135
  ```
136
 
@@ -149,11 +154,55 @@ This separation of concerns allows:
149
  - The web interface to interact with the server using standard HTTP
150
  - Clear boundaries between presentation, business logic, and tool implementation
151
 
152
- ## License
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- This project is licensed under the MIT License - see the LICENSE file for details.
 
 
 
 
 
 
 
 
 
 
155
 
156
- ## Acknowledgments
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- - Model Context Protocol (MCP) - https://modelcontextprotocol.io/
159
- - Gradio - https://gradio.app/
 
127
  ├── client.py # MCP client for calling server tools
128
  ├── app.py # Gradio web interface
129
  ├── run.py # Runner script for different modes
130
+ ├── tests/ # Test suite
131
+ │ ├── test_mcp_server.py # MCP server tests
132
+ │ ├── test_client.py # Client tests
133
+ │ └── test_utils.py # Utility function tests
134
  ├── utils/ # Utility modules
135
  │ ├── multimodal.py # Multi-modal processing utilities
136
  │ └── assessment.py # Assessment and analytics functions
137
  ├── pyproject.toml # Project dependencies
138
+ ├── run_tests.py # Script to run all tests
139
  └── README.md # Project documentation
140
  ```
141
 
 
154
  - The web interface to interact with the server using standard HTTP
155
  - Clear boundaries between presentation, business logic, and tool implementation
156
 
157
+ ## Testing
158
+
159
+ The project includes a comprehensive test suite:
160
+
161
+ ```bash
162
+ # Install test dependencies
163
+ uv install -e ".[test]"
164
+
165
+ # Run test suite
166
+ python run_tests.py
167
+ ```
168
+
169
+ ### Integration with External Systems
170
+
171
+ TutorX-MCP can integrate with various external educational systems:
172
 
173
+ 1. **Learning Management Systems (LMS)**
174
+ - Canvas, Moodle, Blackboard
175
+ - Grade syncing and assignment management
176
+
177
+ 2. **Open Educational Resources (OER)**
178
+ - Search and integration with OER repositories
179
+ - Access to diverse educational content
180
+
181
+ 3. **Real-time Personalized Tutoring Platforms**
182
+ - Schedule and manage tutoring sessions
183
+ - Connect students with expert tutors
184
 
185
+ ## Deployment
186
+
187
+ For production deployment, see [Deployment Guide](docs/deployment.md) which covers:
188
+
189
+ - Docker-based deployment
190
+ - Manual installation
191
+ - Scaling strategies
192
+ - Monitoring setup
193
+ - Security considerations
194
+
195
+ ## Documentation
196
+
197
+ - [API Documentation](docs/api.md): Complete API reference for developers
198
+ - [MCP Protocol](docs/mcp.md): Details about the Model Context Protocol
199
+ - [Product Requirements](docs/prd.md): Original requirements document
200
+ - [SDK Documentation](docs/sdk.md): Client SDK usage
201
+
202
+ ## Contributing
203
+
204
+ We welcome contributions to the TutorX-MCP project! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
205
+
206
+ ## License
207
 
208
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
 
app.py CHANGED
@@ -240,6 +240,102 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
240
  inputs=[submission_input, reference_input],
241
  outputs=[plagiarism_output]
242
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  # Launch the app
245
  if __name__ == "__main__":
 
240
  inputs=[submission_input, reference_input],
241
  outputs=[plagiarism_output]
242
  )
243
+
244
+ # Tab 6: Gamification
245
+ with gr.Tab("Gamification"):
246
+ gr.Markdown("## Student Badges")
247
+
248
+ with gr.Row():
249
+ with gr.Column():
250
+ badges_student_id = gr.Textbox(label="Student ID", value=student_id)
251
+ get_badges_btn = gr.Button("Get Student Badges")
252
+
253
+ with gr.Column():
254
+ badges_output = gr.JSON(label="Student Badges")
255
+
256
+ get_badges_btn.click(
257
+ fn=lambda sid: client.get_badges_for_student(sid),
258
+ inputs=[badges_student_id],
259
+ outputs=[badges_output]
260
+ )
261
+
262
+ gr.Markdown("## Award Badge")
263
+
264
+ with gr.Row():
265
+ with gr.Column():
266
+ award_student_id = gr.Textbox(label="Student ID", value=student_id)
267
+ badge_id = gr.Dropdown(
268
+ choices=["beginner", "persistent", "math_whiz", "science_explorer",
269
+ "speed_demon", "accuracy_master", "helping_hand",
270
+ "night_owl", "early_bird", "perfect_streak"],
271
+ label="Badge to Award",
272
+ value="beginner"
273
+ )
274
+ award_badge_btn = gr.Button("Award Badge")
275
+
276
+ with gr.Column():
277
+ award_output = gr.JSON(label="Award Result")
278
+
279
+ award_badge_btn.click(
280
+ fn=lambda sid, bid: client.award_student_badge(sid, bid),
281
+ inputs=[award_student_id, badge_id],
282
+ outputs=[award_output]
283
+ )
284
+
285
+ gr.Markdown("## Leaderboards")
286
+
287
+ with gr.Row():
288
+ with gr.Column():
289
+ leaderboard_id = gr.Dropdown(
290
+ choices=["weekly_points", "monthly_streak", "problem_solving_speed"],
291
+ label="Leaderboard",
292
+ value="weekly_points"
293
+ )
294
+ get_leaderboard_btn = gr.Button("Get Leaderboard")
295
+
296
+ with gr.Column():
297
+ leaderboard_output = gr.JSON(label="Leaderboard")
298
+
299
+ get_leaderboard_btn.click(
300
+ fn=lambda lid: client.get_current_leaderboard(lid),
301
+ inputs=[leaderboard_id],
302
+ outputs=[leaderboard_output]
303
+ )
304
+
305
+ gr.Markdown("## Track Activity")
306
+
307
+ with gr.Row():
308
+ with gr.Column():
309
+ track_student_id = gr.Textbox(label="Student ID", value=student_id)
310
+ activity_type = gr.Dropdown(
311
+ choices=["lesson_completed", "assessment_completed", "problem_solved", "forum_post", "login"],
312
+ label="Activity Type",
313
+ value="lesson_completed"
314
+ )
315
+ activity_score = gr.Slider(minimum=0, maximum=1, value=0.85, step=0.01, label="Score/Performance")
316
+ activity_subject = gr.Dropdown(
317
+ choices=["math", "science", "language", "history", "other"],
318
+ label="Subject",
319
+ value="math"
320
+ )
321
+ track_btn = gr.Button("Track Activity")
322
+
323
+ with gr.Column():
324
+ activity_output = gr.JSON(label="Activity Tracking Result")
325
+
326
+ track_btn.click(
327
+ fn=lambda sid, atype, score, subject: client.track_student_activity(
328
+ sid,
329
+ {
330
+ "activity_type": atype,
331
+ "score": score,
332
+ "subject": subject,
333
+ "time_seconds": 90 # Simulated time value
334
+ }
335
+ ),
336
+ inputs=[track_student_id, activity_type, activity_score, activity_subject],
337
+ outputs=[activity_output]
338
+ )
339
 
340
  # Launch the app
341
  if __name__ == "__main__":
client.py CHANGED
@@ -198,6 +198,89 @@ class TutorXClient:
198
  "submission": submission,
199
  "reference_sources": reference_sources
200
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  # Create a default client instance for easy import
203
  client = TutorXClient()
 
198
  "submission": submission,
199
  "reference_sources": reference_sources
200
  })
201
+
202
+ # ------------ Gamification ------------
203
+
204
+ def award_student_badge(self, student_id: str, badge_id: str) -> Dict[str, Any]:
205
+ """Award a badge to a student"""
206
+ return self._call_tool("award_student_badge", {
207
+ "student_id": student_id,
208
+ "badge_id": badge_id
209
+ })
210
+
211
+ def get_badges_for_student(self, student_id: str) -> Dict[str, Any]:
212
+ """Get all badges for a student"""
213
+ return self._call_tool("get_badges_for_student", {
214
+ "student_id": student_id
215
+ })
216
+
217
+ def update_student_leaderboard(self, leaderboard_id: str, student_id: str, score: float) -> Dict[str, Any]:
218
+ """Update a leaderboard with a student's score"""
219
+ return self._call_tool("update_student_leaderboard", {
220
+ "leaderboard_id": leaderboard_id,
221
+ "student_id": student_id,
222
+ "score": score
223
+ })
224
+
225
+ def get_current_leaderboard(self, leaderboard_id: str) -> Dict[str, Any]:
226
+ """Get current leaderboard standings"""
227
+ return self._call_tool("get_current_leaderboard", {
228
+ "leaderboard_id": leaderboard_id
229
+ })
230
+
231
+ def track_student_activity(self, student_id: str, activity_data: Dict[str, Any]) -> Dict[str, Any]:
232
+ """Track a student's activity and check for achievements"""
233
+ return self._call_tool("track_student_activity", {
234
+ "student_id": student_id,
235
+ "activity_data": activity_data
236
+ })
237
+
238
+ # ------------ External Integrations ------------
239
+
240
+ def lms_sync_grades(self, lms_type: str, api_url: str, api_key: str,
241
+ course_id: str, assignment_id: str,
242
+ grades: List[Dict[str, Any]]) -> Dict[str, Any]:
243
+ """Sync grades with a Learning Management System"""
244
+ return self._call_tool("lms_sync_grades", {
245
+ "lms_type": lms_type,
246
+ "api_url": api_url,
247
+ "api_key": api_key,
248
+ "course_id": course_id,
249
+ "assignment_id": assignment_id,
250
+ "grades": grades
251
+ })
252
+
253
+ def oer_search(self, repository_url: str, query: str,
254
+ subject: Optional[str] = None, grade_level: Optional[str] = None,
255
+ api_key: Optional[str] = None) -> Dict[str, Any]:
256
+ """Search for educational resources in OER repositories"""
257
+ params = {
258
+ "repository_url": repository_url,
259
+ "query": query
260
+ }
261
+
262
+ if subject:
263
+ params["subject"] = subject
264
+
265
+ if grade_level:
266
+ params["grade_level"] = grade_level
267
+
268
+ if api_key:
269
+ params["api_key"] = api_key
270
+
271
+ return self._call_tool("oer_search", params)
272
+
273
+ def schedule_tutoring_session(self, platform_url: str, client_id: str, client_secret: str,
274
+ student_id: str, subject: str, datetime_str: str) -> Dict[str, Any]:
275
+ """Schedule a session with a real-time personalized tutoring platform"""
276
+ return self._call_tool("schedule_tutoring_session", {
277
+ "platform_url": platform_url,
278
+ "client_id": client_id,
279
+ "client_secret": client_secret,
280
+ "student_id": student_id,
281
+ "subject": subject,
282
+ "datetime_str": datetime_str
283
+ })
284
 
285
  # Create a default client instance for easy import
286
  client = TutorXClient()
deployment/Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY pyproject.toml .
7
+ RUN pip install --no-cache-dir uv && \
8
+ uv pip install --no-cache -e .
9
+
10
+ # Copy application code
11
+ COPY main.py .
12
+ COPY client.py .
13
+ COPY utils/ ./utils/
14
+
15
+ # Expose port for MCP server
16
+ EXPOSE 8000
17
+
18
+ # Run MCP server
19
+ CMD ["python", "main.py"]
deployment/Dockerfile.web ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY pyproject.toml .
7
+ RUN pip install --no-cache-dir uv && \
8
+ uv pip install --no-cache -e .
9
+
10
+ # Copy application code
11
+ COPY app.py .
12
+ COPY client.py .
13
+ COPY utils/ ./utils/
14
+
15
+ # Expose port for Gradio interface
16
+ EXPOSE 7860
17
+
18
+ # Run Gradio interface
19
+ CMD ["python", "app.py"]
deployment/docker-compose.yml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3'
2
+
3
+ services:
4
+ tutorx-mcp:
5
+ build:
6
+ context: ..
7
+ dockerfile: deployment/Dockerfile
8
+ ports:
9
+ - "8000:8000" # MCP server
10
+ environment:
11
+ - MCP_HOST=0.0.0.0
12
+ - MCP_PORT=8000
13
+ - LOG_LEVEL=INFO
14
+ - REDIS_HOST=redis
15
+ volumes:
16
+ - tutorx-data:/data
17
+ depends_on:
18
+ - redis
19
+ restart: unless-stopped
20
+
21
+ tutorx-web:
22
+ build:
23
+ context: ..
24
+ dockerfile: deployment/Dockerfile.web
25
+ ports:
26
+ - "7860:7860" # Gradio interface
27
+ environment:
28
+ - GRADIO_SERVER_NAME=0.0.0.0
29
+ - GRADIO_SERVER_PORT=7860
30
+ - MCP_SERVER_URL=http://tutorx-mcp:8000
31
+ depends_on:
32
+ - tutorx-mcp
33
+ restart: unless-stopped
34
+
35
+ redis:
36
+ image: redis:alpine
37
+ ports:
38
+ - "6379:6379"
39
+ volumes:
40
+ - redis-data:/data
41
+ command: redis-server --appendonly yes
42
+ restart: unless-stopped
43
+
44
+ volumes:
45
+ tutorx-data:
46
+ redis-data:
docs/api.md ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TutorX-MCP API Documentation
2
+
3
+ This document provides comprehensive documentation for the TutorX-MCP API for developers who want to integrate with the system.
4
+
5
+ ## API Overview
6
+
7
+ The TutorX-MCP server exposes a Model Context Protocol (MCP) API that allows clients to interact with various educational tools and resources. This API follows the standard MCP protocol as defined in [MCP Specification](https://github.com/anthropics/anthropic-tools/blob/main/mcp/README.md).
8
+
9
+ ## Authentication
10
+
11
+ For production deployments, all API requests should include an API key in the `Authorization` header:
12
+
13
+ ```
14
+ Authorization: Bearer your-api-key-here
15
+ ```
16
+
17
+ ## Base URL
18
+
19
+ Default: `http://localhost:8000`
20
+
21
+ For production: See your deployment configuration
22
+
23
+ ## Tools API
24
+
25
+ Tools represent functionality that can be invoked by MCP clients. Each tool is accessed via:
26
+
27
+ ```
28
+ POST /tools/{tool_name}
29
+ Content-Type: application/json
30
+
31
+ {
32
+ "param1": "value1",
33
+ "param2": "value2"
34
+ }
35
+ ```
36
+
37
+ ### Core Features
38
+
39
+ #### Adaptive Learning Engine
40
+
41
+ ##### `assess_skill`
42
+
43
+ Assess a student's skill level on a specific concept.
44
+
45
+ **Request:**
46
+ ```json
47
+ {
48
+ "student_id": "student123",
49
+ "concept_id": "math_algebra_basics"
50
+ }
51
+ ```
52
+
53
+ **Response:**
54
+ ```json
55
+ {
56
+ "student_id": "student123",
57
+ "concept_id": "math_algebra_basics",
58
+ "skill_level": 0.75,
59
+ "confidence": 0.85,
60
+ "recommendations": [
61
+ "Practice more complex problems",
62
+ "Review related concept: algebra_linear_equations"
63
+ ],
64
+ "timestamp": "2025-06-07T10:30:45.123456"
65
+ }
66
+ ```
67
+
68
+ ##### `generate_quiz`
69
+
70
+ Generate a quiz based on specified concepts and difficulty.
71
+
72
+ **Request:**
73
+ ```json
74
+ {
75
+ "concept_ids": ["math_algebra_basics", "math_algebra_linear_equations"],
76
+ "difficulty": 2
77
+ }
78
+ ```
79
+
80
+ **Response:**
81
+ ```json
82
+ {
83
+ "quiz_id": "q12345",
84
+ "concept_ids": ["math_algebra_basics", "math_algebra_linear_equations"],
85
+ "difficulty": 2,
86
+ "questions": [
87
+ {
88
+ "id": "q1",
89
+ "text": "Solve for x: 2x + 3 = 7",
90
+ "type": "algebraic_equation",
91
+ "answer": "x = 2",
92
+ "solution_steps": [
93
+ "2x + 3 = 7",
94
+ "2x = 7 - 3",
95
+ "2x = 4",
96
+ "x = 4/2 = 2"
97
+ ]
98
+ }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ #### Feedback System
104
+
105
+ ##### `analyze_error_patterns`
106
+
107
+ Analyze common error patterns for a student on a specific concept.
108
+
109
+ **Request:**
110
+ ```json
111
+ {
112
+ "student_id": "student123",
113
+ "concept_id": "math_algebra_basics"
114
+ }
115
+ ```
116
+
117
+ **Response:**
118
+ ```json
119
+ {
120
+ "student_id": "student123",
121
+ "concept_id": "math_algebra_basics",
122
+ "common_errors": [
123
+ {
124
+ "type": "sign_error",
125
+ "frequency": 0.65,
126
+ "example": "2x - 3 = 5 → 2x = 5 - 3 → 2x = 2 → x = 1 (should be x = 4)"
127
+ }
128
+ ],
129
+ "recommendations": [
130
+ "Practice more sign manipulation problems"
131
+ ]
132
+ }
133
+ ```
134
+
135
+ ### Advanced Features
136
+
137
+ #### Neurological Engagement Monitor
138
+
139
+ ##### `analyze_cognitive_state`
140
+
141
+ Analyze EEG data to determine cognitive state.
142
+
143
+ **Request:**
144
+ ```json
145
+ {
146
+ "eeg_data": {
147
+ "channels": [...],
148
+ "sampling_rate": 256,
149
+ "duration": 10.0
150
+ }
151
+ }
152
+ ```
153
+
154
+ **Response:**
155
+ ```json
156
+ {
157
+ "attention_level": 0.82,
158
+ "cognitive_load": 0.65,
159
+ "stress_level": 0.25,
160
+ "recommendations": [
161
+ "Student is engaged but approaching cognitive overload",
162
+ "Consider simplifying next problems slightly"
163
+ ],
164
+ "timestamp": "2025-06-07T10:32:15.123456"
165
+ }
166
+ ```
167
+
168
+ ### External Integrations
169
+
170
+ #### Learning Management Systems
171
+
172
+ ##### `lms_sync_grades`
173
+
174
+ Sync grades with a Learning Management System.
175
+
176
+ **Request:**
177
+ ```json
178
+ {
179
+ "lms_type": "canvas",
180
+ "api_url": "https://canvas.example.com/api/v1",
181
+ "api_key": "your-api-key",
182
+ "course_id": "course123",
183
+ "assignment_id": "assign456",
184
+ "grades": [
185
+ {
186
+ "student_id": "student123",
187
+ "score": 85.5
188
+ }
189
+ ]
190
+ }
191
+ ```
192
+
193
+ **Response:**
194
+ ```json
195
+ {
196
+ "success": true,
197
+ "timestamp": "2025-06-07T10:35:22.123456",
198
+ "message": "Grades successfully synced"
199
+ }
200
+ ```
201
+
202
+ #### Open Educational Resources
203
+
204
+ ##### `oer_search`
205
+
206
+ Search for educational resources in OER repositories.
207
+
208
+ **Request:**
209
+ ```json
210
+ {
211
+ "repository_url": "https://oer.example.com/api",
212
+ "query": "linear equations",
213
+ "subject": "mathematics",
214
+ "grade_level": "8"
215
+ }
216
+ ```
217
+
218
+ **Response:**
219
+ ```json
220
+ {
221
+ "success": true,
222
+ "count": 2,
223
+ "results": [
224
+ {
225
+ "id": "resource123",
226
+ "title": "Introduction to Linear Equations",
227
+ "description": "A comprehensive guide to solving linear equations",
228
+ "url": "https://oer.example.com/resources/resource123",
229
+ "subject": "mathematics",
230
+ "grade_level": "8-9",
231
+ "license": "CC-BY"
232
+ }
233
+ ],
234
+ "timestamp": "2025-06-07T10:36:12.123456"
235
+ }
236
+ ```
237
+
238
+ #### Real-Time Personalized Tutoring
239
+
240
+ ##### `schedule_tutoring_session`
241
+
242
+ Schedule a session with a real-time personalized tutoring platform.
243
+
244
+ **Request:**
245
+ ```json
246
+ {
247
+ "platform_url": "https://tutoring.example.com/api",
248
+ "client_id": "your-client-id",
249
+ "client_secret": "your-client-secret",
250
+ "student_id": "student123",
251
+ "subject": "mathematics",
252
+ "datetime_str": "2025-06-10T15:00:00Z"
253
+ }
254
+ ```
255
+
256
+ **Response:**
257
+ ```json
258
+ {
259
+ "success": true,
260
+ "session_id": "session789",
261
+ "tutor": {
262
+ "id": "tutor456",
263
+ "name": "Dr. Jane Smith",
264
+ "rating": 4.9,
265
+ "specialization": "mathematics"
266
+ },
267
+ "datetime": "2025-06-10T15:00:00Z",
268
+ "join_url": "https://tutoring.example.com/session/session789",
269
+ "timestamp": "2025-06-07T10:37:45.123456"
270
+ }
271
+ ```
272
+
273
+ ## Resources API
274
+
275
+ Resources represent data that can be fetched by MCP clients. Each resource is accessed via:
276
+
277
+ ```
278
+ GET /resources?uri={resource_uri}
279
+ Accept: application/json
280
+ ```
281
+
282
+ ### Available Resources
283
+
284
+ #### `concept-graph://`
285
+
286
+ Retrieves the full knowledge concept graph.
287
+
288
+ #### `learning-path://{student_id}`
289
+
290
+ Retrieves the personalized learning path for a student.
291
+
292
+ #### `curriculum-standards://{country_code}`
293
+
294
+ Retrieves curriculum standards for a specific country.
295
+
296
+ #### `student-dashboard://{student_id}`
297
+
298
+ Retrieves dashboard data for a specific student.
299
+
300
+ ## Error Handling
301
+
302
+ API errors follow a standard format:
303
+
304
+ ```json
305
+ {
306
+ "error": {
307
+ "code": "error_code",
308
+ "message": "Human-readable error message",
309
+ "details": {}
310
+ }
311
+ }
312
+ ```
313
+
314
+ Common error codes:
315
+ - `invalid_request`: The request was malformed
316
+ - `authentication_error`: Authentication failed
317
+ - `not_found`: The requested resource does not exist
318
+ - `server_error`: Internal server error
319
+
320
+ ## Rate Limiting
321
+
322
+ Production deployments implement rate limiting to prevent abuse. Clients should monitor the following headers:
323
+
324
+ - `X-RateLimit-Limit`: Maximum requests per hour
325
+ - `X-RateLimit-Remaining`: Remaining requests for the current hour
326
+ - `X-RateLimit-Reset`: Timestamp when the limit will reset
327
+
328
+ ## SDK
329
+
330
+ For easier integration, we provide client SDKs in multiple languages:
331
+
332
+ - Python: `pip install tutorx-client`
333
+ - JavaScript: `npm install tutorx-client`
334
+
335
+ Example usage (Python):
336
+
337
+ ```python
338
+ from tutorx_client import TutorXClient
339
+
340
+ client = TutorXClient("http://localhost:8000", api_key="your-api-key")
341
+
342
+ # Call a tool
343
+ result = client.assess_skill("student123", "math_algebra_basics")
344
+ print(result["skill_level"])
345
+
346
+ # Access a resource
347
+ concept_graph = client.get_concept_graph()
348
+ ```
349
+
350
+ ## Webhooks
351
+
352
+ For real-time updates, you can register webhook endpoints:
353
+
354
+ ```
355
+ POST /webhooks/register
356
+ Content-Type: application/json
357
+ Authorization: Bearer your-api-key
358
+
359
+ {
360
+ "url": "https://your-app.example.com/webhook",
361
+ "events": ["assessment.completed", "badge.awarded"],
362
+ "secret": "your-webhook-secret"
363
+ }
364
+ ```
365
+
366
+ ## Support
367
+
368
+ For API support, contact us at [email protected]
docs/deployment.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TutorX-MCP Deployment Guide
2
+
3
+ This guide provides instructions for deploying the TutorX-MCP server in production environments.
4
+
5
+ ## Deployment Options
6
+
7
+ ### 1. Docker Deployment (Recommended)
8
+
9
+ The easiest way to deploy TutorX-MCP is using Docker and Docker Compose:
10
+
11
+ ```bash
12
+ # Navigate to the deployment directory
13
+ cd deployment
14
+
15
+ # Start the services
16
+ docker-compose up -d
17
+ ```
18
+
19
+ This will start:
20
+ - The MCP server at http://localhost:8000
21
+ - The Gradio web interface at http://localhost:7860
22
+ - A Redis instance for caching and session management
23
+
24
+ ### 2. Manual Deployment
25
+
26
+ #### Prerequisites
27
+
28
+ - Python 3.12 or higher
29
+ - Redis (optional, but recommended for production)
30
+
31
+ #### Steps
32
+
33
+ 1. Install dependencies:
34
+ ```bash
35
+ uv install -e .
36
+ ```
37
+
38
+ 2. Configure environment variables:
39
+ ```bash
40
+ # Server configuration
41
+ export MCP_HOST=0.0.0.0
42
+ export MCP_PORT=8000
43
+
44
+ # Redis configuration (if using)
45
+ export REDIS_HOST=localhost
46
+ export REDIS_PORT=6379
47
+ ```
48
+
49
+ 3. Run the server:
50
+ ```bash
51
+ python run.py --mode both --host 0.0.0.0
52
+ ```
53
+
54
+ ## Scaling
55
+
56
+ For high-traffic deployments, consider:
57
+
58
+ 1. Using a reverse proxy like Nginx or Traefik in front of the services
59
+ 2. Implementing load balancing for multiple MCP server instances
60
+ 3. Scaling the Redis cache using Redis Sentinel or Redis Cluster
61
+
62
+ ## Monitoring
63
+
64
+ We recommend setting up:
65
+
66
+ 1. Prometheus for metrics collection
67
+ 2. Grafana for visualization
68
+ 3. ELK stack for log management
69
+
70
+ ## Security Considerations
71
+
72
+ 1. In production, always use HTTPS
73
+ 2. Implement proper authentication for API access
74
+ 3. Keep dependencies updated
75
+ 4. Follow least privilege principles for service accounts
76
+
77
+ ## Environment Variables
78
+
79
+ | Variable | Description | Default |
80
+ |----------|-------------|---------|
81
+ | MCP_HOST | Host address for MCP server | 127.0.0.1 |
82
+ | MCP_PORT | Port for MCP server | 8000 |
83
+ | REDIS_HOST | Redis host address | localhost |
84
+ | REDIS_PORT | Redis port | 6379 |
85
+ | LOG_LEVEL | Logging level (DEBUG, INFO, WARNING, ERROR) | INFO |
main.py CHANGED
@@ -1,6 +1,7 @@
1
  # TutorX MCP Server
2
  from mcp.server.fastmcp import FastMCP
3
  import json
 
4
  from typing import List, Dict, Any, Optional
5
  from datetime import datetime
6
 
@@ -17,6 +18,18 @@ from utils.assessment import (
17
  generate_performance_analytics,
18
  detect_plagiarism
19
  )
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  # Create the TutorX MCP server
22
  mcp = FastMCP("TutorX")
@@ -579,5 +592,200 @@ def check_submission_originality(submission: str, reference_sources: List[str])
579
  """
580
  return detect_plagiarism(submission, reference_sources)
581
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  if __name__ == "__main__":
583
  mcp.run()
 
1
  # TutorX MCP Server
2
  from mcp.server.fastmcp import FastMCP
3
  import json
4
+ import os
5
  from typing import List, Dict, Any, Optional
6
  from datetime import datetime
7
 
 
18
  generate_performance_analytics,
19
  detect_plagiarism
20
  )
21
+ from utils.gamification import (
22
+ award_badge,
23
+ get_student_badges,
24
+ update_leaderboard,
25
+ get_leaderboard,
26
+ check_achievements
27
+ )
28
+ from utils.integrations import (
29
+ LMSIntegration,
30
+ OERIntegration,
31
+ RTPTIntegration
32
+ )
33
 
34
  # Create the TutorX MCP server
35
  mcp = FastMCP("TutorX")
 
592
  """
593
  return detect_plagiarism(submission, reference_sources)
594
 
595
+ # ------------------ Gamification Features ------------------
596
+
597
+ @mcp.tool()
598
+ def award_student_badge(student_id: str, badge_id: str) -> Dict[str, Any]:
599
+ """
600
+ Award a badge to a student
601
+
602
+ Args:
603
+ student_id: The student's unique identifier
604
+ badge_id: The badge ID to award
605
+
606
+ Returns:
607
+ Badge information
608
+ """
609
+ return award_badge(student_id, badge_id)
610
+
611
+ @mcp.tool()
612
+ def get_badges_for_student(student_id: str) -> Dict[str, Any]:
613
+ """
614
+ Get all badges for a student
615
+
616
+ Args:
617
+ student_id: The student's unique identifier
618
+
619
+ Returns:
620
+ Badge information
621
+ """
622
+ return get_student_badges(student_id)
623
+
624
+ @mcp.tool()
625
+ def update_student_leaderboard(leaderboard_id: str, student_id: str, score: float) -> Dict[str, Any]:
626
+ """
627
+ Update a leaderboard with a student's score
628
+
629
+ Args:
630
+ leaderboard_id: ID of the leaderboard to update
631
+ student_id: The student's unique identifier
632
+ score: The score to record
633
+
634
+ Returns:
635
+ Leaderboard information
636
+ """
637
+ return update_leaderboard(leaderboard_id, student_id, score)
638
+
639
+ @mcp.tool()
640
+ def get_current_leaderboard(leaderboard_id: str) -> Dict[str, Any]:
641
+ """
642
+ Get current leaderboard standings
643
+
644
+ Args:
645
+ leaderboard_id: ID of the leaderboard to get
646
+
647
+ Returns:
648
+ Leaderboard information
649
+ """
650
+ return get_leaderboard(leaderboard_id)
651
+
652
+ @mcp.tool()
653
+ def track_student_activity(student_id: str, activity_data: Dict[str, Any]) -> Dict[str, Any]:
654
+ """
655
+ Track a student's activity and check for achievements
656
+
657
+ Args:
658
+ student_id: The student's unique identifier
659
+ activity_data: Data about the activity
660
+
661
+ Returns:
662
+ Tracking information and any new badges
663
+ """
664
+ new_badges = check_achievements(student_id, activity_data)
665
+
666
+ return {
667
+ "student_id": student_id,
668
+ "activity_tracked": True,
669
+ "activity_data": activity_data,
670
+ "new_badges": new_badges,
671
+ "timestamp": datetime.now().isoformat()
672
+ }
673
+
674
+ # ------------------ External Integrations ------------------
675
+
676
+ @mcp.tool()
677
+ def lms_sync_grades(lms_type: str, api_url: str, api_key: str,
678
+ course_id: str, assignment_id: str,
679
+ grades: List[Dict[str, Any]]) -> Dict[str, Any]:
680
+ """
681
+ Sync grades with a Learning Management System
682
+
683
+ Args:
684
+ lms_type: Type of LMS ('canvas', 'moodle', 'blackboard')
685
+ api_url: URL for the LMS API
686
+ api_key: API key for authentication
687
+ course_id: ID of the course
688
+ assignment_id: ID of the assignment
689
+ grades: List of grade data to sync
690
+
691
+ Returns:
692
+ Status of the sync operation
693
+ """
694
+ try:
695
+ lms = LMSIntegration(lms_type, api_url, api_key)
696
+ success = lms.sync_grades(course_id, assignment_id, grades)
697
+ return {
698
+ "success": success,
699
+ "timestamp": datetime.now().isoformat(),
700
+ "message": "Grades successfully synced" if success else "Failed to sync grades"
701
+ }
702
+ except Exception as e:
703
+ return {
704
+ "success": False,
705
+ "error": str(e),
706
+ "timestamp": datetime.now().isoformat()
707
+ }
708
+
709
+ @mcp.tool()
710
+ def oer_search(repository_url: str, query: str,
711
+ subject: Optional[str] = None, grade_level: Optional[str] = None,
712
+ api_key: Optional[str] = None) -> Dict[str, Any]:
713
+ """
714
+ Search for educational resources in OER repositories
715
+
716
+ Args:
717
+ repository_url: URL of the OER repository
718
+ query: Search query
719
+ subject: Optional subject filter
720
+ grade_level: Optional grade level filter
721
+ api_key: Optional API key if required
722
+
723
+ Returns:
724
+ List of matching resources
725
+ """
726
+ try:
727
+ oer = OERIntegration(repository_url, api_key)
728
+ results = oer.search_resources(query, subject, grade_level)
729
+ return {
730
+ "success": True,
731
+ "count": len(results),
732
+ "results": results,
733
+ "timestamp": datetime.now().isoformat()
734
+ }
735
+ except Exception as e:
736
+ return {
737
+ "success": False,
738
+ "error": str(e),
739
+ "timestamp": datetime.now().isoformat()
740
+ }
741
+
742
+ @mcp.tool()
743
+ def schedule_tutoring_session(platform_url: str, client_id: str, client_secret: str,
744
+ student_id: str, subject: str, datetime_str: str) -> Dict[str, Any]:
745
+ """
746
+ Schedule a session with a real-time personalized tutoring platform
747
+
748
+ Args:
749
+ platform_url: URL of the tutoring platform
750
+ client_id: OAuth client ID
751
+ client_secret: OAuth client secret
752
+ student_id: ID of the student
753
+ subject: Subject for tutoring
754
+ datetime_str: ISO format datetime for the session
755
+
756
+ Returns:
757
+ Session details
758
+ """
759
+ try:
760
+ # Find an available tutor
761
+ rtpt = RTPTIntegration(platform_url, client_id, client_secret)
762
+ tutors = rtpt.get_available_tutors(subject, "intermediate")
763
+
764
+ if not tutors:
765
+ return {
766
+ "success": False,
767
+ "message": "No tutors available for this subject",
768
+ "timestamp": datetime.now().isoformat()
769
+ }
770
+
771
+ # Schedule with first available tutor
772
+ tutor_id = tutors[0]["id"]
773
+ session = rtpt.schedule_session(student_id, tutor_id, subject, datetime_str)
774
+
775
+ return {
776
+ "success": True,
777
+ "session_id": session.get("id"),
778
+ "tutor": session.get("tutor"),
779
+ "datetime": session.get("datetime"),
780
+ "join_url": session.get("join_url"),
781
+ "timestamp": datetime.now().isoformat()
782
+ }
783
+ except Exception as e:
784
+ return {
785
+ "success": False,
786
+ "error": str(e),
787
+ "timestamp": datetime.now().isoformat()
788
+ }
789
+
790
  if __name__ == "__main__":
791
  mcp.run()
pyproject.toml CHANGED
@@ -11,3 +11,13 @@ dependencies = [
11
  "pillow>=10.0.0",
12
  "requests>=2.31.0",
13
  ]
 
 
 
 
 
 
 
 
 
 
 
11
  "pillow>=10.0.0",
12
  "requests>=2.31.0",
13
  ]
14
+
15
+ [project.optional-dependencies]
16
+ test = [
17
+ "pytest>=7.4.0",
18
+ "pytest-cov>=4.1.0",
19
+ ]
20
+
21
+ [tool.pytest.ini_options]
22
+ testpaths = ["tests"]
23
+ python_files = "test_*.py"
run_tests.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script to run all tests for the TutorX MCP server
3
+ """
4
+ import sys
5
+ import os
6
+ import unittest
7
+ import pytest
8
+
9
+ def run_tests():
10
+ """Run all tests"""
11
+ print("Running TutorX-MCP Tests...")
12
+
13
+ # First run unittest tests
14
+ unittest_loader = unittest.TestLoader()
15
+ test_directory = os.path.join(os.path.dirname(__file__), "tests")
16
+ test_suite = unittest_loader.discover(test_directory)
17
+
18
+ test_runner = unittest.TextTestRunner(verbosity=2)
19
+ unittest_result = test_runner.run(test_suite)
20
+
21
+ # Then run pytest tests (with coverage)
22
+ pytest_args = [
23
+ "tests",
24
+ "--cov=.",
25
+ "--cov-report=term",
26
+ "--cov-report=html:coverage_html",
27
+ "-v"
28
+ ]
29
+
30
+ pytest_result = pytest.main(pytest_args)
31
+
32
+ # Return success if both test runners succeeded
33
+ return unittest_result.wasSuccessful() and pytest_result == 0
34
+
35
+ if __name__ == "__main__":
36
+ success = run_tests()
37
+ sys.exit(0 if success else 1)
tests/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Test package for TutorX MCP Server
3
+ """
tests/test_client.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for the TutorX MCP client
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ import unittest
8
+ from unittest.mock import patch, MagicMock
9
+ import json
10
+ import requests
11
+
12
+ # Add parent directory to path to import modules
13
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
14
+
15
+ from client import TutorXClient
16
+
17
+
18
+ class TestTutorXClient(unittest.TestCase):
19
+ """Test cases for the TutorX MCP client"""
20
+
21
+ def setUp(self):
22
+ """Set up test fixtures"""
23
+ self.client = TutorXClient("http://localhost:8000")
24
+ self.student_id = "test_student_123"
25
+ self.concept_id = "math_algebra_basics"
26
+
27
+ @patch('client.requests.post')
28
+ def test_call_tool(self, mock_post):
29
+ """Test _call_tool method"""
30
+ # Setup mock response
31
+ mock_response = MagicMock()
32
+ mock_response.json.return_value = {"result": "success"}
33
+ mock_response.raise_for_status = MagicMock()
34
+ mock_post.return_value = mock_response
35
+
36
+ # Call method
37
+ result = self.client._call_tool("test_tool", {"param": "value"})
38
+
39
+ # Assertions
40
+ self.assertEqual(result, {"result": "success"})
41
+ mock_post.assert_called_once_with(
42
+ "http://localhost:8000/tools/test_tool",
43
+ json={"param": "value"},
44
+ headers={"Content-Type": "application/json"}
45
+ )
46
+ mock_response.raise_for_status.assert_called_once()
47
+
48
+ @patch('client.requests.get')
49
+ def test_get_resource(self, mock_get):
50
+ """Test _get_resource method"""
51
+ # Setup mock response
52
+ mock_response = MagicMock()
53
+ mock_response.json.return_value = {"resource": "data"}
54
+ mock_response.raise_for_status = MagicMock()
55
+ mock_get.return_value = mock_response
56
+
57
+ # Call method
58
+ result = self.client._get_resource("test-resource://identifier")
59
+
60
+ # Assertions
61
+ self.assertEqual(result, {"resource": "data"})
62
+ mock_get.assert_called_once_with(
63
+ "http://localhost:8000/resources?uri=test-resource://identifier",
64
+ headers={"Accept": "application/json"}
65
+ )
66
+ mock_response.raise_for_status.assert_called_once()
67
+
68
+ @patch('client.TutorXClient._call_tool')
69
+ def test_assess_skill(self, mock_call_tool):
70
+ """Test assess_skill method"""
71
+ # Setup mock return value
72
+ mock_call_tool.return_value = {"skill_level": 0.75}
73
+
74
+ # Call method
75
+ result = self.client.assess_skill(self.student_id, self.concept_id)
76
+
77
+ # Assertions
78
+ self.assertEqual(result, {"skill_level": 0.75})
79
+ mock_call_tool.assert_called_once_with("assess_skill", {
80
+ "student_id": self.student_id,
81
+ "concept_id": self.concept_id
82
+ })
83
+
84
+ @patch('client.TutorXClient._get_resource')
85
+ def test_get_concept_graph(self, mock_get_resource):
86
+ """Test get_concept_graph method"""
87
+ # Setup mock return value
88
+ mock_get_resource.return_value = {"nodes": [], "edges": []}
89
+
90
+ # Call method
91
+ result = self.client.get_concept_graph()
92
+
93
+ # Assertions
94
+ self.assertEqual(result, {"nodes": [], "edges": []})
95
+ mock_get_resource.assert_called_once_with("concept-graph://")
96
+
97
+ @patch('client.TutorXClient._call_tool')
98
+ def test_generate_quiz(self, mock_call_tool):
99
+ """Test generate_quiz method"""
100
+ # Setup mock return value
101
+ mock_call_tool.return_value = {"questions": []}
102
+
103
+ # Call method
104
+ concept_ids = [self.concept_id]
105
+ difficulty = 3
106
+ result = self.client.generate_quiz(concept_ids, difficulty)
107
+
108
+ # Assertions
109
+ self.assertEqual(result, {"questions": []})
110
+ mock_call_tool.assert_called_once_with("generate_quiz", {
111
+ "concept_ids": concept_ids,
112
+ "difficulty": difficulty
113
+ })
114
+
115
+ @patch('client.requests.post')
116
+ def test_error_handling(self, mock_post):
117
+ """Test error handling in _call_tool"""
118
+ # Setup mock to raise exception
119
+ mock_post.side_effect = requests.RequestException("Connection error")
120
+
121
+ # Call method
122
+ result = self.client._call_tool("test_tool", {})
123
+
124
+ # Assertions
125
+ self.assertIn("error", result)
126
+ self.assertIn("Connection error", result["error"])
127
+ self.assertIn("timestamp", result)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ unittest.main()
tests/test_mcp_server.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for the TutorX MCP server
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ import unittest
8
+ import json
9
+ from datetime import datetime
10
+ from unittest.mock import patch, MagicMock
11
+
12
+ # Add parent directory to path to import modules
13
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
14
+
15
+ from main import assess_skill, generate_quiz, get_concept_graph
16
+
17
+
18
+ class TestMCPServer(unittest.TestCase):
19
+ """Test cases for the TutorX MCP server"""
20
+
21
+ def setUp(self):
22
+ """Set up test fixtures"""
23
+ self.student_id = "test_student_123"
24
+ self.concept_id = "math_algebra_basics"
25
+
26
+ def test_assess_skill(self):
27
+ """Test assess_skill tool"""
28
+ result = assess_skill(self.student_id, self.concept_id)
29
+
30
+ # Verify the structure of the result
31
+ self.assertIsInstance(result, dict)
32
+ self.assertEqual(result["student_id"], self.student_id)
33
+ self.assertEqual(result["concept_id"], self.concept_id)
34
+ self.assertIsInstance(result["skill_level"], float)
35
+ self.assertIsInstance(result["confidence"], float)
36
+ self.assertIsInstance(result["recommendations"], list)
37
+ self.assertIn("timestamp", result)
38
+
39
+ def test_generate_quiz(self):
40
+ """Test generate_quiz tool"""
41
+ concept_ids = [self.concept_id]
42
+ difficulty = 2
43
+
44
+ result = generate_quiz(concept_ids, difficulty)
45
+
46
+ # Verify the structure of the result
47
+ self.assertIsInstance(result, dict)
48
+ self.assertIn("quiz_id", result)
49
+ self.assertEqual(result["concept_ids"], concept_ids)
50
+ self.assertEqual(result["difficulty"], difficulty)
51
+ self.assertIsInstance(result["questions"], list)
52
+ self.assertGreater(len(result["questions"]), 0)
53
+
54
+ # Check question structure
55
+ question = result["questions"][0]
56
+ self.assertIn("id", question)
57
+ self.assertIn("text", question)
58
+ self.assertIn("type", question)
59
+ self.assertIn("answer", question)
60
+ self.assertIn("solution_steps", question)
61
+
62
+ def test_get_concept_graph(self):
63
+ """Test get_concept_graph resource"""
64
+ result = get_concept_graph()
65
+
66
+ # Verify the structure of the result
67
+ self.assertIsInstance(result, dict)
68
+ self.assertIn("nodes", result)
69
+ self.assertIn("edges", result)
70
+ self.assertIsInstance(result["nodes"], list)
71
+ self.assertIsInstance(result["edges"], list)
72
+ self.assertGreater(len(result["nodes"]), 0)
73
+ self.assertGreater(len(result["edges"]), 0)
74
+
75
+ # Check node structure
76
+ node = result["nodes"][0]
77
+ self.assertIn("id", node)
78
+ self.assertIn("name", node)
79
+ self.assertIn("difficulty", node)
80
+
81
+ # Check edge structure
82
+ edge = result["edges"][0]
83
+ self.assertIn("from", edge)
84
+ self.assertIn("to", edge)
85
+ self.assertIn("weight", edge)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ unittest.main()
tests/test_utils.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for TutorX MCP utility functions
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ import unittest
8
+ from unittest.mock import patch, MagicMock
9
+
10
+ # Add parent directory to path to import modules
11
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
12
+
13
+ from utils.multimodal import process_text_query, process_voice_input, process_handwriting
14
+ from utils.assessment import generate_question, evaluate_student_answer
15
+
16
+
17
+ class TestMultimodalUtils(unittest.TestCase):
18
+ """Test cases for multimodal utility functions"""
19
+
20
+ def test_process_text_query(self):
21
+ """Test text query processing"""
22
+ # Test with a "solve" query
23
+ solve_query = "Please solve this equation: 2x + 3 = 7"
24
+ result = process_text_query(solve_query)
25
+
26
+ self.assertIsInstance(result, dict)
27
+ self.assertEqual(result["query"], solve_query)
28
+ self.assertEqual(result["response_type"], "math_solution")
29
+ self.assertIn("response", result)
30
+ self.assertIn("confidence", result)
31
+ self.assertIn("timestamp", result)
32
+
33
+ # Test with "what is" query
34
+ what_is_query = "What is a quadratic equation?"
35
+ result = process_text_query(what_is_query)
36
+
37
+ self.assertIsInstance(result, dict)
38
+ self.assertEqual(result["query"], what_is_query)
39
+ self.assertEqual(result["response_type"], "definition")
40
+ self.assertIn("response", result)
41
+
42
+ # Test with unknown query
43
+ unknown_query = "Something completely different"
44
+ result = process_text_query(unknown_query)
45
+
46
+ self.assertIsInstance(result, dict)
47
+ self.assertEqual(result["query"], unknown_query)
48
+ self.assertEqual(result["response_type"], "general")
49
+ self.assertIn("response", result)
50
+
51
+ def test_process_voice_input(self):
52
+ """Test voice input processing"""
53
+ mock_audio_data = "bW9jayBhdWRpbyBkYXRh" # Base64 for "mock audio data"
54
+
55
+ result = process_voice_input(mock_audio_data)
56
+
57
+ self.assertIsInstance(result, dict)
58
+ self.assertIn("transcription", result)
59
+ self.assertIn("confidence", result)
60
+ self.assertIn("detected_emotions", result)
61
+ self.assertIn("timestamp", result)
62
+
63
+ def test_process_handwriting(self):
64
+ """Test handwriting recognition"""
65
+ mock_image_data = "bW9jayBpbWFnZSBkYXRh" # Base64 for "mock image data"
66
+
67
+ result = process_handwriting(mock_image_data)
68
+
69
+ self.assertIsInstance(result, dict)
70
+ self.assertIn("transcription", result)
71
+ self.assertIn("confidence", result)
72
+ self.assertIn("detected_content_type", result)
73
+ self.assertIn("equation_type", result)
74
+ self.assertIn("parsed_latex", result)
75
+ self.assertIn("timestamp", result)
76
+
77
+
78
+ class TestAssessmentUtils(unittest.TestCase):
79
+ """Test cases for assessment utility functions"""
80
+
81
+ def test_generate_question_algebra_basics(self):
82
+ """Test question generation for algebra basics"""
83
+ concept_id = "math_algebra_basics"
84
+ difficulty = 2
85
+
86
+ question = generate_question(concept_id, difficulty)
87
+
88
+ self.assertIsInstance(question, dict)
89
+ self.assertIn("id", question)
90
+ self.assertEqual(question["concept_id"], concept_id)
91
+ self.assertEqual(question["difficulty"], difficulty)
92
+ self.assertIn("text", question)
93
+ self.assertIn("solution", question)
94
+ self.assertIn("answer", question)
95
+ self.assertIn("variables", question)
96
+
97
+ def test_generate_question_linear_equations(self):
98
+ """Test question generation for linear equations"""
99
+ concept_id = "math_algebra_linear_equations"
100
+ difficulty = 3
101
+
102
+ question = generate_question(concept_id, difficulty)
103
+
104
+ self.assertIsInstance(question, dict)
105
+ self.assertEqual(question["concept_id"], concept_id)
106
+ self.assertEqual(question["difficulty"], difficulty)
107
+ self.assertIn("text", question)
108
+ self.assertIn("solution", question)
109
+ self.assertIn("answer", question)
110
+
111
+ def test_evaluate_student_answer_correct(self):
112
+ """Test student answer evaluation - correct answer"""
113
+ question = {
114
+ "id": "q_test_123",
115
+ "concept_id": "math_algebra_basics",
116
+ "difficulty": 2,
117
+ "text": "Solve: x + 5 = 8",
118
+ "solution": "x + 5 = 8\nx = 8 - 5\nx = 3",
119
+ "answer": "x = 3",
120
+ "variables": {"a": 5, "b": 8}
121
+ }
122
+
123
+ # Test correct answer
124
+ correct_answer = "x = 3"
125
+ result = evaluate_student_answer(question, correct_answer)
126
+
127
+ self.assertIsInstance(result, dict)
128
+ self.assertEqual(result["question_id"], question["id"])
129
+ self.assertTrue(result["is_correct"])
130
+ self.assertIsNone(result["error_type"])
131
+ self.assertEqual(result["correct_answer"], question["answer"])
132
+ self.assertEqual(result["student_answer"], correct_answer)
133
+
134
+ def test_evaluate_student_answer_incorrect(self):
135
+ """Test student answer evaluation - incorrect answer"""
136
+ question = {
137
+ "id": "q_test_456",
138
+ "concept_id": "math_algebra_linear_equations",
139
+ "difficulty": 3,
140
+ "text": "Solve: 2x + 3 = 9",
141
+ "solution": "2x + 3 = 9\n2x = 9 - 3\n2x = 6\nx = 6/2\nx = 3",
142
+ "answer": "x = 3",
143
+ "variables": {"a": 2, "b": 3, "c": 9}
144
+ }
145
+
146
+ # Test incorrect answer
147
+ incorrect_answer = "x = 4"
148
+ result = evaluate_student_answer(question, incorrect_answer)
149
+
150
+ self.assertIsInstance(result, dict)
151
+ self.assertEqual(result["question_id"], question["id"])
152
+ self.assertFalse(result["is_correct"])
153
+ self.assertEqual(result["correct_answer"], question["answer"])
154
+ self.assertEqual(result["student_answer"], incorrect_answer)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ unittest.main()
utils/gamification.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gamification utilities for the TutorX MCP server
3
+ """
4
+
5
+ from typing import Dict, Any, List, Optional
6
+ from datetime import datetime
7
+ import random
8
+
9
+
10
+ # Dictionary to store badges for students
11
+ # In a real application, this would be stored in a database
12
+ BADGES_DB = {}
13
+
14
+ # Dictionary to store leaderboards
15
+ # In a real application, this would be stored in a database
16
+ LEADERBOARDS_DB = {
17
+ "weekly_points": {},
18
+ "monthly_streak": {},
19
+ "problem_solving_speed": {}
20
+ }
21
+
22
+ # Badge definitions
23
+ BADGES = {
24
+ "beginner": {
25
+ "name": "Beginner",
26
+ "description": "Completed your first lesson",
27
+ "icon": "🔰",
28
+ "points": 10
29
+ },
30
+ "persistent": {
31
+ "name": "Persistent Learner",
32
+ "description": "Completed 5 lessons in a row",
33
+ "icon": "🔄",
34
+ "points": 25
35
+ },
36
+ "math_whiz": {
37
+ "name": "Math Whiz",
38
+ "description": "Scored 100% on a math assessment",
39
+ "icon": "🧮",
40
+ "points": 50
41
+ },
42
+ "science_explorer": {
43
+ "name": "Science Explorer",
44
+ "description": "Completed 10 science modules",
45
+ "icon": "🔬",
46
+ "points": 50
47
+ },
48
+ "speed_demon": {
49
+ "name": "Speed Demon",
50
+ "description": "Solved 5 problems in under 2 minutes each",
51
+ "icon": "⚡",
52
+ "points": 35
53
+ },
54
+ "accuracy_master": {
55
+ "name": "Accuracy Master",
56
+ "description": "Maintained 90% accuracy over 20 problems",
57
+ "icon": "🎯",
58
+ "points": 40
59
+ },
60
+ "helping_hand": {
61
+ "name": "Helping Hand",
62
+ "description": "Helped 5 other students in the forum",
63
+ "icon": "🤝",
64
+ "points": 30
65
+ },
66
+ "night_owl": {
67
+ "name": "Night Owl",
68
+ "description": "Studied for 3 hours after 8 PM",
69
+ "icon": "🌙",
70
+ "points": 20
71
+ },
72
+ "early_bird": {
73
+ "name": "Early Bird",
74
+ "description": "Studied for 3 hours before 9 AM",
75
+ "icon": "🌅",
76
+ "points": 20
77
+ },
78
+ "perfect_streak": {
79
+ "name": "Perfect Streak",
80
+ "description": "Logged in for 7 days straight",
81
+ "icon": "🔥",
82
+ "points": 45
83
+ }
84
+ }
85
+
86
+
87
+ def award_badge(student_id: str, badge_id: str) -> Dict[str, Any]:
88
+ """
89
+ Award a badge to a student
90
+
91
+ Args:
92
+ student_id: The student's unique identifier
93
+ badge_id: The badge's unique identifier
94
+
95
+ Returns:
96
+ Badge information
97
+ """
98
+ if badge_id not in BADGES:
99
+ return {
100
+ "error": "Invalid badge ID",
101
+ "timestamp": datetime.now().isoformat()
102
+ }
103
+
104
+ if student_id not in BADGES_DB:
105
+ BADGES_DB[student_id] = {}
106
+
107
+ # Check if student already has the badge
108
+ if badge_id in BADGES_DB[student_id]:
109
+ return {
110
+ "message": "Badge already awarded",
111
+ "badge": BADGES[badge_id],
112
+ "timestamp": BADGES_DB[student_id][badge_id]["timestamp"]
113
+ }
114
+
115
+ # Award the badge
116
+ BADGES_DB[student_id][badge_id] = {
117
+ "timestamp": datetime.now().isoformat()
118
+ }
119
+
120
+ return {
121
+ "message": "Badge awarded!",
122
+ "badge": BADGES[badge_id],
123
+ "timestamp": datetime.now().isoformat()
124
+ }
125
+
126
+
127
+ def get_student_badges(student_id: str) -> Dict[str, Any]:
128
+ """
129
+ Get all badges for a student
130
+
131
+ Args:
132
+ student_id: The student's unique identifier
133
+
134
+ Returns:
135
+ Dictionary of badges and points
136
+ """
137
+ if student_id not in BADGES_DB or not BADGES_DB[student_id]:
138
+ return {
139
+ "student_id": student_id,
140
+ "badges": [],
141
+ "total_points": 0,
142
+ "timestamp": datetime.now().isoformat()
143
+ }
144
+
145
+ badges_list = []
146
+ total_points = 0
147
+
148
+ for badge_id in BADGES_DB[student_id]:
149
+ if badge_id in BADGES:
150
+ badge_info = BADGES[badge_id].copy()
151
+ badge_info["awarded_at"] = BADGES_DB[student_id][badge_id]["timestamp"]
152
+ badges_list.append(badge_info)
153
+ total_points += badge_info["points"]
154
+
155
+ return {
156
+ "student_id": student_id,
157
+ "badges": badges_list,
158
+ "total_points": total_points,
159
+ "timestamp": datetime.now().isoformat()
160
+ }
161
+
162
+
163
+ def update_leaderboard(leaderboard_id: str, student_id: str, score: float) -> Dict[str, Any]:
164
+ """
165
+ Update a leaderboard with a new score for a student
166
+
167
+ Args:
168
+ leaderboard_id: The leaderboard to update
169
+ student_id: The student's unique identifier
170
+ score: The score to record
171
+
172
+ Returns:
173
+ Updated leaderboard information
174
+ """
175
+ if leaderboard_id not in LEADERBOARDS_DB:
176
+ return {
177
+ "error": "Invalid leaderboard ID",
178
+ "timestamp": datetime.now().isoformat()
179
+ }
180
+
181
+ # Update student's score
182
+ LEADERBOARDS_DB[leaderboard_id][student_id] = {
183
+ "score": score,
184
+ "timestamp": datetime.now().isoformat()
185
+ }
186
+
187
+ # Get top 10 students
188
+ top_students = sorted(
189
+ LEADERBOARDS_DB[leaderboard_id].items(),
190
+ key=lambda x: x[1]["score"],
191
+ reverse=True
192
+ )[:10]
193
+
194
+ # Format leaderboard
195
+ leaderboard = []
196
+ for i, (sid, data) in enumerate(top_students):
197
+ leaderboard.append({
198
+ "rank": i + 1,
199
+ "student_id": sid,
200
+ "score": data["score"],
201
+ "last_updated": data["timestamp"]
202
+ })
203
+
204
+ # Find student's rank
205
+ student_rank = next(
206
+ (i + 1 for i, (sid, _) in enumerate(top_students) if sid == student_id),
207
+ len(LEADERBOARDS_DB[leaderboard_id]) + 1
208
+ )
209
+
210
+ return {
211
+ "leaderboard_id": leaderboard_id,
212
+ "leaderboard": leaderboard,
213
+ "student_rank": student_rank,
214
+ "student_score": score,
215
+ "timestamp": datetime.now().isoformat()
216
+ }
217
+
218
+
219
+ def get_leaderboard(leaderboard_id: str) -> Dict[str, Any]:
220
+ """
221
+ Get the current state of a leaderboard
222
+
223
+ Args:
224
+ leaderboard_id: The leaderboard to get
225
+
226
+ Returns:
227
+ Leaderboard information
228
+ """
229
+ if leaderboard_id not in LEADERBOARDS_DB:
230
+ return {
231
+ "error": "Invalid leaderboard ID",
232
+ "timestamp": datetime.now().isoformat()
233
+ }
234
+
235
+ # Get top 10 students
236
+ top_students = sorted(
237
+ LEADERBOARDS_DB[leaderboard_id].items(),
238
+ key=lambda x: x[1]["score"],
239
+ reverse=True
240
+ )[:10]
241
+
242
+ # Format leaderboard
243
+ leaderboard = []
244
+ for i, (sid, data) in enumerate(top_students):
245
+ leaderboard.append({
246
+ "rank": i + 1,
247
+ "student_id": sid,
248
+ "score": data["score"],
249
+ "last_updated": data["timestamp"]
250
+ })
251
+
252
+ return {
253
+ "leaderboard_id": leaderboard_id,
254
+ "leaderboard": leaderboard,
255
+ "total_students": len(LEADERBOARDS_DB[leaderboard_id]),
256
+ "timestamp": datetime.now().isoformat()
257
+ }
258
+
259
+
260
+ def check_achievements(student_id: str, activity_data: Dict[str, Any]) -> List[Dict[str, Any]]:
261
+ """
262
+ Check if a student's activity unlocks any new badges
263
+
264
+ Args:
265
+ student_id: The student's unique identifier
266
+ activity_data: Data about the student's activity
267
+
268
+ Returns:
269
+ List of newly awarded badges
270
+ """
271
+ new_badges = []
272
+
273
+ # Initialize student badge record if needed
274
+ if student_id not in BADGES_DB:
275
+ BADGES_DB[student_id] = {}
276
+
277
+ # Check for potential badge earnings based on activity
278
+ if "activity_type" in activity_data:
279
+ activity_type = activity_data["activity_type"]
280
+
281
+ # Beginner badge - first lesson
282
+ if activity_type == "lesson_completed" and "beginner" not in BADGES_DB[student_id]:
283
+ badge_result = award_badge(student_id, "beginner")
284
+ if "error" not in badge_result:
285
+ new_badges.append(badge_result)
286
+
287
+ # Math Whiz - perfect math assessment
288
+ if (activity_type == "assessment_completed" and
289
+ activity_data.get("subject") == "math" and
290
+ activity_data.get("score") == 1.0 and
291
+ "math_whiz" not in BADGES_DB[student_id]):
292
+ badge_result = award_badge(student_id, "math_whiz")
293
+ if "error" not in badge_result:
294
+ new_badges.append(badge_result)
295
+
296
+ # Speed Demon - fast problem solving
297
+ if (activity_type == "problem_solved" and
298
+ activity_data.get("time_seconds", 999) < 120):
299
+ # In a real system, we'd track the count over time
300
+ # Here we'll simulate it
301
+ if random.random() < 0.2 and "speed_demon" not in BADGES_DB[student_id]:
302
+ badge_result = award_badge(student_id, "speed_demon")
303
+ if "error" not in badge_result:
304
+ new_badges.append(badge_result)
305
+
306
+ return new_badges
utils/integrations.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Integration utilities for connecting TutorX-MCP with external educational systems
3
+ """
4
+
5
+ import requests
6
+ import json
7
+ import os
8
+ from typing import Dict, Any, List, Optional
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class LMSIntegration:
14
+ """Integration with Learning Management Systems (Canvas, Moodle, etc.)"""
15
+
16
+ def __init__(self, lms_type: str, api_url: str, api_key: str):
17
+ """
18
+ Initialize LMS integration
19
+
20
+ Args:
21
+ lms_type: Type of LMS ('canvas', 'moodle', 'blackboard', etc.)
22
+ api_url: Base URL for LMS API
23
+ api_key: API key or token for authentication
24
+ """
25
+ self.lms_type = lms_type.lower()
26
+ self.api_url = api_url.rstrip('/')
27
+ self.api_key = api_key
28
+
29
+ def get_courses(self) -> List[Dict[str, Any]]:
30
+ """Get list of courses from LMS"""
31
+ if self.lms_type == 'canvas':
32
+ return self._canvas_get_courses()
33
+ elif self.lms_type == 'moodle':
34
+ return self._moodle_get_courses()
35
+ else:
36
+ raise ValueError(f"Unsupported LMS type: {self.lms_type}")
37
+
38
+ def _canvas_get_courses(self) -> List[Dict[str, Any]]:
39
+ """Get courses from Canvas LMS"""
40
+ headers = {"Authorization": f"Bearer {self.api_key}"}
41
+ response = requests.get(f"{self.api_url}/courses", headers=headers)
42
+ response.raise_for_status()
43
+ return response.json()
44
+
45
+ def _moodle_get_courses(self) -> List[Dict[str, Any]]:
46
+ """Get courses from Moodle"""
47
+ params = {
48
+ "wstoken": self.api_key,
49
+ "wsfunction": "core_course_get_courses",
50
+ "moodlewsrestformat": "json"
51
+ }
52
+ response = requests.get(f"{self.api_url}/webservice/rest/server.php", params=params)
53
+ response.raise_for_status()
54
+ return response.json()
55
+
56
+ def sync_grades(self, course_id: str, assignment_id: str, grades: List[Dict[str, Any]]) -> bool:
57
+ """
58
+ Sync grades to LMS
59
+
60
+ Args:
61
+ course_id: ID of the course
62
+ assignment_id: ID of the assignment
63
+ grades: List of grade data to sync
64
+
65
+ Returns:
66
+ Success status
67
+ """
68
+ try:
69
+ if self.lms_type == 'canvas':
70
+ return self._canvas_sync_grades(course_id, assignment_id, grades)
71
+ elif self.lms_type == 'moodle':
72
+ return self._moodle_sync_grades(course_id, assignment_id, grades)
73
+ else:
74
+ raise ValueError(f"Unsupported LMS type: {self.lms_type}")
75
+ except Exception as e:
76
+ logger.error(f"Error syncing grades: {e}")
77
+ return False
78
+
79
+ def _canvas_sync_grades(self, course_id: str, assignment_id: str, grades: List[Dict[str, Any]]) -> bool:
80
+ """Sync grades to Canvas LMS"""
81
+ headers = {"Authorization": f"Bearer {self.api_key}"}
82
+
83
+ for grade in grades:
84
+ data = {
85
+ "submission": {
86
+ "posted_grade": grade["score"]
87
+ }
88
+ }
89
+
90
+ url = f"{self.api_url}/courses/{course_id}/assignments/{assignment_id}/submissions/{grade['student_id']}"
91
+ response = requests.put(url, json=data, headers=headers)
92
+
93
+ if response.status_code != 200:
94
+ logger.error(f"Failed to sync grade for student {grade['student_id']}: {response.text}")
95
+ return False
96
+
97
+ return True
98
+
99
+ def _moodle_sync_grades(self, course_id: str, assignment_id: str, grades: List[Dict[str, Any]]) -> bool:
100
+ """Sync grades to Moodle"""
101
+ # Implementation specific to Moodle's API
102
+ # This would use the Moodle grade update API
103
+ return True # Placeholder
104
+
105
+ class OERIntegration:
106
+ """Integration with Open Educational Resources repositories"""
107
+
108
+ def __init__(self, repository_url: str, api_key: Optional[str] = None):
109
+ """
110
+ Initialize OER repository integration
111
+
112
+ Args:
113
+ repository_url: Base URL for OER repository API
114
+ api_key: Optional API key if required by the repository
115
+ """
116
+ self.repository_url = repository_url.rstrip('/')
117
+ self.api_key = api_key
118
+
119
+ def search_resources(self, query: str, subject: Optional[str] = None,
120
+ grade_level: Optional[str] = None) -> List[Dict[str, Any]]:
121
+ """
122
+ Search for educational resources
123
+
124
+ Args:
125
+ query: Search query
126
+ subject: Optional subject filter
127
+ grade_level: Optional grade level filter
128
+
129
+ Returns:
130
+ List of matching resources
131
+ """
132
+ params = {"q": query}
133
+
134
+ if subject:
135
+ params["subject"] = subject
136
+
137
+ if grade_level:
138
+ params["grade"] = grade_level
139
+
140
+ headers = {}
141
+ if self.api_key:
142
+ headers["Authorization"] = f"Bearer {self.api_key}"
143
+
144
+ response = requests.get(f"{self.repository_url}/search", params=params, headers=headers)
145
+ response.raise_for_status()
146
+
147
+ return response.json().get("results", [])
148
+
149
+ def get_resource(self, resource_id: str) -> Dict[str, Any]:
150
+ """
151
+ Get details for a specific resource
152
+
153
+ Args:
154
+ resource_id: ID of the resource to fetch
155
+
156
+ Returns:
157
+ Resource details
158
+ """
159
+ headers = {}
160
+ if self.api_key:
161
+ headers["Authorization"] = f"Bearer {self.api_key}"
162
+
163
+ response = requests.get(f"{self.repository_url}/resources/{resource_id}", headers=headers)
164
+ response.raise_for_status()
165
+
166
+ return response.json()
167
+
168
+
169
+ class RTPTIntegration:
170
+ """Integration with real-time personalized tutoring platforms"""
171
+
172
+ def __init__(self, platform_url: str, client_id: str, client_secret: str):
173
+ """
174
+ Initialize RTPT integration
175
+
176
+ Args:
177
+ platform_url: Base URL for RTPT platform API
178
+ client_id: OAuth client ID
179
+ client_secret: OAuth client secret
180
+ """
181
+ self.platform_url = platform_url.rstrip('/')
182
+ self.client_id = client_id
183
+ self.client_secret = client_secret
184
+ self._access_token = None
185
+
186
+ def _get_access_token(self) -> str:
187
+ """Get OAuth access token"""
188
+ if self._access_token:
189
+ return self._access_token
190
+
191
+ data = {
192
+ "grant_type": "client_credentials",
193
+ "client_id": self.client_id,
194
+ "client_secret": self.client_secret
195
+ }
196
+
197
+ response = requests.post(f"{self.platform_url}/oauth/token", data=data)
198
+ response.raise_for_status()
199
+
200
+ token_data = response.json()
201
+ self._access_token = token_data["access_token"]
202
+ return self._access_token
203
+
204
+ def get_available_tutors(self, subject: str, level: str) -> List[Dict[str, Any]]:
205
+ """
206
+ Get available tutors for a subject and level
207
+
208
+ Args:
209
+ subject: Academic subject
210
+ level: Academic level
211
+
212
+ Returns:
213
+ List of available tutors
214
+ """
215
+ headers = {"Authorization": f"Bearer {self._get_access_token()}"}
216
+ params = {
217
+ "subject": subject,
218
+ "level": level
219
+ }
220
+
221
+ response = requests.get(f"{self.platform_url}/tutors/available", params=params, headers=headers)
222
+ response.raise_for_status()
223
+
224
+ return response.json()
225
+
226
+ def schedule_session(self, student_id: str, tutor_id: str,
227
+ subject: str, datetime_str: str) -> Dict[str, Any]:
228
+ """
229
+ Schedule a tutoring session
230
+
231
+ Args:
232
+ student_id: ID of the student
233
+ tutor_id: ID of the tutor
234
+ subject: Subject for tutoring
235
+ datetime_str: ISO format datetime for the session
236
+
237
+ Returns:
238
+ Session details
239
+ """
240
+ headers = {"Authorization": f"Bearer {self._get_access_token()}"}
241
+ data = {
242
+ "student_id": student_id,
243
+ "tutor_id": tutor_id,
244
+ "subject": subject,
245
+ "datetime": datetime_str
246
+ }
247
+
248
+ response = requests.post(f"{self.platform_url}/sessions", json=data, headers=headers)
249
+ response.raise_for_status()
250
+
251
+ return response.json()