Spaces:
Sleeping
Sleeping
Upload 30 files
Browse files- .gitattributes +35 -35
- Dockerfile +23 -0
- README.md +25 -10
- main.py +54 -0
- project_plan.md +135 -0
- requirements.txt +11 -0
- src/api/__pycache__/app.cpython-311.pyc +0 -0
- src/api/app.py +429 -0
- src/demo_apps/demo_ui.py +504 -0
- src/edtech_extensions/__pycache__/content_adaptation.cpython-311.pyc +0 -0
- src/edtech_extensions/__pycache__/progress_tracking.cpython-311.pyc +0 -0
- src/edtech_extensions/__pycache__/student_profile.cpython-311.pyc +0 -0
- src/edtech_extensions/assessment.py +596 -0
- src/edtech_extensions/content_adaptation.py +267 -0
- src/edtech_extensions/progress_tracking.py +450 -0
- src/edtech_extensions/student_profile.py +289 -0
- src/mcp_core/__init__.py +3 -0
- src/mcp_core/__pycache__/__init__.cpython-311.pyc +0 -0
- src/mcp_core/__pycache__/context.cpython-311.pyc +0 -0
- src/mcp_core/__pycache__/model_adapters.cpython-311.pyc +0 -0
- src/mcp_core/__pycache__/processor.cpython-311.pyc +0 -0
- src/mcp_core/__pycache__/protocol.cpython-311.pyc +0 -0
- src/mcp_core/context.py +109 -0
- src/mcp_core/model_adapters.py +313 -0
- src/mcp_core/persistence.py +505 -0
- src/mcp_core/processor.py +184 -0
- src/mcp_core/protocol.py +158 -0
- src/static/index.html +483 -0
- static/index.html +411 -0
- storage/students/293fc89c-8969-43ed-9a8f-f3e2131d97cd.json +20 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.11 image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
RUN mkdir -p /app/storage/contexts /app/storage/students /app/storage/progress/assessments /app/storage/progress/skills \
|
| 12 |
+
&& chmod -R 777 /app/storage
|
| 13 |
+
|
| 14 |
+
RUN mkdir -p /app/static \
|
| 15 |
+
&& cp -r /app/src/static/* /app/static/ 2>/dev/null || true \
|
| 16 |
+
&& chmod -R 755 /app/static
|
| 17 |
+
|
| 18 |
+
ENV PORT=7860
|
| 19 |
+
ENV PYTHONPATH=/app
|
| 20 |
+
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
CMD ["python", "main.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,25 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: MCP EdTech
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: MCP EdTech Project
|
| 3 |
+
emoji: 🎓
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
sdk_version: "latest"
|
| 8 |
+
app_file: main.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# MCP EdTech Project
|
| 13 |
+
|
| 14 |
+
A comprehensive implementation of the Model Context Protocol (MCP) for educational technology applications.
|
| 15 |
+
|
| 16 |
+
## Overview
|
| 17 |
+
|
| 18 |
+
The Model Context Protocol (MCP) is a standardized interface for AI model interactions that allows for consistent handling of context across different models, stateful conversations, and structured input/output formats. This project implements MCP specifically for EdTech applications, providing features such as:
|
| 19 |
+
|
| 20 |
+
- Context management for stateful conversations
|
| 21 |
+
- Student profile management
|
| 22 |
+
- Learning progress tracking
|
| 23 |
+
- Educational content adaptation
|
| 24 |
+
- Assessment and feedback systems
|
| 25 |
+
- Personalized learning path generation
|
main.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uvicorn
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from fastapi.staticfiles import StaticFiles
|
| 5 |
+
from fastapi.responses import RedirectResponse, HTMLResponse, JSONResponse
|
| 6 |
+
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
| 7 |
+
from fastapi.openapi.utils import get_openapi
|
| 8 |
+
|
| 9 |
+
from src.api.app import app as api_app
|
| 10 |
+
|
| 11 |
+
app = FastAPI(title="MCP EdTech", description="Model Context Protocol for Educational Technology")
|
| 12 |
+
|
| 13 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 14 |
+
|
| 15 |
+
@app.get("/", response_class=HTMLResponse)
|
| 16 |
+
async def serve_root():
|
| 17 |
+
try:
|
| 18 |
+
with open("static/index.html", "r") as f:
|
| 19 |
+
html_content = f.read()
|
| 20 |
+
return HTMLResponse(content=html_content)
|
| 21 |
+
except Exception as e:
|
| 22 |
+
return JSONResponse(content={
|
| 23 |
+
"name": "MCP EdTech API",
|
| 24 |
+
"version": "1.0.0",
|
| 25 |
+
"description": "Model Context Protocol implementation for EdTech applications",
|
| 26 |
+
"demo_ui": "Please visit /demo for the interactive demo"
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
@app.get("/demo", response_class=HTMLResponse)
|
| 30 |
+
async def serve_demo():
|
| 31 |
+
with open("static/index.html", "r") as f:
|
| 32 |
+
html_content = f.read()
|
| 33 |
+
return HTMLResponse(content=html_content)
|
| 34 |
+
|
| 35 |
+
for route in api_app.routes:
|
| 36 |
+
app.routes.append(route)
|
| 37 |
+
|
| 38 |
+
def custom_openapi():
|
| 39 |
+
if app.openapi_schema:
|
| 40 |
+
return app.openapi_schema
|
| 41 |
+
openapi_schema = get_openapi(
|
| 42 |
+
title="MCP EdTech API BY MHA",
|
| 43 |
+
version="1.0.0",
|
| 44 |
+
description="Model Context Protocol implementation for EdTech applications",
|
| 45 |
+
routes=app.routes,
|
| 46 |
+
)
|
| 47 |
+
app.openapi_schema = openapi_schema
|
| 48 |
+
return app.openapi_schema
|
| 49 |
+
|
| 50 |
+
app.openapi = custom_openapi
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
port = int(os.environ.get("PORT", 7860))
|
| 54 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
project_plan.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MCP EdTech Project Plan
|
| 2 |
+
|
| 3 |
+
## Project Overview
|
| 4 |
+
This project implements the Model Context Protocol (MCP) for educational technology applications. The implementation will provide a standardized way for EdTech applications to interact with various AI models while maintaining context and state across interactions.
|
| 5 |
+
|
| 6 |
+
## What is Model Context Protocol (MCP)?
|
| 7 |
+
The Model Context Protocol (MCP) is a standardized interface for AI model interactions that allows for:
|
| 8 |
+
- Consistent handling of context across different models
|
| 9 |
+
- Stateful conversations and interactions
|
| 10 |
+
- Structured input/output formats
|
| 11 |
+
- Model-agnostic implementations
|
| 12 |
+
- Enhanced security and privacy controls
|
| 13 |
+
|
| 14 |
+
## Project Architecture
|
| 15 |
+
|
| 16 |
+
### Core Components
|
| 17 |
+
1. **MCP Core**
|
| 18 |
+
- Protocol specification implementation
|
| 19 |
+
- Context management system
|
| 20 |
+
- State persistence layer
|
| 21 |
+
- Model interface adapters
|
| 22 |
+
|
| 23 |
+
2. **EdTech-Specific Extensions**
|
| 24 |
+
- Student profile management
|
| 25 |
+
- Learning progress tracking
|
| 26 |
+
- Educational content adaptation
|
| 27 |
+
- Assessment and feedback systems
|
| 28 |
+
|
| 29 |
+
3. **API Layer**
|
| 30 |
+
- RESTful endpoints
|
| 31 |
+
- WebSocket support for real-time interactions
|
| 32 |
+
- Authentication and authorization
|
| 33 |
+
- Rate limiting and usage monitoring
|
| 34 |
+
|
| 35 |
+
4. **Demo Applications**
|
| 36 |
+
- Interactive tutoring system
|
| 37 |
+
- Personalized learning path generator
|
| 38 |
+
- Knowledge assessment tool
|
| 39 |
+
|
| 40 |
+
## Technical Stack
|
| 41 |
+
- **Backend**: FastAPI
|
| 42 |
+
- **Database**: SQLite (development), PostgreSQL (production)
|
| 43 |
+
- **Authentication**: JWT-based auth
|
| 44 |
+
- **Documentation**: OpenAPI/Swagger, ReDoc
|
| 45 |
+
- **Testing**: Pytest
|
| 46 |
+
- **Deployment**: Docker, Hugging Face Spaces, Heroku/Render compatibility
|
| 47 |
+
|
| 48 |
+
## Implementation Plan
|
| 49 |
+
|
| 50 |
+
### Phase 1: Core MCP Implementation
|
| 51 |
+
- Define MCP specification for EdTech use cases
|
| 52 |
+
- Implement context management system
|
| 53 |
+
- Create model interface adapters
|
| 54 |
+
- Develop state persistence layer
|
| 55 |
+
|
| 56 |
+
### Phase 2: EdTech Extensions
|
| 57 |
+
- Implement student profile management
|
| 58 |
+
- Create learning progress tracking
|
| 59 |
+
- Develop educational content adaptation
|
| 60 |
+
- Build assessment and feedback systems
|
| 61 |
+
|
| 62 |
+
### Phase 3: API Development
|
| 63 |
+
- Design and implement RESTful endpoints
|
| 64 |
+
- Add WebSocket support
|
| 65 |
+
- Implement authentication and authorization
|
| 66 |
+
- Add rate limiting and usage monitoring
|
| 67 |
+
|
| 68 |
+
### Phase 4: Demo Applications
|
| 69 |
+
- Build interactive tutoring system
|
| 70 |
+
- Create personalized learning path generator
|
| 71 |
+
- Develop knowledge assessment tool
|
| 72 |
+
|
| 73 |
+
### Phase 5: Documentation and Deployment
|
| 74 |
+
- Write comprehensive documentation
|
| 75 |
+
- Create usage examples
|
| 76 |
+
- Prepare deployment configurations
|
| 77 |
+
- Deploy to Hugging Face and prepare for Heroku/Render
|
| 78 |
+
|
| 79 |
+
## API Endpoints
|
| 80 |
+
|
| 81 |
+
### MCP Core Endpoints
|
| 82 |
+
- `POST /api/v1/context/create` - Create a new context
|
| 83 |
+
- `GET /api/v1/context/{context_id}` - Get context information
|
| 84 |
+
- `PUT /api/v1/context/{context_id}` - Update context
|
| 85 |
+
- `DELETE /api/v1/context/{context_id}` - Delete context
|
| 86 |
+
- `POST /api/v1/interact` - Process an interaction within a context
|
| 87 |
+
|
| 88 |
+
### EdTech-Specific Endpoints
|
| 89 |
+
- `POST /api/v1/students` - Create student profile
|
| 90 |
+
- `GET /api/v1/students/{student_id}` - Get student information
|
| 91 |
+
- `PUT /api/v1/students/{student_id}` - Update student information
|
| 92 |
+
- `GET /api/v1/students/{student_id}/progress` - Get learning progress
|
| 93 |
+
- `POST /api/v1/assessments` - Create assessment
|
| 94 |
+
- `GET /api/v1/learning-paths/{student_id}` - Get personalized learning path
|
| 95 |
+
|
| 96 |
+
## Deployment Strategy
|
| 97 |
+
1. **Development Environment**
|
| 98 |
+
- Local development with SQLite
|
| 99 |
+
- Docker containerization for consistent environments
|
| 100 |
+
|
| 101 |
+
2. **Testing Environment**
|
| 102 |
+
- Automated testing with GitHub Actions
|
| 103 |
+
- Integration testing with test databases
|
| 104 |
+
|
| 105 |
+
3. **Production Deployment**
|
| 106 |
+
- Hugging Face Spaces for showcase
|
| 107 |
+
- Deployment scripts for Heroku and Render
|
| 108 |
+
- PostgreSQL for production database
|
| 109 |
+
|
| 110 |
+
## Documentation Plan
|
| 111 |
+
1. **Technical Documentation**
|
| 112 |
+
- MCP specification
|
| 113 |
+
- API reference
|
| 114 |
+
- Architecture overview
|
| 115 |
+
- Database schema
|
| 116 |
+
|
| 117 |
+
2. **User Documentation**
|
| 118 |
+
- Getting started guide
|
| 119 |
+
- Integration examples
|
| 120 |
+
- Deployment instructions
|
| 121 |
+
- Customization guide
|
| 122 |
+
|
| 123 |
+
3. **Demo Documentation**
|
| 124 |
+
- Use case examples
|
| 125 |
+
- Interactive tutorials
|
| 126 |
+
- Sample applications
|
| 127 |
+
|
| 128 |
+
## Timeline
|
| 129 |
+
- Phase 1: 1-2 weeks
|
| 130 |
+
- Phase 2: 1-2 weeks
|
| 131 |
+
- Phase 3: 1 week
|
| 132 |
+
- Phase 4: 1-2 weeks
|
| 133 |
+
- Phase 5: 1 week
|
| 134 |
+
|
| 135 |
+
Total estimated time: 5-8 weeks for full implementation
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.95.0
|
| 2 |
+
uvicorn>=0.21.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
sqlalchemy>=2.0.0
|
| 5 |
+
python-jose[cryptography]>=3.3.0
|
| 6 |
+
passlib[bcrypt]>=1.7.4
|
| 7 |
+
python-multipart>=0.0.6
|
| 8 |
+
pytest>=7.3.1
|
| 9 |
+
httpx>=0.24.0
|
| 10 |
+
aiosqlite>=0.19.0
|
| 11 |
+
websockets>=11.0.3
|
src/api/__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (21.4 kB). View file
|
|
|
src/api/app.py
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API Layer - FastAPI Application
|
| 3 |
+
|
| 4 |
+
This module implements the FastAPI application for the MCP EdTech project,
|
| 5 |
+
providing RESTful endpoints for interacting with the MCP system.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from typing import Dict, List, Optional, Any, Union
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
|
| 12 |
+
from fastapi import FastAPI, Depends, HTTPException, status, BackgroundTasks
|
| 13 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from fastapi.responses import JSONResponse, FileResponse
|
| 16 |
+
from fastapi.staticfiles import StaticFiles
|
| 17 |
+
from pydantic import BaseModel
|
| 18 |
+
|
| 19 |
+
from ..mcp_core.context import Context, ContextManager
|
| 20 |
+
from ..mcp_core.model_adapters import ModelRegistry, MockModelAdapter
|
| 21 |
+
from ..mcp_core.processor import InteractionProcessor
|
| 22 |
+
from ..mcp_core.protocol import (
|
| 23 |
+
MCPRequest, MCPResponse, InteractionType, ContentFormat,
|
| 24 |
+
EducationalLevel, LearningObjective, StudentProfile
|
| 25 |
+
)
|
| 26 |
+
from ..edtech_extensions.student_profile import StudentProfileManager
|
| 27 |
+
from ..edtech_extensions.progress_tracking import ProgressTracker, AssessmentType
|
| 28 |
+
from ..edtech_extensions.content_adaptation import ContentAdapter
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Initialize FastAPI app
|
| 32 |
+
app = FastAPI(
|
| 33 |
+
title="MCP EdTech API",
|
| 34 |
+
description="Model Context Protocol implementation for EdTech applications",
|
| 35 |
+
version="1.0.0"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Add CORS middleware
|
| 39 |
+
app.add_middleware(
|
| 40 |
+
CORSMiddleware,
|
| 41 |
+
allow_origins=["*"], # In production, specify actual origins
|
| 42 |
+
allow_credentials=True,
|
| 43 |
+
allow_methods=["*"],
|
| 44 |
+
allow_headers=["*"],
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Initialize core components
|
| 48 |
+
context_manager = ContextManager()
|
| 49 |
+
model_registry = ModelRegistry()
|
| 50 |
+
|
| 51 |
+
# Register mock model adapter for demonstration
|
| 52 |
+
mock_adapter = MockModelAdapter(model_id="mock-edtech-model")
|
| 53 |
+
model_registry.register_adapter("mock", mock_adapter)
|
| 54 |
+
|
| 55 |
+
# Initialize processor
|
| 56 |
+
interaction_processor = InteractionProcessor(context_manager, model_registry)
|
| 57 |
+
|
| 58 |
+
# Initialize EdTech extensions
|
| 59 |
+
os.makedirs("./storage", exist_ok=True)
|
| 60 |
+
student_profile_manager = StudentProfileManager()
|
| 61 |
+
progress_tracker = ProgressTracker()
|
| 62 |
+
content_adapter = ContentAdapter()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# API Models
|
| 66 |
+
|
| 67 |
+
class ContextCreate(BaseModel):
|
| 68 |
+
"""Request model for creating a new context."""
|
| 69 |
+
metadata: Dict[str, Any] = {}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class ContextUpdate(BaseModel):
|
| 73 |
+
"""Request model for updating a context."""
|
| 74 |
+
metadata: Dict[str, Any] = {}
|
| 75 |
+
state: Dict[str, Any] = {}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class InteractionRequest(BaseModel):
|
| 79 |
+
"""Request model for processing an interaction."""
|
| 80 |
+
context_id: Optional[str] = None
|
| 81 |
+
interaction_type: InteractionType
|
| 82 |
+
content: Dict[str, Any]
|
| 83 |
+
format: ContentFormat = ContentFormat.TEXT
|
| 84 |
+
metadata: Dict[str, Any] = {}
|
| 85 |
+
model_name: str = "mock" # Default to mock model
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class StudentProfileCreate(BaseModel):
|
| 89 |
+
"""Request model for creating a student profile."""
|
| 90 |
+
name: str
|
| 91 |
+
educational_level: EducationalLevel
|
| 92 |
+
learning_style: Optional[str] = None
|
| 93 |
+
interests: List[str] = []
|
| 94 |
+
strengths: List[str] = []
|
| 95 |
+
areas_for_improvement: List[str] = []
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class StudentProfileUpdate(BaseModel):
|
| 99 |
+
"""Request model for updating a student profile."""
|
| 100 |
+
name: Optional[str] = None
|
| 101 |
+
educational_level: Optional[EducationalLevel] = None
|
| 102 |
+
learning_style: Optional[str] = None
|
| 103 |
+
interests: Optional[List[str]] = None
|
| 104 |
+
strengths: Optional[List[str]] = None
|
| 105 |
+
areas_for_improvement: Optional[List[str]] = None
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class AssessmentCreate(BaseModel):
|
| 109 |
+
"""Request model for creating an assessment."""
|
| 110 |
+
student_id: str
|
| 111 |
+
assessment_type: AssessmentType
|
| 112 |
+
title: str
|
| 113 |
+
score: float
|
| 114 |
+
max_score: float
|
| 115 |
+
objectives: List[str]
|
| 116 |
+
feedback: str = ""
|
| 117 |
+
details: Dict[str, Any] = {}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class ContentAdaptRequest(BaseModel):
|
| 121 |
+
"""Request model for adapting content."""
|
| 122 |
+
content: Dict[str, Any]
|
| 123 |
+
student_id: str
|
| 124 |
+
learning_objectives: List[str]
|
| 125 |
+
content_format: ContentFormat = ContentFormat.TEXT
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# API Routes
|
| 129 |
+
|
| 130 |
+
@app.get("/")
|
| 131 |
+
async def root():
|
| 132 |
+
"""Root endpoint returning API information."""
|
| 133 |
+
return {
|
| 134 |
+
"name": "MCP EdTech API",
|
| 135 |
+
"version": "1.0.0",
|
| 136 |
+
"description": "Model Context Protocol implementation for EdTech applications"
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# Context endpoints
|
| 141 |
+
|
| 142 |
+
@app.post("/api/v1/context/create", response_model=Dict[str, Any])
|
| 143 |
+
async def create_context(request: ContextCreate):
|
| 144 |
+
"""Create a new context."""
|
| 145 |
+
context = context_manager.create_context(metadata=request.metadata)
|
| 146 |
+
return context.to_dict()
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@app.get("/api/v1/context/{context_id}", response_model=Dict[str, Any])
|
| 150 |
+
async def get_context(context_id: str):
|
| 151 |
+
"""Get context information."""
|
| 152 |
+
context = context_manager.get_context(context_id)
|
| 153 |
+
if not context:
|
| 154 |
+
raise HTTPException(status_code=404, detail="Context not found")
|
| 155 |
+
return context.to_dict()
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@app.put("/api/v1/context/{context_id}", response_model=Dict[str, Any])
|
| 159 |
+
async def update_context(context_id: str, request: ContextUpdate):
|
| 160 |
+
"""Update context."""
|
| 161 |
+
context = context_manager.get_context(context_id)
|
| 162 |
+
if not context:
|
| 163 |
+
raise HTTPException(status_code=404, detail="Context not found")
|
| 164 |
+
|
| 165 |
+
if request.metadata:
|
| 166 |
+
context.metadata.update(request.metadata)
|
| 167 |
+
|
| 168 |
+
if request.state:
|
| 169 |
+
context.update_state(request.state)
|
| 170 |
+
|
| 171 |
+
return context.to_dict()
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@app.delete("/api/v1/context/{context_id}", response_model=Dict[str, str])
|
| 175 |
+
async def delete_context(context_id: str):
|
| 176 |
+
"""Delete context."""
|
| 177 |
+
success = context_manager.delete_context(context_id)
|
| 178 |
+
if not success:
|
| 179 |
+
raise HTTPException(status_code=404, detail="Context not found")
|
| 180 |
+
return {"status": "deleted", "context_id": context_id}
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# Interaction endpoint
|
| 184 |
+
|
| 185 |
+
@app.post("/api/v1/interact", response_model=Dict[str, Any])
|
| 186 |
+
async def process_interaction(request: InteractionRequest):
|
| 187 |
+
"""Process an interaction."""
|
| 188 |
+
try:
|
| 189 |
+
mcp_request = MCPRequest(
|
| 190 |
+
context_id=request.context_id,
|
| 191 |
+
interaction_type=request.interaction_type,
|
| 192 |
+
content=request.content,
|
| 193 |
+
format=request.format,
|
| 194 |
+
metadata=request.metadata
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
response = await interaction_processor.process_interaction(
|
| 198 |
+
mcp_request,
|
| 199 |
+
request.model_name
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
return response.dict()
|
| 203 |
+
except ValueError as e:
|
| 204 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 205 |
+
except Exception as e:
|
| 206 |
+
raise HTTPException(status_code=500, detail=f"Error processing interaction: {str(e)}")
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# Student profile endpoints
|
| 210 |
+
|
| 211 |
+
@app.post("/api/v1/students", response_model=Dict[str, Any])
|
| 212 |
+
async def create_student_profile(request: StudentProfileCreate):
|
| 213 |
+
"""Create student profile."""
|
| 214 |
+
profile = await student_profile_manager.create_profile(
|
| 215 |
+
name=request.name,
|
| 216 |
+
educational_level=request.educational_level,
|
| 217 |
+
learning_style=request.learning_style,
|
| 218 |
+
interests=request.interests,
|
| 219 |
+
strengths=request.strengths,
|
| 220 |
+
areas_for_improvement=request.areas_for_improvement
|
| 221 |
+
)
|
| 222 |
+
return profile.dict()
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@app.get("/api/v1/students/{student_id}", response_model=Dict[str, Any])
|
| 226 |
+
async def get_student_profile(student_id: str):
|
| 227 |
+
"""Get student information."""
|
| 228 |
+
profile = await student_profile_manager.get_profile(student_id)
|
| 229 |
+
if not profile:
|
| 230 |
+
raise HTTPException(status_code=404, detail="Student profile not found")
|
| 231 |
+
return profile.dict()
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
@app.put("/api/v1/students/{student_id}", response_model=Dict[str, Any])
|
| 235 |
+
async def update_student_profile(student_id: str, request: StudentProfileUpdate):
|
| 236 |
+
"""Update student information."""
|
| 237 |
+
updates = {k: v for k, v in request.dict().items() if v is not None}
|
| 238 |
+
profile = await student_profile_manager.update_profile(student_id, updates)
|
| 239 |
+
if not profile:
|
| 240 |
+
raise HTTPException(status_code=404, detail="Student profile not found")
|
| 241 |
+
return profile.dict()
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
# Progress tracking endpoints
|
| 245 |
+
|
| 246 |
+
@app.get("/api/v1/students/{student_id}/progress", response_model=Dict[str, Any])
|
| 247 |
+
async def get_student_progress(student_id: str):
|
| 248 |
+
"""Get learning progress."""
|
| 249 |
+
# Check if student exists
|
| 250 |
+
profile = await student_profile_manager.get_profile(student_id)
|
| 251 |
+
if not profile:
|
| 252 |
+
raise HTTPException(status_code=404, detail="Student profile not found")
|
| 253 |
+
|
| 254 |
+
summary = await progress_tracker.get_progress_summary(student_id)
|
| 255 |
+
return summary
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
@app.post("/api/v1/assessments", response_model=Dict[str, Any])
|
| 259 |
+
async def create_assessment(request: AssessmentCreate):
|
| 260 |
+
"""Create assessment."""
|
| 261 |
+
# Check if student exists
|
| 262 |
+
profile = await student_profile_manager.get_profile(request.student_id)
|
| 263 |
+
if not profile:
|
| 264 |
+
raise HTTPException(status_code=404, detail="Student profile not found")
|
| 265 |
+
|
| 266 |
+
result = await progress_tracker.record_assessment(
|
| 267 |
+
student_id=request.student_id,
|
| 268 |
+
assessment_type=request.assessment_type,
|
| 269 |
+
title=request.title,
|
| 270 |
+
score=request.score,
|
| 271 |
+
max_score=request.max_score,
|
| 272 |
+
objectives=request.objectives,
|
| 273 |
+
feedback=request.feedback,
|
| 274 |
+
details=request.details
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
# Update student profile with completed objectives if score is good
|
| 278 |
+
if request.score / request.max_score >= 0.7: # 70% or better
|
| 279 |
+
for objective_id in request.objectives:
|
| 280 |
+
await student_profile_manager.add_completed_objective(
|
| 281 |
+
request.student_id,
|
| 282 |
+
objective_id
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
return result.dict()
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
@app.get("/api/v1/students/{student_id}/assessments", response_model=List[Dict[str, Any]])
|
| 289 |
+
async def list_student_assessments(student_id: str):
|
| 290 |
+
"""List assessments for a student."""
|
| 291 |
+
# Check if student exists
|
| 292 |
+
profile = await student_profile_manager.get_profile(student_id)
|
| 293 |
+
if not profile:
|
| 294 |
+
raise HTTPException(status_code=404, detail="Student profile not found")
|
| 295 |
+
|
| 296 |
+
results = await progress_tracker.list_assessments(student_id)
|
| 297 |
+
return [result.dict() for result in results]
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
# Content adaptation endpoint
|
| 301 |
+
|
| 302 |
+
@app.post("/api/v1/content/adapt", response_model=Dict[str, Any])
|
| 303 |
+
async def adapt_content(request: ContentAdaptRequest):
|
| 304 |
+
"""Adapt content for a student."""
|
| 305 |
+
# Get student profile
|
| 306 |
+
profile = await student_profile_manager.get_profile(request.student_id)
|
| 307 |
+
if not profile:
|
| 308 |
+
raise HTTPException(status_code=404, detail="Student profile not found")
|
| 309 |
+
|
| 310 |
+
# Create learning objectives (in a real app, these would come from a database)
|
| 311 |
+
learning_objectives = [
|
| 312 |
+
LearningObjective(
|
| 313 |
+
id=obj_id,
|
| 314 |
+
description=f"Learning objective {obj_id}",
|
| 315 |
+
taxonomy_level="understand",
|
| 316 |
+
subject_area="general",
|
| 317 |
+
prerequisites=[]
|
| 318 |
+
)
|
| 319 |
+
for obj_id in request.learning_objectives
|
| 320 |
+
]
|
| 321 |
+
|
| 322 |
+
# Adapt content
|
| 323 |
+
adapted_content = await content_adapter.adapt_content(
|
| 324 |
+
content=request.content,
|
| 325 |
+
student_profile=profile,
|
| 326 |
+
learning_objectives=learning_objectives,
|
| 327 |
+
content_format=request.content_format
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
return adapted_content
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
# Learning path endpoint
|
| 334 |
+
|
| 335 |
+
@app.get("/api/v1/learning-paths/{student_id}", response_model=Dict[str, Any])
|
| 336 |
+
async def get_learning_path(student_id: str):
|
| 337 |
+
"""Get personalized learning path."""
|
| 338 |
+
# Check if student exists
|
| 339 |
+
profile = await student_profile_manager.get_profile(student_id)
|
| 340 |
+
if not profile:
|
| 341 |
+
raise HTTPException(status_code=404, detail="Student profile not found")
|
| 342 |
+
|
| 343 |
+
# In a real implementation, this would generate a personalized learning path
|
| 344 |
+
# based on the student's profile, progress, and available learning materials.
|
| 345 |
+
# For this demo, we'll return a mock learning path.
|
| 346 |
+
|
| 347 |
+
# Get student's completed objectives
|
| 348 |
+
completed_objectives = profile.completed_objectives
|
| 349 |
+
current_objectives = profile.current_objectives
|
| 350 |
+
|
| 351 |
+
# Mock learning path
|
| 352 |
+
return {
|
| 353 |
+
"student_id": student_id,
|
| 354 |
+
"student_name": profile.name,
|
| 355 |
+
"educational_level": profile.educational_level,
|
| 356 |
+
"completed_objectives": completed_objectives,
|
| 357 |
+
"current_objectives": current_objectives,
|
| 358 |
+
"recommended_objectives": [
|
| 359 |
+
{
|
| 360 |
+
"id": f"obj-{i}",
|
| 361 |
+
"title": f"Recommended Objective {i}",
|
| 362 |
+
"description": f"This is a recommended learning objective for {profile.name}",
|
| 363 |
+
"estimated_time": f"{i*2} hours",
|
| 364 |
+
"difficulty": "intermediate"
|
| 365 |
+
}
|
| 366 |
+
for i in range(1, 4)
|
| 367 |
+
],
|
| 368 |
+
"recommended_resources": [
|
| 369 |
+
{
|
| 370 |
+
"id": f"res-{i}",
|
| 371 |
+
"title": f"Resource {i}",
|
| 372 |
+
"type": "video" if i % 2 == 0 else "article",
|
| 373 |
+
"url": f"https://example.com/resource-{i}",
|
| 374 |
+
"description": f"This is a recommended resource for {profile.name}"
|
| 375 |
+
}
|
| 376 |
+
for i in range(1, 6)
|
| 377 |
+
],
|
| 378 |
+
"generated_at": datetime.utcnow().isoformat()
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
# Model information endpoints
|
| 383 |
+
|
| 384 |
+
@app.get("/api/v1/models", response_model=List[str])
|
| 385 |
+
async def list_models():
|
| 386 |
+
"""List available models."""
|
| 387 |
+
return model_registry.list_adapters()
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@app.get("/api/v1/models/{model_name}/capabilities", response_model=Dict[str, Any])
|
| 391 |
+
async def get_model_capabilities(model_name: str):
|
| 392 |
+
"""Get model capabilities."""
|
| 393 |
+
capabilities = model_registry.get_adapter_capabilities(model_name)
|
| 394 |
+
if not capabilities:
|
| 395 |
+
raise HTTPException(status_code=404, detail="Model not found")
|
| 396 |
+
return capabilities
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
# Health check endpoint
|
| 400 |
+
|
| 401 |
+
@app.get("/health", response_model=Dict[str, str])
|
| 402 |
+
async def health_check():
|
| 403 |
+
"""Health check endpoint."""
|
| 404 |
+
return {
|
| 405 |
+
"status": "healthy",
|
| 406 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
# Demo UI route
|
| 411 |
+
@app.get("/demo", response_class=FileResponse)
|
| 412 |
+
async def demo_ui():
|
| 413 |
+
"""Serve the demo UI."""
|
| 414 |
+
return FileResponse("static/index.html")
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# Serve static files for demo UI
|
| 418 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
# Main function to run the app
|
| 422 |
+
def start():
|
| 423 |
+
"""Start the FastAPI application using uvicorn."""
|
| 424 |
+
import uvicorn
|
| 425 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
if __name__ == "__main__":
|
| 429 |
+
start()
|
src/demo_apps/demo_ui.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Demo application for the MCP EdTech project.
|
| 3 |
+
|
| 4 |
+
This module provides a simple web interface for demonstrating the MCP EdTech functionality.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Create static directory for demo UI
|
| 11 |
+
static_dir = Path(__file__).parent.parent / "static"
|
| 12 |
+
static_dir.mkdir(exist_ok=True)
|
| 13 |
+
|
| 14 |
+
# Create index.html for the demo UI
|
| 15 |
+
index_html = """<!DOCTYPE html>
|
| 16 |
+
<html lang="en">
|
| 17 |
+
<head>
|
| 18 |
+
<meta charset="UTF-8">
|
| 19 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 20 |
+
<title>MCP EdTech Demo</title>
|
| 21 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 22 |
+
<style>
|
| 23 |
+
body {
|
| 24 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 25 |
+
padding-top: 20px;
|
| 26 |
+
background-color: #f8f9fa;
|
| 27 |
+
}
|
| 28 |
+
.navbar {
|
| 29 |
+
background-color: #4a6fa5 !important;
|
| 30 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 31 |
+
}
|
| 32 |
+
.navbar-brand {
|
| 33 |
+
font-weight: bold;
|
| 34 |
+
color: white !important;
|
| 35 |
+
}
|
| 36 |
+
.nav-link {
|
| 37 |
+
color: rgba(255,255,255,0.85) !important;
|
| 38 |
+
}
|
| 39 |
+
.nav-link:hover {
|
| 40 |
+
color: white !important;
|
| 41 |
+
}
|
| 42 |
+
.card {
|
| 43 |
+
border-radius: 10px;
|
| 44 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 45 |
+
margin-bottom: 20px;
|
| 46 |
+
transition: transform 0.3s;
|
| 47 |
+
}
|
| 48 |
+
.card:hover {
|
| 49 |
+
transform: translateY(-5px);
|
| 50 |
+
}
|
| 51 |
+
.card-header {
|
| 52 |
+
background-color: #4a6fa5;
|
| 53 |
+
color: white;
|
| 54 |
+
font-weight: bold;
|
| 55 |
+
border-radius: 10px 10px 0 0 !important;
|
| 56 |
+
}
|
| 57 |
+
.btn-primary {
|
| 58 |
+
background-color: #4a6fa5;
|
| 59 |
+
border-color: #4a6fa5;
|
| 60 |
+
}
|
| 61 |
+
.btn-primary:hover {
|
| 62 |
+
background-color: #3a5a8f;
|
| 63 |
+
border-color: #3a5a8f;
|
| 64 |
+
}
|
| 65 |
+
.api-url {
|
| 66 |
+
font-family: monospace;
|
| 67 |
+
background-color: #f1f1f1;
|
| 68 |
+
padding: 5px;
|
| 69 |
+
border-radius: 4px;
|
| 70 |
+
}
|
| 71 |
+
.response-area {
|
| 72 |
+
background-color: #f8f9fa;
|
| 73 |
+
border: 1px solid #dee2e6;
|
| 74 |
+
border-radius: 5px;
|
| 75 |
+
padding: 15px;
|
| 76 |
+
max-height: 300px;
|
| 77 |
+
overflow-y: auto;
|
| 78 |
+
font-family: monospace;
|
| 79 |
+
}
|
| 80 |
+
.footer {
|
| 81 |
+
margin-top: 50px;
|
| 82 |
+
padding: 20px 0;
|
| 83 |
+
background-color: #343a40;
|
| 84 |
+
color: white;
|
| 85 |
+
}
|
| 86 |
+
#loading {
|
| 87 |
+
display: none;
|
| 88 |
+
}
|
| 89 |
+
.spinner-border {
|
| 90 |
+
width: 1rem;
|
| 91 |
+
height: 1rem;
|
| 92 |
+
}
|
| 93 |
+
</style>
|
| 94 |
+
</head>
|
| 95 |
+
<body>
|
| 96 |
+
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
| 97 |
+
<div class="container">
|
| 98 |
+
<a class="navbar-brand" href="#">MCP EdTech</a>
|
| 99 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
| 100 |
+
<span class="navbar-toggler-icon"></span>
|
| 101 |
+
</button>
|
| 102 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 103 |
+
<ul class="navbar-nav">
|
| 104 |
+
<li class="nav-item">
|
| 105 |
+
<a class="nav-link active" href="#home">Home</a>
|
| 106 |
+
</li>
|
| 107 |
+
<li class="nav-item">
|
| 108 |
+
<a class="nav-link" href="#api-demo">API Demo</a>
|
| 109 |
+
</li>
|
| 110 |
+
<li class="nav-item">
|
| 111 |
+
<a class="nav-link" href="#documentation">Documentation</a>
|
| 112 |
+
</li>
|
| 113 |
+
</ul>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</nav>
|
| 117 |
+
|
| 118 |
+
<div class="container" id="home">
|
| 119 |
+
<div class="row mb-4">
|
| 120 |
+
<div class="col-12">
|
| 121 |
+
<div class="card">
|
| 122 |
+
<div class="card-header">
|
| 123 |
+
Model Context Protocol (MCP) for EdTech
|
| 124 |
+
</div>
|
| 125 |
+
<div class="card-body">
|
| 126 |
+
<h5 class="card-title">Welcome to the MCP EdTech Demo</h5>
|
| 127 |
+
<p class="card-text">
|
| 128 |
+
This demo showcases the Model Context Protocol (MCP) implementation for educational technology applications.
|
| 129 |
+
MCP provides a standardized way for EdTech applications to interact with various AI models while maintaining
|
| 130 |
+
context and state across interactions.
|
| 131 |
+
</p>
|
| 132 |
+
<p>
|
| 133 |
+
Key features of this implementation:
|
| 134 |
+
</p>
|
| 135 |
+
<ul>
|
| 136 |
+
<li>Context management for stateful conversations</li>
|
| 137 |
+
<li>Student profile management</li>
|
| 138 |
+
<li>Learning progress tracking</li>
|
| 139 |
+
<li>Content adaptation based on student profiles</li>
|
| 140 |
+
<li>Assessment and feedback systems</li>
|
| 141 |
+
</ul>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div class="row mb-4" id="api-demo">
|
| 148 |
+
<div class="col-12">
|
| 149 |
+
<div class="card">
|
| 150 |
+
<div class="card-header">
|
| 151 |
+
API Demo
|
| 152 |
+
</div>
|
| 153 |
+
<div class="card-body">
|
| 154 |
+
<h5 class="card-title">Try the MCP API</h5>
|
| 155 |
+
|
| 156 |
+
<div class="mb-4">
|
| 157 |
+
<h6>1. Create a Context</h6>
|
| 158 |
+
<p>First, let's create a new context for our interaction:</p>
|
| 159 |
+
<div class="mb-3">
|
| 160 |
+
<button id="createContextBtn" class="btn btn-primary">Create Context</button>
|
| 161 |
+
<span id="loading-context" class="ms-2" style="display: none;">
|
| 162 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 163 |
+
</span>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="response-area" id="contextResponse">
|
| 166 |
+
<!-- Context response will appear here -->
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<div class="mb-4">
|
| 171 |
+
<h6>2. Process an Interaction</h6>
|
| 172 |
+
<p>Now, let's process an interaction using the context we created:</p>
|
| 173 |
+
<div class="mb-3">
|
| 174 |
+
<label for="interactionText" class="form-label">Enter your question or prompt:</label>
|
| 175 |
+
<input type="text" class="form-control" id="interactionText"
|
| 176 |
+
placeholder="e.g., Explain the concept of photosynthesis">
|
| 177 |
+
</div>
|
| 178 |
+
<div class="mb-3">
|
| 179 |
+
<button id="interactBtn" class="btn btn-primary">Process Interaction</button>
|
| 180 |
+
<span id="loading-interaction" class="ms-2" style="display: none;">
|
| 181 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 182 |
+
</span>
|
| 183 |
+
</div>
|
| 184 |
+
<div class="response-area" id="interactionResponse">
|
| 185 |
+
<!-- Interaction response will appear here -->
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div class="mb-4">
|
| 190 |
+
<h6>3. Create a Student Profile</h6>
|
| 191 |
+
<p>Let's create a student profile:</p>
|
| 192 |
+
<div class="mb-3">
|
| 193 |
+
<label for="studentName" class="form-label">Student Name:</label>
|
| 194 |
+
<input type="text" class="form-control" id="studentName" placeholder="John Doe">
|
| 195 |
+
</div>
|
| 196 |
+
<div class="mb-3">
|
| 197 |
+
<label for="educationalLevel" class="form-label">Educational Level:</label>
|
| 198 |
+
<select class="form-select" id="educationalLevel">
|
| 199 |
+
<option value="elementary">Elementary</option>
|
| 200 |
+
<option value="middle_school">Middle School</option>
|
| 201 |
+
<option value="high_school">High School</option>
|
| 202 |
+
<option value="undergraduate">Undergraduate</option>
|
| 203 |
+
<option value="graduate">Graduate</option>
|
| 204 |
+
<option value="professional">Professional</option>
|
| 205 |
+
</select>
|
| 206 |
+
</div>
|
| 207 |
+
<div class="mb-3">
|
| 208 |
+
<button id="createStudentBtn" class="btn btn-primary">Create Student Profile</button>
|
| 209 |
+
<span id="loading-student" class="ms-2" style="display: none;">
|
| 210 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 211 |
+
</span>
|
| 212 |
+
</div>
|
| 213 |
+
<div class="response-area" id="studentResponse">
|
| 214 |
+
<!-- Student profile response will appear here -->
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<div class="mb-4">
|
| 219 |
+
<h6>4. Adapt Content for Student</h6>
|
| 220 |
+
<p>Now, let's adapt some content for the student:</p>
|
| 221 |
+
<div class="mb-3">
|
| 222 |
+
<label for="contentText" class="form-label">Content to adapt:</label>
|
| 223 |
+
<textarea class="form-control" id="contentText" rows="3"
|
| 224 |
+
placeholder="The process of photosynthesis involves the conversion of light energy into chemical energy that can be used by plants and other organisms."></textarea>
|
| 225 |
+
</div>
|
| 226 |
+
<div class="mb-3">
|
| 227 |
+
<button id="adaptContentBtn" class="btn btn-primary">Adapt Content</button>
|
| 228 |
+
<span id="loading-content" class="ms-2" style="display: none;">
|
| 229 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 230 |
+
</span>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="response-area" id="contentResponse">
|
| 233 |
+
<!-- Content adaptation response will appear here -->
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<div class="row mb-4" id="documentation">
|
| 242 |
+
<div class="col-12">
|
| 243 |
+
<div class="card">
|
| 244 |
+
<div class="card-header">
|
| 245 |
+
API Documentation
|
| 246 |
+
</div>
|
| 247 |
+
<div class="card-body">
|
| 248 |
+
<h5 class="card-title">Available Endpoints</h5>
|
| 249 |
+
|
| 250 |
+
<div class="mb-3">
|
| 251 |
+
<h6>Context Management</h6>
|
| 252 |
+
<ul>
|
| 253 |
+
<li><span class="api-url">POST /api/v1/context/create</span> - Create a new context</li>
|
| 254 |
+
<li><span class="api-url">GET /api/v1/context/{context_id}</span> - Get context information</li>
|
| 255 |
+
<li><span class="api-url">PUT /api/v1/context/{context_id}</span> - Update context</li>
|
| 256 |
+
<li><span class="api-url">DELETE /api/v1/context/{context_id}</span> - Delete context</li>
|
| 257 |
+
</ul>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<div class="mb-3">
|
| 261 |
+
<h6>Interaction</h6>
|
| 262 |
+
<ul>
|
| 263 |
+
<li><span class="api-url">POST /api/v1/interact</span> - Process an interaction</li>
|
| 264 |
+
</ul>
|
| 265 |
+
</div>
|
| 266 |
+
|
| 267 |
+
<div class="mb-3">
|
| 268 |
+
<h6>Student Profiles</h6>
|
| 269 |
+
<ul>
|
| 270 |
+
<li><span class="api-url">POST /api/v1/students</span> - Create student profile</li>
|
| 271 |
+
<li><span class="api-url">GET /api/v1/students/{student_id}</span> - Get student information</li>
|
| 272 |
+
<li><span class="api-url">PUT /api/v1/students/{student_id}</span> - Update student information</li>
|
| 273 |
+
</ul>
|
| 274 |
+
</div>
|
| 275 |
+
|
| 276 |
+
<div class="mb-3">
|
| 277 |
+
<h6>Progress Tracking</h6>
|
| 278 |
+
<ul>
|
| 279 |
+
<li><span class="api-url">GET /api/v1/students/{student_id}/progress</span> - Get learning progress</li>
|
| 280 |
+
<li><span class="api-url">POST /api/v1/assessments</span> - Create assessment</li>
|
| 281 |
+
<li><span class="api-url">GET /api/v1/students/{student_id}/assessments</span> - List student assessments</li>
|
| 282 |
+
</ul>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<div class="mb-3">
|
| 286 |
+
<h6>Content Adaptation</h6>
|
| 287 |
+
<ul>
|
| 288 |
+
<li><span class="api-url">POST /api/v1/content/adapt</span> - Adapt content for a student</li>
|
| 289 |
+
</ul>
|
| 290 |
+
</div>
|
| 291 |
+
|
| 292 |
+
<div class="mb-3">
|
| 293 |
+
<h6>Learning Paths</h6>
|
| 294 |
+
<ul>
|
| 295 |
+
<li><span class="api-url">GET /api/v1/learning-paths/{student_id}</span> - Get personalized learning path</li>
|
| 296 |
+
</ul>
|
| 297 |
+
</div>
|
| 298 |
+
|
| 299 |
+
<div class="mb-3">
|
| 300 |
+
<h6>Models</h6>
|
| 301 |
+
<ul>
|
| 302 |
+
<li><span class="api-url">GET /api/v1/models</span> - List available models</li>
|
| 303 |
+
<li><span class="api-url">GET /api/v1/models/{model_name}/capabilities</span> - Get model capabilities</li>
|
| 304 |
+
</ul>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<p class="mt-4">
|
| 308 |
+
For complete API documentation, visit the <a href="/docs" target="_blank">Swagger UI</a> or
|
| 309 |
+
<a href="/redoc" target="_blank">ReDoc</a> pages.
|
| 310 |
+
</p>
|
| 311 |
+
</div>
|
| 312 |
+
</div>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<footer class="footer">
|
| 318 |
+
<div class="container">
|
| 319 |
+
<div class="row">
|
| 320 |
+
<div class="col-md-6">
|
| 321 |
+
<h5>MCP EdTech Project</h5>
|
| 322 |
+
<p>An open-source implementation of the Model Context Protocol for educational technology applications.</p>
|
| 323 |
+
</div>
|
| 324 |
+
<div class="col-md-6 text-md-end">
|
| 325 |
+
<p>© 2025 MCP EdTech Project</p>
|
| 326 |
+
<p>Licensed under MIT License</p>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
</footer>
|
| 331 |
+
|
| 332 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
| 333 |
+
<script>
|
| 334 |
+
// Store context and student IDs
|
| 335 |
+
let currentContextId = null;
|
| 336 |
+
let currentStudentId = null;
|
| 337 |
+
|
| 338 |
+
// Function to format JSON responses
|
| 339 |
+
function formatJSON(json) {
|
| 340 |
+
return JSON.stringify(json, null, 2);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// Create Context
|
| 344 |
+
document.getElementById('createContextBtn').addEventListener('click', async () => {
|
| 345 |
+
const loadingElement = document.getElementById('loading-context');
|
| 346 |
+
loadingElement.style.display = 'inline';
|
| 347 |
+
|
| 348 |
+
try {
|
| 349 |
+
const response = await fetch('/api/v1/context/create', {
|
| 350 |
+
method: 'POST',
|
| 351 |
+
headers: {
|
| 352 |
+
'Content-Type': 'application/json'
|
| 353 |
+
},
|
| 354 |
+
body: JSON.stringify({
|
| 355 |
+
metadata: {
|
| 356 |
+
source: 'demo-ui',
|
| 357 |
+
created_by: 'demo-user'
|
| 358 |
+
}
|
| 359 |
+
})
|
| 360 |
+
});
|
| 361 |
+
|
| 362 |
+
const data = await response.json();
|
| 363 |
+
currentContextId = data.context_id;
|
| 364 |
+
|
| 365 |
+
document.getElementById('contextResponse').innerText = formatJSON(data);
|
| 366 |
+
} catch (error) {
|
| 367 |
+
document.getElementById('contextResponse').innerText = `Error: ${error.message}`;
|
| 368 |
+
} finally {
|
| 369 |
+
loadingElement.style.display = 'none';
|
| 370 |
+
}
|
| 371 |
+
});
|
| 372 |
+
|
| 373 |
+
// Process Interaction
|
| 374 |
+
document.getElementById('interactBtn').addEventListener('click', async () => {
|
| 375 |
+
if (!currentContextId) {
|
| 376 |
+
alert('Please create a context first!');
|
| 377 |
+
return;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
const interactionText = document.getElementById('interactionText').value;
|
| 381 |
+
if (!interactionText) {
|
| 382 |
+
alert('Please enter a question or prompt!');
|
| 383 |
+
return;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
const loadingElement = document.getElementById('loading-interaction');
|
| 387 |
+
loadingElement.style.display = 'inline';
|
| 388 |
+
|
| 389 |
+
try {
|
| 390 |
+
const response = await fetch('/api/v1/interact', {
|
| 391 |
+
method: 'POST',
|
| 392 |
+
headers: {
|
| 393 |
+
'Content-Type': 'application/json'
|
| 394 |
+
},
|
| 395 |
+
body: JSON.stringify({
|
| 396 |
+
context_id: currentContextId,
|
| 397 |
+
interaction_type: 'text',
|
| 398 |
+
content: {
|
| 399 |
+
text: interactionText
|
| 400 |
+
},
|
| 401 |
+
format: 'text',
|
| 402 |
+
model_name: 'mock'
|
| 403 |
+
})
|
| 404 |
+
});
|
| 405 |
+
|
| 406 |
+
const data = await response.json();
|
| 407 |
+
document.getElementById('interactionResponse').innerText = formatJSON(data);
|
| 408 |
+
} catch (error) {
|
| 409 |
+
document.getElementById('interactionResponse').innerText = `Error: ${error.message}`;
|
| 410 |
+
} finally {
|
| 411 |
+
loadingElement.style.display = 'none';
|
| 412 |
+
}
|
| 413 |
+
});
|
| 414 |
+
|
| 415 |
+
// Create Student Profile
|
| 416 |
+
document.getElementById('createStudentBtn').addEventListener('click', async () => {
|
| 417 |
+
const studentName = document.getElementById('studentName').value;
|
| 418 |
+
if (!studentName) {
|
| 419 |
+
alert('Please enter a student name!');
|
| 420 |
+
return;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
const educationalLevel = document.getElementById('educationalLevel').value;
|
| 424 |
+
|
| 425 |
+
const loadingElement = document.getElementById('loading-student');
|
| 426 |
+
loadingElement.style.display = 'inline';
|
| 427 |
+
|
| 428 |
+
try {
|
| 429 |
+
const response = await fetch('/api/v1/students', {
|
| 430 |
+
method: 'POST',
|
| 431 |
+
headers: {
|
| 432 |
+
'Content-Type': 'application/json'
|
| 433 |
+
},
|
| 434 |
+
body: JSON.stringify({
|
| 435 |
+
name: studentName,
|
| 436 |
+
educational_level: educationalLevel,
|
| 437 |
+
learning_style: 'visual',
|
| 438 |
+
interests: ['science', 'technology', 'art'],
|
| 439 |
+
strengths: ['problem-solving', 'creativity'],
|
| 440 |
+
areas_for_improvement: ['time-management']
|
| 441 |
+
})
|
| 442 |
+
});
|
| 443 |
+
|
| 444 |
+
const data = await response.json();
|
| 445 |
+
currentStudentId = data.id;
|
| 446 |
+
|
| 447 |
+
document.getElementById('studentResponse').innerText = formatJSON(data);
|
| 448 |
+
} catch (error) {
|
| 449 |
+
document.getElementById('studentResponse').innerText = `Error: ${error.message}`;
|
| 450 |
+
} finally {
|
| 451 |
+
loadingElement.style.display = 'none';
|
| 452 |
+
}
|
| 453 |
+
});
|
| 454 |
+
|
| 455 |
+
// Adapt Content
|
| 456 |
+
document.getElementById('adaptContentBtn').addEventListener('click', async () => {
|
| 457 |
+
if (!currentStudentId) {
|
| 458 |
+
alert('Please create a student profile first!');
|
| 459 |
+
return;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
const contentText = document.getElementById('contentText').value;
|
| 463 |
+
if (!contentText) {
|
| 464 |
+
alert('Please enter content to adapt!');
|
| 465 |
+
return;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
const loadingElement = document.getElementById('loading-content');
|
| 469 |
+
loadingElement.style.display = 'inline';
|
| 470 |
+
|
| 471 |
+
try {
|
| 472 |
+
const response = await fetch('/api/v1/content/adapt', {
|
| 473 |
+
method: 'POST',
|
| 474 |
+
headers: {
|
| 475 |
+
'Content-Type': 'application/json'
|
| 476 |
+
},
|
| 477 |
+
body: JSON.stringify({
|
| 478 |
+
content: {
|
| 479 |
+
text: contentText
|
| 480 |
+
},
|
| 481 |
+
student_id: currentStudentId,
|
| 482 |
+
learning_objectives: ['obj-1', 'obj-2'],
|
| 483 |
+
content_format: 'text'
|
| 484 |
+
})
|
| 485 |
+
});
|
| 486 |
+
|
| 487 |
+
const data = await response.json();
|
| 488 |
+
document.getElementById('contentResponse').innerText = formatJSON(data);
|
| 489 |
+
} catch (error) {
|
| 490 |
+
document.getElementById('contentResponse').innerText = `Error: ${error.message}`;
|
| 491 |
+
} finally {
|
| 492 |
+
loadingElement.style.display = 'none';
|
| 493 |
+
}
|
| 494 |
+
});
|
| 495 |
+
</script>
|
| 496 |
+
</body>
|
| 497 |
+
</html>
|
| 498 |
+
"""
|
| 499 |
+
|
| 500 |
+
# Write the index.html file
|
| 501 |
+
with open(static_dir / "index.html", "w") as f:
|
| 502 |
+
f.write(index_html)
|
| 503 |
+
|
| 504 |
+
print(f"Demo UI created at {static_dir / 'index.html'}")
|
src/edtech_extensions/__pycache__/content_adaptation.cpython-311.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
src/edtech_extensions/__pycache__/progress_tracking.cpython-311.pyc
ADDED
|
Binary file (21.4 kB). View file
|
|
|
src/edtech_extensions/__pycache__/student_profile.cpython-311.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
src/edtech_extensions/assessment.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
EdTech Extensions - Assessment and Feedback System
|
| 3 |
+
|
| 4 |
+
This module provides functionality for creating and managing assessments,
|
| 5 |
+
as well as generating personalized feedback for students.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import Dict, List, Optional, Any, Union
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from enum import Enum
|
| 13 |
+
from uuid import uuid4
|
| 14 |
+
|
| 15 |
+
from pydantic import BaseModel, Field
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class QuestionType(str, Enum):
|
| 19 |
+
"""Types of questions supported in assessments."""
|
| 20 |
+
MULTIPLE_CHOICE = "multiple_choice"
|
| 21 |
+
TRUE_FALSE = "true_false"
|
| 22 |
+
SHORT_ANSWER = "short_answer"
|
| 23 |
+
ESSAY = "essay"
|
| 24 |
+
MATCHING = "matching"
|
| 25 |
+
FILL_BLANK = "fill_blank"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Question(BaseModel):
|
| 29 |
+
"""Model representing an assessment question."""
|
| 30 |
+
id: str = Field(..., description="Unique identifier for this question")
|
| 31 |
+
type: QuestionType = Field(..., description="Type of question")
|
| 32 |
+
text: str = Field(..., description="Question text")
|
| 33 |
+
options: List[Dict[str, Any]] = Field(default_factory=list, description="Answer options for multiple choice, etc.")
|
| 34 |
+
correct_answer: Any = Field(..., description="Correct answer or answers")
|
| 35 |
+
points: int = Field(1, description="Points for this question")
|
| 36 |
+
difficulty: str = Field("medium", description="Question difficulty")
|
| 37 |
+
tags: List[str] = Field(default_factory=list, description="Tags for categorizing questions")
|
| 38 |
+
explanation: str = Field("", description="Explanation of the correct answer")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Assessment(BaseModel):
|
| 42 |
+
"""Model representing an assessment."""
|
| 43 |
+
id: str = Field(..., description="Unique identifier for this assessment")
|
| 44 |
+
title: str = Field(..., description="Assessment title")
|
| 45 |
+
description: str = Field("", description="Assessment description")
|
| 46 |
+
questions: List[Question] = Field(..., description="Questions in this assessment")
|
| 47 |
+
time_limit_minutes: Optional[int] = Field(None, description="Time limit in minutes, if any")
|
| 48 |
+
passing_score: float = Field(70.0, description="Passing score percentage")
|
| 49 |
+
tags: List[str] = Field(default_factory=list, description="Tags for categorizing assessments")
|
| 50 |
+
learning_objectives: List[str] = Field(default_factory=list, description="Learning objectives covered")
|
| 51 |
+
created_at: str = Field(..., description="Creation timestamp")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class StudentResponse(BaseModel):
|
| 55 |
+
"""Model representing a student's response to a question."""
|
| 56 |
+
question_id: str = Field(..., description="ID of the question")
|
| 57 |
+
student_answer: Any = Field(..., description="Student's answer")
|
| 58 |
+
is_correct: Optional[bool] = Field(None, description="Whether the answer is correct")
|
| 59 |
+
points_earned: Optional[float] = Field(None, description="Points earned for this response")
|
| 60 |
+
feedback: str = Field("", description="Feedback on this response")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class AssessmentSubmission(BaseModel):
|
| 64 |
+
"""Model representing a student's assessment submission."""
|
| 65 |
+
id: str = Field(..., description="Unique identifier for this submission")
|
| 66 |
+
assessment_id: str = Field(..., description="ID of the assessment")
|
| 67 |
+
student_id: str = Field(..., description="ID of the student")
|
| 68 |
+
responses: List[StudentResponse] = Field(..., description="Student's responses")
|
| 69 |
+
start_time: str = Field(..., description="Start timestamp")
|
| 70 |
+
end_time: str = Field(..., description="End timestamp")
|
| 71 |
+
score: Optional[float] = Field(None, description="Overall score")
|
| 72 |
+
feedback: str = Field("", description="Overall feedback")
|
| 73 |
+
graded: bool = Field(False, description="Whether the submission has been graded")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class AssessmentSystem:
|
| 77 |
+
"""
|
| 78 |
+
Manages assessments and student submissions.
|
| 79 |
+
|
| 80 |
+
This class provides functionality for creating, retrieving, and grading
|
| 81 |
+
assessments, as well as generating personalized feedback.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
def __init__(self, storage_dir: str = "./storage/assessments"):
|
| 85 |
+
"""
|
| 86 |
+
Initialize a new AssessmentSystem.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
storage_dir: Directory to store assessment data in.
|
| 90 |
+
"""
|
| 91 |
+
self.storage_dir = storage_dir
|
| 92 |
+
self.assessments_dir = os.path.join(storage_dir, "definitions")
|
| 93 |
+
self.submissions_dir = os.path.join(storage_dir, "submissions")
|
| 94 |
+
|
| 95 |
+
os.makedirs(self.assessments_dir, exist_ok=True)
|
| 96 |
+
os.makedirs(self.submissions_dir, exist_ok=True)
|
| 97 |
+
|
| 98 |
+
def _get_assessment_path(self, assessment_id: str) -> str:
|
| 99 |
+
"""
|
| 100 |
+
Get the file path for an assessment.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
assessment_id: The ID of the assessment.
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
The file path for the assessment.
|
| 107 |
+
"""
|
| 108 |
+
return os.path.join(self.assessments_dir, f"{assessment_id}.json")
|
| 109 |
+
|
| 110 |
+
def _get_submission_path(self, submission_id: str) -> str:
|
| 111 |
+
"""
|
| 112 |
+
Get the file path for a submission.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
submission_id: The ID of the submission.
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
The file path for the submission.
|
| 119 |
+
"""
|
| 120 |
+
return os.path.join(self.submissions_dir, f"{submission_id}.json")
|
| 121 |
+
|
| 122 |
+
async def create_assessment(
|
| 123 |
+
self,
|
| 124 |
+
title: str,
|
| 125 |
+
questions: List[Question],
|
| 126 |
+
description: str = "",
|
| 127 |
+
time_limit_minutes: Optional[int] = None,
|
| 128 |
+
passing_score: float = 70.0,
|
| 129 |
+
tags: List[str] = None,
|
| 130 |
+
learning_objectives: List[str] = None
|
| 131 |
+
) -> Assessment:
|
| 132 |
+
"""
|
| 133 |
+
Create a new assessment.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
title: Assessment title.
|
| 137 |
+
questions: List of questions.
|
| 138 |
+
description: Assessment description.
|
| 139 |
+
time_limit_minutes: Time limit in minutes, if any.
|
| 140 |
+
passing_score: Passing score percentage.
|
| 141 |
+
tags: Tags for categorizing the assessment.
|
| 142 |
+
learning_objectives: Learning objectives covered.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
The newly created Assessment.
|
| 146 |
+
"""
|
| 147 |
+
assessment_id = str(uuid4())
|
| 148 |
+
created_at = datetime.utcnow().isoformat()
|
| 149 |
+
|
| 150 |
+
assessment = Assessment(
|
| 151 |
+
id=assessment_id,
|
| 152 |
+
title=title,
|
| 153 |
+
description=description,
|
| 154 |
+
questions=questions,
|
| 155 |
+
time_limit_minutes=time_limit_minutes,
|
| 156 |
+
passing_score=passing_score,
|
| 157 |
+
tags=tags or [],
|
| 158 |
+
learning_objectives=learning_objectives or [],
|
| 159 |
+
created_at=created_at
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Save to disk
|
| 163 |
+
await self._save_assessment(assessment)
|
| 164 |
+
|
| 165 |
+
return assessment
|
| 166 |
+
|
| 167 |
+
async def _save_assessment(self, assessment: Assessment) -> bool:
|
| 168 |
+
"""
|
| 169 |
+
Save an assessment to disk.
|
| 170 |
+
|
| 171 |
+
Args:
|
| 172 |
+
assessment: The Assessment to save.
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
True if the save was successful, False otherwise.
|
| 176 |
+
"""
|
| 177 |
+
try:
|
| 178 |
+
with open(self._get_assessment_path(assessment.id), 'w') as f:
|
| 179 |
+
json.dump(assessment.dict(), f, indent=2)
|
| 180 |
+
return True
|
| 181 |
+
except Exception as e:
|
| 182 |
+
print(f"Error saving assessment: {e}")
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
async def get_assessment(self, assessment_id: str) -> Optional[Assessment]:
|
| 186 |
+
"""
|
| 187 |
+
Get an assessment by ID.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
assessment_id: The ID of the assessment.
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
The Assessment if found, None otherwise.
|
| 194 |
+
"""
|
| 195 |
+
try:
|
| 196 |
+
path = self._get_assessment_path(assessment_id)
|
| 197 |
+
if not os.path.exists(path):
|
| 198 |
+
return None
|
| 199 |
+
|
| 200 |
+
with open(path, 'r') as f:
|
| 201 |
+
data = json.load(f)
|
| 202 |
+
|
| 203 |
+
return Assessment(**data)
|
| 204 |
+
except Exception as e:
|
| 205 |
+
print(f"Error loading assessment: {e}")
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
async def list_assessments(
|
| 209 |
+
self,
|
| 210 |
+
tags: Optional[List[str]] = None,
|
| 211 |
+
learning_objectives: Optional[List[str]] = None
|
| 212 |
+
) -> List[Assessment]:
|
| 213 |
+
"""
|
| 214 |
+
List assessments, optionally filtered.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
tags: Optional filter by tags.
|
| 218 |
+
learning_objectives: Optional filter by learning objectives.
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
List of Assessment objects.
|
| 222 |
+
"""
|
| 223 |
+
assessments = []
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
for filename in os.listdir(self.assessments_dir):
|
| 227 |
+
if filename.endswith('.json'):
|
| 228 |
+
path = os.path.join(self.assessments_dir, filename)
|
| 229 |
+
|
| 230 |
+
with open(path, 'r') as f:
|
| 231 |
+
data = json.load(f)
|
| 232 |
+
|
| 233 |
+
assessment = Assessment(**data)
|
| 234 |
+
|
| 235 |
+
# Apply filters
|
| 236 |
+
include = True
|
| 237 |
+
|
| 238 |
+
if tags:
|
| 239 |
+
# Include if any tag matches
|
| 240 |
+
tag_match = any(tag in assessment.tags for tag in tags)
|
| 241 |
+
if not tag_match:
|
| 242 |
+
include = False
|
| 243 |
+
|
| 244 |
+
if learning_objectives:
|
| 245 |
+
# Include if any learning objective matches
|
| 246 |
+
obj_match = any(obj in assessment.learning_objectives for obj in learning_objectives)
|
| 247 |
+
if not obj_match:
|
| 248 |
+
include = False
|
| 249 |
+
|
| 250 |
+
if include:
|
| 251 |
+
assessments.append(assessment)
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"Error listing assessments: {e}")
|
| 254 |
+
|
| 255 |
+
return assessments
|
| 256 |
+
|
| 257 |
+
async def submit_assessment(
|
| 258 |
+
self,
|
| 259 |
+
assessment_id: str,
|
| 260 |
+
student_id: str,
|
| 261 |
+
responses: List[StudentResponse]
|
| 262 |
+
) -> AssessmentSubmission:
|
| 263 |
+
"""
|
| 264 |
+
Submit an assessment.
|
| 265 |
+
|
| 266 |
+
Args:
|
| 267 |
+
assessment_id: ID of the assessment.
|
| 268 |
+
student_id: ID of the student.
|
| 269 |
+
responses: List of student responses.
|
| 270 |
+
|
| 271 |
+
Returns:
|
| 272 |
+
The AssessmentSubmission.
|
| 273 |
+
"""
|
| 274 |
+
submission_id = str(uuid4())
|
| 275 |
+
start_time = datetime.utcnow().isoformat()
|
| 276 |
+
end_time = datetime.utcnow().isoformat()
|
| 277 |
+
|
| 278 |
+
submission = AssessmentSubmission(
|
| 279 |
+
id=submission_id,
|
| 280 |
+
assessment_id=assessment_id,
|
| 281 |
+
student_id=student_id,
|
| 282 |
+
responses=responses,
|
| 283 |
+
start_time=start_time,
|
| 284 |
+
end_time=end_time,
|
| 285 |
+
score=None,
|
| 286 |
+
feedback="",
|
| 287 |
+
graded=False
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# Save to disk
|
| 291 |
+
await self._save_submission(submission)
|
| 292 |
+
|
| 293 |
+
# Grade the submission
|
| 294 |
+
graded_submission = await self.grade_submission(submission_id)
|
| 295 |
+
|
| 296 |
+
return graded_submission
|
| 297 |
+
|
| 298 |
+
async def _save_submission(self, submission: AssessmentSubmission) -> bool:
|
| 299 |
+
"""
|
| 300 |
+
Save a submission to disk.
|
| 301 |
+
|
| 302 |
+
Args:
|
| 303 |
+
submission: The AssessmentSubmission to save.
|
| 304 |
+
|
| 305 |
+
Returns:
|
| 306 |
+
True if the save was successful, False otherwise.
|
| 307 |
+
"""
|
| 308 |
+
try:
|
| 309 |
+
with open(self._get_submission_path(submission.id), 'w') as f:
|
| 310 |
+
json.dump(submission.dict(), f, indent=2)
|
| 311 |
+
return True
|
| 312 |
+
except Exception as e:
|
| 313 |
+
print(f"Error saving submission: {e}")
|
| 314 |
+
return False
|
| 315 |
+
|
| 316 |
+
async def get_submission(self, submission_id: str) -> Optional[AssessmentSubmission]:
|
| 317 |
+
"""
|
| 318 |
+
Get a submission by ID.
|
| 319 |
+
|
| 320 |
+
Args:
|
| 321 |
+
submission_id: The ID of the submission.
|
| 322 |
+
|
| 323 |
+
Returns:
|
| 324 |
+
The AssessmentSubmission if found, None otherwise.
|
| 325 |
+
"""
|
| 326 |
+
try:
|
| 327 |
+
path = self._get_submission_path(submission_id)
|
| 328 |
+
if not os.path.exists(path):
|
| 329 |
+
return None
|
| 330 |
+
|
| 331 |
+
with open(path, 'r') as f:
|
| 332 |
+
data = json.load(f)
|
| 333 |
+
|
| 334 |
+
return AssessmentSubmission(**data)
|
| 335 |
+
except Exception as e:
|
| 336 |
+
print(f"Error loading submission: {e}")
|
| 337 |
+
return None
|
| 338 |
+
|
| 339 |
+
async def grade_submission(self, submission_id: str) -> Optional[AssessmentSubmission]:
|
| 340 |
+
"""
|
| 341 |
+
Grade a submission.
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
submission_id: The ID of the submission.
|
| 345 |
+
|
| 346 |
+
Returns:
|
| 347 |
+
The graded AssessmentSubmission if found, None otherwise.
|
| 348 |
+
"""
|
| 349 |
+
submission = await self.get_submission(submission_id)
|
| 350 |
+
if not submission:
|
| 351 |
+
return None
|
| 352 |
+
|
| 353 |
+
assessment = await self.get_assessment(submission.assessment_id)
|
| 354 |
+
if not assessment:
|
| 355 |
+
return None
|
| 356 |
+
|
| 357 |
+
# Create a map of questions by ID for easy lookup
|
| 358 |
+
questions_map = {q.id: q for q in assessment.questions}
|
| 359 |
+
|
| 360 |
+
total_points = 0
|
| 361 |
+
earned_points = 0
|
| 362 |
+
|
| 363 |
+
# Grade each response
|
| 364 |
+
for response in submission.responses:
|
| 365 |
+
question = questions_map.get(response.question_id)
|
| 366 |
+
if not question:
|
| 367 |
+
continue
|
| 368 |
+
|
| 369 |
+
total_points += question.points
|
| 370 |
+
|
| 371 |
+
# Grade based on question type
|
| 372 |
+
if question.type == QuestionType.MULTIPLE_CHOICE or question.type == QuestionType.TRUE_FALSE:
|
| 373 |
+
# Simple equality check
|
| 374 |
+
is_correct = response.student_answer == question.correct_answer
|
| 375 |
+
points_earned = question.points if is_correct else 0
|
| 376 |
+
|
| 377 |
+
response.is_correct = is_correct
|
| 378 |
+
response.points_earned = points_earned
|
| 379 |
+
response.feedback = question.explanation if question.explanation else (
|
| 380 |
+
"Correct!" if is_correct else f"Incorrect. The correct answer is: {question.correct_answer}"
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
elif question.type == QuestionType.FILL_BLANK:
|
| 384 |
+
# Case-insensitive equality check
|
| 385 |
+
is_correct = str(response.student_answer).lower() == str(question.correct_answer).lower()
|
| 386 |
+
points_earned = question.points if is_correct else 0
|
| 387 |
+
|
| 388 |
+
response.is_correct = is_correct
|
| 389 |
+
response.points_earned = points_earned
|
| 390 |
+
response.feedback = question.explanation if question.explanation else (
|
| 391 |
+
"Correct!" if is_correct else f"Incorrect. The correct answer is: {question.correct_answer}"
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
elif question.type == QuestionType.SHORT_ANSWER:
|
| 395 |
+
# For short answer, we'll use a simple keyword check
|
| 396 |
+
# In a real implementation, this would use more sophisticated NLP techniques
|
| 397 |
+
student_answer = str(response.student_answer).lower()
|
| 398 |
+
correct_keywords = [str(k).lower() for k in question.correct_answer]
|
| 399 |
+
|
| 400 |
+
# Check how many keywords are present
|
| 401 |
+
matches = sum(1 for k in correct_keywords if k in student_answer)
|
| 402 |
+
match_ratio = matches / len(correct_keywords) if correct_keywords else 0
|
| 403 |
+
|
| 404 |
+
# Full points if all keywords present, partial otherwise
|
| 405 |
+
is_correct = match_ratio >= 0.7 # 70% or more keywords
|
| 406 |
+
points_earned = question.points * match_ratio
|
| 407 |
+
|
| 408 |
+
response.is_correct = is_correct
|
| 409 |
+
response.points_earned = points_earned
|
| 410 |
+
response.feedback = question.explanation if question.explanation else (
|
| 411 |
+
"Good answer!" if is_correct else
|
| 412 |
+
f"Your answer could be improved. Important concepts to include: {', '.join(correct_keywords)}"
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
elif question.type == QuestionType.ESSAY:
|
| 416 |
+
# For essays, we'll mark as needing manual grading
|
| 417 |
+
# In a real implementation, this might use AI for initial grading
|
| 418 |
+
response.is_correct = None
|
| 419 |
+
response.points_earned = None
|
| 420 |
+
response.feedback = "This response requires manual grading."
|
| 421 |
+
|
| 422 |
+
elif question.type == QuestionType.MATCHING:
|
| 423 |
+
# Check if all matches are correct
|
| 424 |
+
correct_matches = question.correct_answer
|
| 425 |
+
student_matches = response.student_answer
|
| 426 |
+
|
| 427 |
+
# Count correct matches
|
| 428 |
+
correct_count = sum(1 for k, v in student_matches.items() if k in correct_matches and correct_matches[k] == v)
|
| 429 |
+
total_matches = len(correct_matches)
|
| 430 |
+
|
| 431 |
+
match_ratio = correct_count / total_matches if total_matches else 0
|
| 432 |
+
is_correct = match_ratio >= 0.7 # 70% or more correct
|
| 433 |
+
points_earned = question.points * match_ratio
|
| 434 |
+
|
| 435 |
+
response.is_correct = is_correct
|
| 436 |
+
response.points_earned = points_earned
|
| 437 |
+
response.feedback = question.explanation if question.explanation else (
|
| 438 |
+
"Good matching!" if is_correct else
|
| 439 |
+
f"Some matches were incorrect. Please review the correct answers."
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
# Add to total points
|
| 443 |
+
if response.points_earned is not None:
|
| 444 |
+
earned_points += response.points_earned
|
| 445 |
+
|
| 446 |
+
# Calculate overall score
|
| 447 |
+
score_percentage = (earned_points / total_points * 100) if total_points > 0 else 0
|
| 448 |
+
|
| 449 |
+
# Generate overall feedback
|
| 450 |
+
if score_percentage >= assessment.passing_score:
|
| 451 |
+
overall_feedback = f"Congratulations! You passed with a score of {score_percentage:.1f}%."
|
| 452 |
+
else:
|
| 453 |
+
overall_feedback = f"You scored {score_percentage:.1f}%, which is below the passing score of {assessment.passing_score}%. Please review the material and try again."
|
| 454 |
+
|
| 455 |
+
# Update submission
|
| 456 |
+
submission.score = score_percentage
|
| 457 |
+
submission.feedback = overall_feedback
|
| 458 |
+
submission.graded = True
|
| 459 |
+
|
| 460 |
+
# Save updated submission
|
| 461 |
+
await self._save_submission(submission)
|
| 462 |
+
|
| 463 |
+
return submission
|
| 464 |
+
|
| 465 |
+
async def list_student_submissions(self, student_id: str) -> List[AssessmentSubmission]:
|
| 466 |
+
"""
|
| 467 |
+
List submissions for a student.
|
| 468 |
+
|
| 469 |
+
Args:
|
| 470 |
+
student_id: ID of the student.
|
| 471 |
+
|
| 472 |
+
Returns:
|
| 473 |
+
List of AssessmentSubmission objects.
|
| 474 |
+
"""
|
| 475 |
+
submissions = []
|
| 476 |
+
|
| 477 |
+
try:
|
| 478 |
+
for filename in os.listdir(self.submissions_dir):
|
| 479 |
+
if filename.endswith('.json'):
|
| 480 |
+
path = os.path.join(self.submissions_dir, filename)
|
| 481 |
+
|
| 482 |
+
with open(path, 'r') as f:
|
| 483 |
+
data = json.load(f)
|
| 484 |
+
|
| 485 |
+
if data.get("student_id") == student_id:
|
| 486 |
+
submissions.append(AssessmentSubmission(**data))
|
| 487 |
+
except Exception as e:
|
| 488 |
+
print(f"Error listing submissions: {e}")
|
| 489 |
+
|
| 490 |
+
# Sort by end time, newest first
|
| 491 |
+
submissions.sort(key=lambda x: x.end_time, reverse=True)
|
| 492 |
+
return submissions
|
| 493 |
+
|
| 494 |
+
async def generate_feedback_report(self, submission_id: str) -> Dict[str, Any]:
|
| 495 |
+
"""
|
| 496 |
+
Generate a detailed feedback report for a submission.
|
| 497 |
+
|
| 498 |
+
Args:
|
| 499 |
+
submission_id: ID of the submission.
|
| 500 |
+
|
| 501 |
+
Returns:
|
| 502 |
+
Dictionary containing the feedback report.
|
| 503 |
+
"""
|
| 504 |
+
submission = await self.get_submission(submission_id)
|
| 505 |
+
if not submission:
|
| 506 |
+
return {"error": "Submission not found"}
|
| 507 |
+
|
| 508 |
+
assessment = await self.get_assessment(submission.assessment_id)
|
| 509 |
+
if not assessment:
|
| 510 |
+
return {"error": "Assessment not found"}
|
| 511 |
+
|
| 512 |
+
# Create a map of questions by ID for easy lookup
|
| 513 |
+
questions_map = {q.id: q for q in assessment.questions}
|
| 514 |
+
|
| 515 |
+
# Group questions and responses by tags
|
| 516 |
+
tag_groups = {}
|
| 517 |
+
for response in submission.responses:
|
| 518 |
+
question = questions_map.get(response.question_id)
|
| 519 |
+
if not question:
|
| 520 |
+
continue
|
| 521 |
+
|
| 522 |
+
for tag in question.tags:
|
| 523 |
+
if tag not in tag_groups:
|
| 524 |
+
tag_groups[tag] = {
|
| 525 |
+
"total": 0,
|
| 526 |
+
"correct": 0,
|
| 527 |
+
"questions": []
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
tag_groups[tag]["total"] += 1
|
| 531 |
+
if response.is_correct:
|
| 532 |
+
tag_groups[tag]["correct"] += 1
|
| 533 |
+
|
| 534 |
+
tag_groups[tag]["questions"].append({
|
| 535 |
+
"question_text": question.text,
|
| 536 |
+
"student_answer": response.student_answer,
|
| 537 |
+
"correct_answer": question.correct_answer,
|
| 538 |
+
"is_correct": response.is_correct,
|
| 539 |
+
"feedback": response.feedback
|
| 540 |
+
})
|
| 541 |
+
|
| 542 |
+
# Generate strengths and weaknesses
|
| 543 |
+
strengths = []
|
| 544 |
+
weaknesses = []
|
| 545 |
+
|
| 546 |
+
for tag, data in tag_groups.items():
|
| 547 |
+
score = data["correct"] / data["total"] if data["total"] > 0 else 0
|
| 548 |
+
|
| 549 |
+
if score >= 0.8: # 80% or better
|
| 550 |
+
strengths.append({
|
| 551 |
+
"topic": tag,
|
| 552 |
+
"score": score * 100,
|
| 553 |
+
"comment": f"You show strong understanding of {tag}."
|
| 554 |
+
})
|
| 555 |
+
elif score <= 0.6: # 60% or worse
|
| 556 |
+
weaknesses.append({
|
| 557 |
+
"topic": tag,
|
| 558 |
+
"score": score * 100,
|
| 559 |
+
"comment": f"You may need additional practice with {tag}."
|
| 560 |
+
})
|
| 561 |
+
|
| 562 |
+
# Generate recommended resources (in a real app, these would come from a database)
|
| 563 |
+
recommended_resources = []
|
| 564 |
+
for weakness in weaknesses:
|
| 565 |
+
recommended_resources.append({
|
| 566 |
+
"topic": weakness["topic"],
|
| 567 |
+
"title": f"Review material on {weakness['topic']}",
|
| 568 |
+
"type": "article",
|
| 569 |
+
"url": f"https://example.com/resources/{weakness['topic'].lower().replace(' ', '-')}"
|
| 570 |
+
})
|
| 571 |
+
|
| 572 |
+
# Create the report
|
| 573 |
+
report = {
|
| 574 |
+
"submission_id": submission_id,
|
| 575 |
+
"assessment_id": submission.assessment_id,
|
| 576 |
+
"assessment_title": assessment.title,
|
| 577 |
+
"student_id": submission.student_id,
|
| 578 |
+
"score": submission.score,
|
| 579 |
+
"passing_score": assessment.passing_score,
|
| 580 |
+
"passed": submission.score >= assessment.passing_score if submission.score is not None else False,
|
| 581 |
+
"strengths": strengths,
|
| 582 |
+
"weaknesses": weaknesses,
|
| 583 |
+
"topic_breakdown": [
|
| 584 |
+
{
|
| 585 |
+
"topic": tag,
|
| 586 |
+
"score": (data["correct"] / data["total"] * 100) if data["total"] > 0 else 0,
|
| 587 |
+
"questions_count": data["total"]
|
| 588 |
+
}
|
| 589 |
+
for tag, data in tag_groups.items()
|
| 590 |
+
],
|
| 591 |
+
"recommended_resources": recommended_resources,
|
| 592 |
+
"detailed_feedback": submission.feedback,
|
| 593 |
+
"generated_at": datetime.utcnow().isoformat()
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
return report
|
src/edtech_extensions/content_adaptation.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
EdTech Extensions - Content Adaptation
|
| 3 |
+
|
| 4 |
+
This module provides functionality for adapting educational content
|
| 5 |
+
based on student profiles and learning objectives.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import Dict, List, Optional, Any, Union
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
from ..mcp_core.protocol import StudentProfile, LearningObjective, EducationalLevel, ContentFormat
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ContentAdapter:
|
| 17 |
+
"""
|
| 18 |
+
Adapts educational content based on student profiles and learning objectives.
|
| 19 |
+
|
| 20 |
+
This class provides functionality for personalizing educational content
|
| 21 |
+
to match a student's learning style, educational level, and interests.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self):
|
| 25 |
+
"""Initialize a new ContentAdapter."""
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
async def adapt_content(
|
| 29 |
+
self,
|
| 30 |
+
content: Dict[str, Any],
|
| 31 |
+
student_profile: StudentProfile,
|
| 32 |
+
learning_objectives: List[LearningObjective],
|
| 33 |
+
content_format: ContentFormat = ContentFormat.TEXT
|
| 34 |
+
) -> Dict[str, Any]:
|
| 35 |
+
"""
|
| 36 |
+
Adapt educational content for a specific student.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
content: Original content to adapt.
|
| 40 |
+
student_profile: Student profile to adapt content for.
|
| 41 |
+
learning_objectives: Learning objectives for this content.
|
| 42 |
+
content_format: Format of the content.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
Dictionary containing adapted content and metadata.
|
| 46 |
+
"""
|
| 47 |
+
# In a real implementation, this would use AI models to adapt content
|
| 48 |
+
# based on the student profile and learning objectives.
|
| 49 |
+
# For this demo, we'll implement a simplified version.
|
| 50 |
+
|
| 51 |
+
original_text = content.get("text", "")
|
| 52 |
+
|
| 53 |
+
# Apply adaptations based on educational level
|
| 54 |
+
adapted_text = self._adapt_for_educational_level(
|
| 55 |
+
original_text,
|
| 56 |
+
student_profile.educational_level
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Apply adaptations based on learning style
|
| 60 |
+
adapted_text = self._adapt_for_learning_style(
|
| 61 |
+
adapted_text,
|
| 62 |
+
student_profile.learning_style
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Apply adaptations based on interests
|
| 66 |
+
adapted_text = self._adapt_for_interests(
|
| 67 |
+
adapted_text,
|
| 68 |
+
student_profile.interests
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# Generate recommended activities
|
| 72 |
+
recommended_activities = self._generate_recommended_activities(
|
| 73 |
+
student_profile,
|
| 74 |
+
learning_objectives
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# List adaptations applied
|
| 78 |
+
adaptations_applied = []
|
| 79 |
+
if student_profile.educational_level:
|
| 80 |
+
adaptations_applied.append(f"Adapted for {student_profile.educational_level} educational level")
|
| 81 |
+
if student_profile.learning_style:
|
| 82 |
+
adaptations_applied.append(f"Adapted for {student_profile.learning_style} learning style")
|
| 83 |
+
if student_profile.interests:
|
| 84 |
+
adaptations_applied.append("Incorporated student interests")
|
| 85 |
+
|
| 86 |
+
return {
|
| 87 |
+
"text": adapted_text,
|
| 88 |
+
"original_text": original_text,
|
| 89 |
+
"adaptations_applied": adaptations_applied,
|
| 90 |
+
"recommended_activities": recommended_activities,
|
| 91 |
+
"format": content_format,
|
| 92 |
+
"adapted_at": datetime.utcnow().isoformat()
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
def _adapt_for_educational_level(
|
| 96 |
+
self,
|
| 97 |
+
text: str,
|
| 98 |
+
educational_level: EducationalLevel
|
| 99 |
+
) -> str:
|
| 100 |
+
"""
|
| 101 |
+
Adapt content for a specific educational level.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
text: Original text content.
|
| 105 |
+
educational_level: Target educational level.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
Adapted text content.
|
| 109 |
+
"""
|
| 110 |
+
# In a real implementation, this would use more sophisticated techniques
|
| 111 |
+
# such as vocabulary adjustment, complexity reduction, etc.
|
| 112 |
+
|
| 113 |
+
if educational_level == EducationalLevel.ELEMENTARY:
|
| 114 |
+
# Simplify language for elementary level
|
| 115 |
+
return f"{text}\n\n[Note: This content has been simplified for elementary level students.]"
|
| 116 |
+
|
| 117 |
+
elif educational_level == EducationalLevel.MIDDLE_SCHOOL:
|
| 118 |
+
# Moderate complexity for middle school
|
| 119 |
+
return f"{text}\n\n[Note: This content has been adapted for middle school students.]"
|
| 120 |
+
|
| 121 |
+
elif educational_level == EducationalLevel.HIGH_SCHOOL:
|
| 122 |
+
# Standard complexity for high school
|
| 123 |
+
return f"{text}\n\n[Note: This content has been adapted for high school students.]"
|
| 124 |
+
|
| 125 |
+
elif educational_level == EducationalLevel.UNDERGRADUATE:
|
| 126 |
+
# Higher complexity for undergraduate
|
| 127 |
+
return f"{text}\n\n[Note: This content has been adapted for undergraduate students.]"
|
| 128 |
+
|
| 129 |
+
elif educational_level == EducationalLevel.GRADUATE:
|
| 130 |
+
# Advanced complexity for graduate
|
| 131 |
+
return f"{text}\n\n[Note: This content has been adapted for graduate students with advanced terminology.]"
|
| 132 |
+
|
| 133 |
+
elif educational_level == EducationalLevel.PROFESSIONAL:
|
| 134 |
+
# Professional terminology
|
| 135 |
+
return f"{text}\n\n[Note: This content has been adapted with professional terminology.]"
|
| 136 |
+
|
| 137 |
+
return text
|
| 138 |
+
|
| 139 |
+
def _adapt_for_learning_style(self, text: str, learning_style: str) -> str:
|
| 140 |
+
"""
|
| 141 |
+
Adapt content for a specific learning style.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
text: Original text content.
|
| 145 |
+
learning_style: Target learning style.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
Adapted text content.
|
| 149 |
+
"""
|
| 150 |
+
# In a real implementation, this would use more sophisticated techniques
|
| 151 |
+
|
| 152 |
+
if not learning_style:
|
| 153 |
+
return text
|
| 154 |
+
|
| 155 |
+
if learning_style.lower() == "visual":
|
| 156 |
+
# Add visual cues
|
| 157 |
+
return f"{text}\n\n[Note: Visual diagrams and charts would be included here to support visual learners.]"
|
| 158 |
+
|
| 159 |
+
elif learning_style.lower() == "auditory":
|
| 160 |
+
# Add auditory cues
|
| 161 |
+
return f"{text}\n\n[Note: Audio explanations would be available for auditory learners.]"
|
| 162 |
+
|
| 163 |
+
elif learning_style.lower() == "reading":
|
| 164 |
+
# Add reading cues
|
| 165 |
+
return f"{text}\n\n[Note: Additional reading materials and references are provided for reading-oriented learners.]"
|
| 166 |
+
|
| 167 |
+
elif learning_style.lower() == "kinesthetic":
|
| 168 |
+
# Add kinesthetic cues
|
| 169 |
+
return f"{text}\n\n[Note: Interactive exercises would be included here for kinesthetic learners.]"
|
| 170 |
+
|
| 171 |
+
return text
|
| 172 |
+
|
| 173 |
+
def _adapt_for_interests(self, text: str, interests: List[str]) -> str:
|
| 174 |
+
"""
|
| 175 |
+
Adapt content to incorporate student interests.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
text: Original text content.
|
| 179 |
+
interests: List of student interests.
|
| 180 |
+
|
| 181 |
+
Returns:
|
| 182 |
+
Adapted text content.
|
| 183 |
+
"""
|
| 184 |
+
# In a real implementation, this would use more sophisticated techniques
|
| 185 |
+
|
| 186 |
+
if not interests:
|
| 187 |
+
return text
|
| 188 |
+
|
| 189 |
+
interest_note = f"\n\n[Note: This content would include examples related to your interests in {', '.join(interests)}.]"
|
| 190 |
+
return text + interest_note
|
| 191 |
+
|
| 192 |
+
def _generate_recommended_activities(
|
| 193 |
+
self,
|
| 194 |
+
student_profile: StudentProfile,
|
| 195 |
+
learning_objectives: List[LearningObjective]
|
| 196 |
+
) -> List[Dict[str, Any]]:
|
| 197 |
+
"""
|
| 198 |
+
Generate recommended activities based on student profile and learning objectives.
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
student_profile: Student profile.
|
| 202 |
+
learning_objectives: Learning objectives.
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
List of recommended activities.
|
| 206 |
+
"""
|
| 207 |
+
# In a real implementation, this would generate personalized activities
|
| 208 |
+
# based on the student's profile and learning objectives.
|
| 209 |
+
|
| 210 |
+
activities = []
|
| 211 |
+
|
| 212 |
+
# Add activities based on learning style
|
| 213 |
+
if student_profile.learning_style:
|
| 214 |
+
if student_profile.learning_style.lower() == "visual":
|
| 215 |
+
activities.append({
|
| 216 |
+
"type": "visualization",
|
| 217 |
+
"title": "Create a visual diagram",
|
| 218 |
+
"description": "Create a diagram that visualizes the key concepts."
|
| 219 |
+
})
|
| 220 |
+
elif student_profile.learning_style.lower() == "auditory":
|
| 221 |
+
activities.append({
|
| 222 |
+
"type": "discussion",
|
| 223 |
+
"title": "Verbal explanation",
|
| 224 |
+
"description": "Record yourself explaining the concept to someone else."
|
| 225 |
+
})
|
| 226 |
+
elif student_profile.learning_style.lower() == "reading":
|
| 227 |
+
activities.append({
|
| 228 |
+
"type": "research",
|
| 229 |
+
"title": "Research and summarize",
|
| 230 |
+
"description": "Find additional resources on this topic and write a summary."
|
| 231 |
+
})
|
| 232 |
+
elif student_profile.learning_style.lower() == "kinesthetic":
|
| 233 |
+
activities.append({
|
| 234 |
+
"type": "hands-on",
|
| 235 |
+
"title": "Practical application",
|
| 236 |
+
"description": "Complete a hands-on project applying these concepts."
|
| 237 |
+
})
|
| 238 |
+
|
| 239 |
+
# Add activities based on learning objectives
|
| 240 |
+
for objective in learning_objectives:
|
| 241 |
+
if "remember" in objective.taxonomy_level.lower():
|
| 242 |
+
activities.append({
|
| 243 |
+
"type": "quiz",
|
| 244 |
+
"title": f"Quiz on {objective.subject_area}",
|
| 245 |
+
"description": f"Test your recall of key facts about {objective.description}"
|
| 246 |
+
})
|
| 247 |
+
elif "understand" in objective.taxonomy_level.lower():
|
| 248 |
+
activities.append({
|
| 249 |
+
"type": "explanation",
|
| 250 |
+
"title": f"Explain {objective.subject_area} concepts",
|
| 251 |
+
"description": f"Write an explanation of {objective.description} in your own words."
|
| 252 |
+
})
|
| 253 |
+
elif "apply" in objective.taxonomy_level.lower():
|
| 254 |
+
activities.append({
|
| 255 |
+
"type": "problem",
|
| 256 |
+
"title": f"Apply {objective.subject_area} knowledge",
|
| 257 |
+
"description": f"Solve problems that require applying {objective.description}."
|
| 258 |
+
})
|
| 259 |
+
elif "analyze" in objective.taxonomy_level.lower():
|
| 260 |
+
activities.append({
|
| 261 |
+
"type": "analysis",
|
| 262 |
+
"title": f"Analyze {objective.subject_area}",
|
| 263 |
+
"description": f"Analyze a case study related to {objective.description}."
|
| 264 |
+
})
|
| 265 |
+
|
| 266 |
+
# Limit to 3 activities
|
| 267 |
+
return activities[:3]
|
src/edtech_extensions/progress_tracking.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
EdTech Extensions - Learning Progress Tracking
|
| 3 |
+
|
| 4 |
+
This module provides functionality for tracking student learning progress
|
| 5 |
+
in the MCP EdTech system, including assessment results and skill development.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Dict, List, Optional, Any, Set, Tuple
|
| 11 |
+
from uuid import uuid4
|
| 12 |
+
import json
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
from pydantic import BaseModel, Field
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class AssessmentType(str, Enum):
|
| 19 |
+
"""Types of assessments supported by the system."""
|
| 20 |
+
QUIZ = "quiz"
|
| 21 |
+
TEST = "test"
|
| 22 |
+
ASSIGNMENT = "assignment"
|
| 23 |
+
PROJECT = "project"
|
| 24 |
+
PRACTICE = "practice"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class AssessmentResult(BaseModel):
|
| 28 |
+
"""Model representing an assessment result."""
|
| 29 |
+
id: str = Field(..., description="Unique identifier for this assessment result")
|
| 30 |
+
student_id: str = Field(..., description="ID of the student")
|
| 31 |
+
assessment_type: AssessmentType = Field(..., description="Type of assessment")
|
| 32 |
+
title: str = Field(..., description="Title of the assessment")
|
| 33 |
+
score: float = Field(..., description="Score achieved (0-100)")
|
| 34 |
+
max_score: float = Field(..., description="Maximum possible score")
|
| 35 |
+
objectives: List[str] = Field(default_factory=list, description="Learning objectives covered")
|
| 36 |
+
completed_at: str = Field(..., description="Completion timestamp")
|
| 37 |
+
feedback: str = Field("", description="Feedback on the assessment")
|
| 38 |
+
details: Dict[str, Any] = Field(default_factory=dict, description="Detailed assessment data")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class SkillLevel(str, Enum):
|
| 42 |
+
"""Skill proficiency levels."""
|
| 43 |
+
NOVICE = "novice"
|
| 44 |
+
BEGINNER = "beginner"
|
| 45 |
+
INTERMEDIATE = "intermediate"
|
| 46 |
+
ADVANCED = "advanced"
|
| 47 |
+
EXPERT = "expert"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class SkillProgress(BaseModel):
|
| 51 |
+
"""Model representing progress in a specific skill."""
|
| 52 |
+
skill_id: str = Field(..., description="Unique identifier for this skill")
|
| 53 |
+
name: str = Field(..., description="Name of the skill")
|
| 54 |
+
level: SkillLevel = Field(SkillLevel.NOVICE, description="Current skill level")
|
| 55 |
+
progress_percentage: float = Field(0.0, description="Progress towards next level (0-100)")
|
| 56 |
+
last_updated: str = Field(..., description="Last update timestamp")
|
| 57 |
+
related_objectives: List[str] = Field(default_factory=list, description="Related learning objectives")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class ProgressTracker:
|
| 61 |
+
"""
|
| 62 |
+
Tracks student learning progress in the MCP EdTech system.
|
| 63 |
+
|
| 64 |
+
This class provides functionality for recording and analyzing student
|
| 65 |
+
progress, including assessment results and skill development.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
def __init__(self, storage_dir: str = "./storage/progress"):
|
| 69 |
+
"""
|
| 70 |
+
Initialize a new ProgressTracker.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
storage_dir: Directory to store progress data in.
|
| 74 |
+
"""
|
| 75 |
+
self.storage_dir = storage_dir
|
| 76 |
+
self.assessments_dir = os.path.join(storage_dir, "assessments")
|
| 77 |
+
self.skills_dir = os.path.join(storage_dir, "skills")
|
| 78 |
+
|
| 79 |
+
os.makedirs(self.assessments_dir, exist_ok=True)
|
| 80 |
+
os.makedirs(self.skills_dir, exist_ok=True)
|
| 81 |
+
|
| 82 |
+
def _get_student_assessments_dir(self, student_id: str) -> str:
|
| 83 |
+
"""
|
| 84 |
+
Get the directory for a student's assessment results.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
student_id: The ID of the student.
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
The directory path.
|
| 91 |
+
"""
|
| 92 |
+
path = os.path.join(self.assessments_dir, student_id)
|
| 93 |
+
os.makedirs(path, exist_ok=True)
|
| 94 |
+
return path
|
| 95 |
+
|
| 96 |
+
def _get_student_skills_dir(self, student_id: str) -> str:
|
| 97 |
+
"""
|
| 98 |
+
Get the directory for a student's skill progress.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
student_id: The ID of the student.
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
The directory path.
|
| 105 |
+
"""
|
| 106 |
+
path = os.path.join(self.skills_dir, student_id)
|
| 107 |
+
os.makedirs(path, exist_ok=True)
|
| 108 |
+
return path
|
| 109 |
+
|
| 110 |
+
async def record_assessment(
|
| 111 |
+
self,
|
| 112 |
+
student_id: str,
|
| 113 |
+
assessment_type: AssessmentType,
|
| 114 |
+
title: str,
|
| 115 |
+
score: float,
|
| 116 |
+
max_score: float,
|
| 117 |
+
objectives: List[str],
|
| 118 |
+
feedback: str = "",
|
| 119 |
+
details: Dict[str, Any] = None
|
| 120 |
+
) -> AssessmentResult:
|
| 121 |
+
"""
|
| 122 |
+
Record a new assessment result.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
student_id: ID of the student.
|
| 126 |
+
assessment_type: Type of assessment.
|
| 127 |
+
title: Title of the assessment.
|
| 128 |
+
score: Score achieved.
|
| 129 |
+
max_score: Maximum possible score.
|
| 130 |
+
objectives: Learning objectives covered.
|
| 131 |
+
feedback: Feedback on the assessment.
|
| 132 |
+
details: Detailed assessment data.
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
The newly created AssessmentResult.
|
| 136 |
+
"""
|
| 137 |
+
assessment_id = str(uuid4())
|
| 138 |
+
completed_at = datetime.utcnow().isoformat()
|
| 139 |
+
|
| 140 |
+
result = AssessmentResult(
|
| 141 |
+
id=assessment_id,
|
| 142 |
+
student_id=student_id,
|
| 143 |
+
assessment_type=assessment_type,
|
| 144 |
+
title=title,
|
| 145 |
+
score=score,
|
| 146 |
+
max_score=max_score,
|
| 147 |
+
objectives=objectives,
|
| 148 |
+
completed_at=completed_at,
|
| 149 |
+
feedback=feedback,
|
| 150 |
+
details=details or {}
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# Save to disk
|
| 154 |
+
await self._save_assessment(result)
|
| 155 |
+
|
| 156 |
+
# Update skill progress based on assessment
|
| 157 |
+
for objective_id in objectives:
|
| 158 |
+
await self._update_skills_for_objective(
|
| 159 |
+
student_id,
|
| 160 |
+
objective_id,
|
| 161 |
+
score / max_score
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
return result
|
| 165 |
+
|
| 166 |
+
async def _save_assessment(self, assessment: AssessmentResult) -> bool:
|
| 167 |
+
"""
|
| 168 |
+
Save an assessment result to disk.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
assessment: The AssessmentResult to save.
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
True if the save was successful, False otherwise.
|
| 175 |
+
"""
|
| 176 |
+
try:
|
| 177 |
+
dir_path = self._get_student_assessments_dir(assessment.student_id)
|
| 178 |
+
file_path = os.path.join(dir_path, f"{assessment.id}.json")
|
| 179 |
+
|
| 180 |
+
with open(file_path, 'w') as f:
|
| 181 |
+
json.dump(assessment.dict(), f, indent=2)
|
| 182 |
+
return True
|
| 183 |
+
except Exception as e:
|
| 184 |
+
print(f"Error saving assessment result: {e}")
|
| 185 |
+
return False
|
| 186 |
+
|
| 187 |
+
async def get_assessment(
|
| 188 |
+
self,
|
| 189 |
+
student_id: str,
|
| 190 |
+
assessment_id: str
|
| 191 |
+
) -> Optional[AssessmentResult]:
|
| 192 |
+
"""
|
| 193 |
+
Get an assessment result by ID.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
student_id: ID of the student.
|
| 197 |
+
assessment_id: ID of the assessment.
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
The AssessmentResult if found, None otherwise.
|
| 201 |
+
"""
|
| 202 |
+
try:
|
| 203 |
+
dir_path = self._get_student_assessments_dir(student_id)
|
| 204 |
+
file_path = os.path.join(dir_path, f"{assessment_id}.json")
|
| 205 |
+
|
| 206 |
+
if not os.path.exists(file_path):
|
| 207 |
+
return None
|
| 208 |
+
|
| 209 |
+
with open(file_path, 'r') as f:
|
| 210 |
+
data = json.load(f)
|
| 211 |
+
|
| 212 |
+
return AssessmentResult(**data)
|
| 213 |
+
except Exception as e:
|
| 214 |
+
print(f"Error loading assessment result: {e}")
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
async def list_assessments(
|
| 218 |
+
self,
|
| 219 |
+
student_id: str,
|
| 220 |
+
assessment_type: Optional[AssessmentType] = None
|
| 221 |
+
) -> List[AssessmentResult]:
|
| 222 |
+
"""
|
| 223 |
+
List assessment results for a student.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
student_id: ID of the student.
|
| 227 |
+
assessment_type: Optional filter by assessment type.
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
List of AssessmentResult objects.
|
| 231 |
+
"""
|
| 232 |
+
results = []
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
dir_path = self._get_student_assessments_dir(student_id)
|
| 236 |
+
|
| 237 |
+
for filename in os.listdir(dir_path):
|
| 238 |
+
if filename.endswith('.json'):
|
| 239 |
+
file_path = os.path.join(dir_path, filename)
|
| 240 |
+
|
| 241 |
+
with open(file_path, 'r') as f:
|
| 242 |
+
data = json.load(f)
|
| 243 |
+
|
| 244 |
+
result = AssessmentResult(**data)
|
| 245 |
+
|
| 246 |
+
# Apply filter if specified
|
| 247 |
+
if assessment_type is None or result.assessment_type == assessment_type:
|
| 248 |
+
results.append(result)
|
| 249 |
+
except Exception as e:
|
| 250 |
+
print(f"Error listing assessment results: {e}")
|
| 251 |
+
|
| 252 |
+
# Sort by completion date, newest first
|
| 253 |
+
results.sort(key=lambda x: x.completed_at, reverse=True)
|
| 254 |
+
return results
|
| 255 |
+
|
| 256 |
+
async def _update_skills_for_objective(
|
| 257 |
+
self,
|
| 258 |
+
student_id: str,
|
| 259 |
+
objective_id: str,
|
| 260 |
+
performance: float
|
| 261 |
+
) -> None:
|
| 262 |
+
"""
|
| 263 |
+
Update skill progress based on performance in an objective.
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
student_id: ID of the student.
|
| 267 |
+
objective_id: ID of the learning objective.
|
| 268 |
+
performance: Performance level (0-1).
|
| 269 |
+
"""
|
| 270 |
+
# In a real implementation, this would map objectives to skills
|
| 271 |
+
# and update skill progress accordingly.
|
| 272 |
+
# For this demo, we'll create/update a skill with the same ID as the objective.
|
| 273 |
+
|
| 274 |
+
skill = await self.get_skill_progress(student_id, objective_id)
|
| 275 |
+
|
| 276 |
+
if skill:
|
| 277 |
+
# Update existing skill
|
| 278 |
+
new_progress = skill.progress_percentage + (performance * 20) # Increase by up to 20%
|
| 279 |
+
|
| 280 |
+
# Check if we should level up
|
| 281 |
+
if new_progress >= 100:
|
| 282 |
+
# Level up and reset progress
|
| 283 |
+
levels = list(SkillLevel)
|
| 284 |
+
current_index = levels.index(skill.level)
|
| 285 |
+
|
| 286 |
+
if current_index < len(levels) - 1:
|
| 287 |
+
# Move to next level
|
| 288 |
+
skill.level = levels[current_index + 1]
|
| 289 |
+
skill.progress_percentage = new_progress - 100
|
| 290 |
+
else:
|
| 291 |
+
# Already at max level
|
| 292 |
+
skill.progress_percentage = 100
|
| 293 |
+
else:
|
| 294 |
+
skill.progress_percentage = new_progress
|
| 295 |
+
|
| 296 |
+
skill.last_updated = datetime.utcnow().isoformat()
|
| 297 |
+
else:
|
| 298 |
+
# Create new skill
|
| 299 |
+
skill = SkillProgress(
|
| 300 |
+
skill_id=objective_id,
|
| 301 |
+
name=f"Skill for objective {objective_id}", # In a real app, would use actual skill name
|
| 302 |
+
level=SkillLevel.NOVICE,
|
| 303 |
+
progress_percentage=performance * 20, # Initial progress based on performance
|
| 304 |
+
last_updated=datetime.utcnow().isoformat(),
|
| 305 |
+
related_objectives=[objective_id]
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
await self._save_skill_progress(student_id, skill)
|
| 309 |
+
|
| 310 |
+
async def _save_skill_progress(
|
| 311 |
+
self,
|
| 312 |
+
student_id: str,
|
| 313 |
+
skill: SkillProgress
|
| 314 |
+
) -> bool:
|
| 315 |
+
"""
|
| 316 |
+
Save skill progress to disk.
|
| 317 |
+
|
| 318 |
+
Args:
|
| 319 |
+
student_id: ID of the student.
|
| 320 |
+
skill: The SkillProgress to save.
|
| 321 |
+
|
| 322 |
+
Returns:
|
| 323 |
+
True if the save was successful, False otherwise.
|
| 324 |
+
"""
|
| 325 |
+
try:
|
| 326 |
+
dir_path = self._get_student_skills_dir(student_id)
|
| 327 |
+
file_path = os.path.join(dir_path, f"{skill.skill_id}.json")
|
| 328 |
+
|
| 329 |
+
with open(file_path, 'w') as f:
|
| 330 |
+
json.dump(skill.dict(), f, indent=2)
|
| 331 |
+
return True
|
| 332 |
+
except Exception as e:
|
| 333 |
+
print(f"Error saving skill progress: {e}")
|
| 334 |
+
return False
|
| 335 |
+
|
| 336 |
+
async def get_skill_progress(
|
| 337 |
+
self,
|
| 338 |
+
student_id: str,
|
| 339 |
+
skill_id: str
|
| 340 |
+
) -> Optional[SkillProgress]:
|
| 341 |
+
"""
|
| 342 |
+
Get skill progress by ID.
|
| 343 |
+
|
| 344 |
+
Args:
|
| 345 |
+
student_id: ID of the student.
|
| 346 |
+
skill_id: ID of the skill.
|
| 347 |
+
|
| 348 |
+
Returns:
|
| 349 |
+
The SkillProgress if found, None otherwise.
|
| 350 |
+
"""
|
| 351 |
+
try:
|
| 352 |
+
dir_path = self._get_student_skills_dir(student_id)
|
| 353 |
+
file_path = os.path.join(dir_path, f"{skill_id}.json")
|
| 354 |
+
|
| 355 |
+
if not os.path.exists(file_path):
|
| 356 |
+
return None
|
| 357 |
+
|
| 358 |
+
with open(file_path, 'r') as f:
|
| 359 |
+
data = json.load(f)
|
| 360 |
+
|
| 361 |
+
return SkillProgress(**data)
|
| 362 |
+
except Exception as e:
|
| 363 |
+
print(f"Error loading skill progress: {e}")
|
| 364 |
+
return None
|
| 365 |
+
|
| 366 |
+
async def list_skills(self, student_id: str) -> List[SkillProgress]:
|
| 367 |
+
"""
|
| 368 |
+
List skill progress for a student.
|
| 369 |
+
|
| 370 |
+
Args:
|
| 371 |
+
student_id: ID of the student.
|
| 372 |
+
|
| 373 |
+
Returns:
|
| 374 |
+
List of SkillProgress objects.
|
| 375 |
+
"""
|
| 376 |
+
skills = []
|
| 377 |
+
|
| 378 |
+
try:
|
| 379 |
+
dir_path = self._get_student_skills_dir(student_id)
|
| 380 |
+
|
| 381 |
+
if os.path.exists(dir_path):
|
| 382 |
+
for filename in os.listdir(dir_path):
|
| 383 |
+
if filename.endswith('.json'):
|
| 384 |
+
file_path = os.path.join(dir_path, filename)
|
| 385 |
+
|
| 386 |
+
with open(file_path, 'r') as f:
|
| 387 |
+
data = json.load(f)
|
| 388 |
+
|
| 389 |
+
skills.append(SkillProgress(**data))
|
| 390 |
+
except Exception as e:
|
| 391 |
+
print(f"Error listing skill progress: {e}")
|
| 392 |
+
|
| 393 |
+
# Sort by level and then by progress percentage
|
| 394 |
+
skills.sort(
|
| 395 |
+
key=lambda x: (list(SkillLevel).index(x.level), x.progress_percentage),
|
| 396 |
+
reverse=True
|
| 397 |
+
)
|
| 398 |
+
return skills
|
| 399 |
+
|
| 400 |
+
async def get_progress_summary(self, student_id: str) -> Dict[str, Any]:
|
| 401 |
+
"""
|
| 402 |
+
Get a summary of a student's learning progress.
|
| 403 |
+
|
| 404 |
+
Args:
|
| 405 |
+
student_id: ID of the student.
|
| 406 |
+
|
| 407 |
+
Returns:
|
| 408 |
+
Dictionary containing progress summary.
|
| 409 |
+
"""
|
| 410 |
+
assessments = await self.list_assessments(student_id)
|
| 411 |
+
skills = await self.list_skills(student_id)
|
| 412 |
+
|
| 413 |
+
# Calculate assessment statistics
|
| 414 |
+
total_assessments = len(assessments)
|
| 415 |
+
average_score = 0
|
| 416 |
+
assessment_types_count = {}
|
| 417 |
+
recent_assessments = []
|
| 418 |
+
|
| 419 |
+
if total_assessments > 0:
|
| 420 |
+
total_score_percentage = 0
|
| 421 |
+
|
| 422 |
+
for assessment in assessments:
|
| 423 |
+
score_percentage = (assessment.score / assessment.max_score) * 100
|
| 424 |
+
total_score_percentage += score_percentage
|
| 425 |
+
|
| 426 |
+
# Count by type
|
| 427 |
+
assessment_type = assessment.assessment_type
|
| 428 |
+
assessment_types_count[assessment_type] = assessment_types_count.get(assessment_type, 0) + 1
|
| 429 |
+
|
| 430 |
+
average_score = total_score_percentage / total_assessments
|
| 431 |
+
recent_assessments = [a.dict() for a in assessments[:5]] # 5 most recent
|
| 432 |
+
|
| 433 |
+
# Summarize skills
|
| 434 |
+
skill_levels = {}
|
| 435 |
+
for skill in skills:
|
| 436 |
+
level = skill.level
|
| 437 |
+
skill_levels[level] = skill_levels.get(level, 0) + 1
|
| 438 |
+
|
| 439 |
+
# Create summary
|
| 440 |
+
return {
|
| 441 |
+
"student_id": student_id,
|
| 442 |
+
"total_assessments": total_assessments,
|
| 443 |
+
"average_score": round(average_score, 2),
|
| 444 |
+
"assessment_types_count": assessment_types_count,
|
| 445 |
+
"recent_assessments": recent_assessments,
|
| 446 |
+
"total_skills": len(skills),
|
| 447 |
+
"skill_levels": skill_levels,
|
| 448 |
+
"top_skills": [s.dict() for s in skills[:3]], # 3 highest-level skills
|
| 449 |
+
"generated_at": datetime.utcnow().isoformat()
|
| 450 |
+
}
|
src/edtech_extensions/student_profile.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
EdTech Extensions - Student Profile Management
|
| 3 |
+
|
| 4 |
+
This module provides functionality for managing student profiles in the MCP EdTech system.
|
| 5 |
+
It includes classes and functions for creating, retrieving, updating, and analyzing student profiles.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import Dict, List, Optional, Any, Set
|
| 10 |
+
from uuid import uuid4
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
from pydantic import BaseModel, Field
|
| 16 |
+
|
| 17 |
+
from ..mcp_core.protocol import EducationalLevel, LearningObjective, StudentProfile
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class LearningStyle(str, Enum):
|
| 21 |
+
"""Learning styles for student profile customization."""
|
| 22 |
+
VISUAL = "visual"
|
| 23 |
+
AUDITORY = "auditory"
|
| 24 |
+
READING = "reading"
|
| 25 |
+
KINESTHETIC = "kinesthetic"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class StudentProfileManager:
|
| 29 |
+
"""
|
| 30 |
+
Manages student profiles in the MCP EdTech system.
|
| 31 |
+
|
| 32 |
+
This class provides functionality for creating, retrieving, updating,
|
| 33 |
+
and analyzing student profiles.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self, storage_dir: str = "./storage/students"):
|
| 37 |
+
"""
|
| 38 |
+
Initialize a new StudentProfileManager.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
storage_dir: Directory to store student profiles in.
|
| 42 |
+
"""
|
| 43 |
+
self.storage_dir = storage_dir
|
| 44 |
+
os.makedirs(storage_dir, exist_ok=True)
|
| 45 |
+
self.profiles: Dict[str, StudentProfile] = {}
|
| 46 |
+
|
| 47 |
+
def _get_profile_path(self, student_id: str) -> str:
|
| 48 |
+
"""
|
| 49 |
+
Get the file path for a student profile.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
student_id: The ID of the student.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
The file path for the student profile.
|
| 56 |
+
"""
|
| 57 |
+
return os.path.join(self.storage_dir, f"{student_id}.json")
|
| 58 |
+
|
| 59 |
+
async def create_profile(
|
| 60 |
+
self,
|
| 61 |
+
name: str,
|
| 62 |
+
educational_level: EducationalLevel,
|
| 63 |
+
learning_style: Optional[str] = None,
|
| 64 |
+
interests: Optional[List[str]] = None,
|
| 65 |
+
strengths: Optional[List[str]] = None,
|
| 66 |
+
areas_for_improvement: Optional[List[str]] = None
|
| 67 |
+
) -> StudentProfile:
|
| 68 |
+
"""
|
| 69 |
+
Create a new student profile.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
name: Student's name.
|
| 73 |
+
educational_level: Student's educational level.
|
| 74 |
+
learning_style: Student's preferred learning style.
|
| 75 |
+
interests: Student's interests.
|
| 76 |
+
strengths: Student's academic strengths.
|
| 77 |
+
areas_for_improvement: Areas where the student needs improvement.
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
The newly created StudentProfile.
|
| 81 |
+
"""
|
| 82 |
+
student_id = str(uuid4())
|
| 83 |
+
|
| 84 |
+
profile = StudentProfile(
|
| 85 |
+
id=student_id,
|
| 86 |
+
name=name,
|
| 87 |
+
educational_level=educational_level,
|
| 88 |
+
learning_style=learning_style or "",
|
| 89 |
+
interests=interests or [],
|
| 90 |
+
strengths=strengths or [],
|
| 91 |
+
areas_for_improvement=areas_for_improvement or [],
|
| 92 |
+
completed_objectives=[],
|
| 93 |
+
current_objectives=[]
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Save to memory and disk
|
| 97 |
+
self.profiles[student_id] = profile
|
| 98 |
+
await self._save_profile(profile)
|
| 99 |
+
|
| 100 |
+
return profile
|
| 101 |
+
|
| 102 |
+
async def _save_profile(self, profile: StudentProfile) -> bool:
|
| 103 |
+
"""
|
| 104 |
+
Save a student profile to disk.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
profile: The StudentProfile to save.
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
True if the save was successful, False otherwise.
|
| 111 |
+
"""
|
| 112 |
+
try:
|
| 113 |
+
with open(self._get_profile_path(profile.id), 'w') as f:
|
| 114 |
+
json.dump(profile.dict(), f, indent=2)
|
| 115 |
+
return True
|
| 116 |
+
except Exception as e:
|
| 117 |
+
print(f"Error saving student profile: {e}")
|
| 118 |
+
return False
|
| 119 |
+
|
| 120 |
+
async def get_profile(self, student_id: str) -> Optional[StudentProfile]:
|
| 121 |
+
"""
|
| 122 |
+
Get a student profile by ID.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
student_id: The ID of the student.
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
The StudentProfile if found, None otherwise.
|
| 129 |
+
"""
|
| 130 |
+
# Check memory cache first
|
| 131 |
+
if student_id in self.profiles:
|
| 132 |
+
return self.profiles[student_id]
|
| 133 |
+
|
| 134 |
+
# Try to load from disk
|
| 135 |
+
try:
|
| 136 |
+
path = self._get_profile_path(student_id)
|
| 137 |
+
if not os.path.exists(path):
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
with open(path, 'r') as f:
|
| 141 |
+
data = json.load(f)
|
| 142 |
+
|
| 143 |
+
profile = StudentProfile(**data)
|
| 144 |
+
self.profiles[student_id] = profile
|
| 145 |
+
return profile
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"Error loading student profile: {e}")
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
async def update_profile(
|
| 151 |
+
self,
|
| 152 |
+
student_id: str,
|
| 153 |
+
updates: Dict[str, Any]
|
| 154 |
+
) -> Optional[StudentProfile]:
|
| 155 |
+
"""
|
| 156 |
+
Update a student profile.
|
| 157 |
+
|
| 158 |
+
Args:
|
| 159 |
+
student_id: The ID of the student.
|
| 160 |
+
updates: Dictionary of updates to apply.
|
| 161 |
+
|
| 162 |
+
Returns:
|
| 163 |
+
The updated StudentProfile if found, None otherwise.
|
| 164 |
+
"""
|
| 165 |
+
profile = await self.get_profile(student_id)
|
| 166 |
+
if not profile:
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
# Apply updates
|
| 170 |
+
profile_dict = profile.dict()
|
| 171 |
+
for key, value in updates.items():
|
| 172 |
+
if key in profile_dict:
|
| 173 |
+
setattr(profile, key, value)
|
| 174 |
+
|
| 175 |
+
# Save updated profile
|
| 176 |
+
await self._save_profile(profile)
|
| 177 |
+
self.profiles[student_id] = profile
|
| 178 |
+
|
| 179 |
+
return profile
|
| 180 |
+
|
| 181 |
+
async def delete_profile(self, student_id: str) -> bool:
|
| 182 |
+
"""
|
| 183 |
+
Delete a student profile.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
student_id: The ID of the student.
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
True if the deletion was successful, False otherwise.
|
| 190 |
+
"""
|
| 191 |
+
try:
|
| 192 |
+
# Remove from memory
|
| 193 |
+
if student_id in self.profiles:
|
| 194 |
+
del self.profiles[student_id]
|
| 195 |
+
|
| 196 |
+
# Remove from disk
|
| 197 |
+
path = self._get_profile_path(student_id)
|
| 198 |
+
if os.path.exists(path):
|
| 199 |
+
os.remove(path)
|
| 200 |
+
return True
|
| 201 |
+
return False
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"Error deleting student profile: {e}")
|
| 204 |
+
return False
|
| 205 |
+
|
| 206 |
+
async def list_profiles(self) -> List[StudentProfile]:
|
| 207 |
+
"""
|
| 208 |
+
List all student profiles.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
List of StudentProfile objects.
|
| 212 |
+
"""
|
| 213 |
+
profiles = []
|
| 214 |
+
|
| 215 |
+
# Load all profiles from disk
|
| 216 |
+
try:
|
| 217 |
+
for filename in os.listdir(self.storage_dir):
|
| 218 |
+
if filename.endswith('.json'):
|
| 219 |
+
student_id = filename[:-5] # Remove .json extension
|
| 220 |
+
profile = await self.get_profile(student_id)
|
| 221 |
+
if profile:
|
| 222 |
+
profiles.append(profile)
|
| 223 |
+
except Exception as e:
|
| 224 |
+
print(f"Error listing student profiles: {e}")
|
| 225 |
+
|
| 226 |
+
return profiles
|
| 227 |
+
|
| 228 |
+
async def add_completed_objective(
|
| 229 |
+
self,
|
| 230 |
+
student_id: str,
|
| 231 |
+
objective_id: str
|
| 232 |
+
) -> Optional[StudentProfile]:
|
| 233 |
+
"""
|
| 234 |
+
Add a completed learning objective to a student profile.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
student_id: The ID of the student.
|
| 238 |
+
objective_id: The ID of the completed learning objective.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
The updated StudentProfile if found, None otherwise.
|
| 242 |
+
"""
|
| 243 |
+
profile = await self.get_profile(student_id)
|
| 244 |
+
if not profile:
|
| 245 |
+
return None
|
| 246 |
+
|
| 247 |
+
# Add to completed objectives if not already there
|
| 248 |
+
if objective_id not in profile.completed_objectives:
|
| 249 |
+
profile.completed_objectives.append(objective_id)
|
| 250 |
+
|
| 251 |
+
# Remove from current objectives if present
|
| 252 |
+
if objective_id in profile.current_objectives:
|
| 253 |
+
profile.current_objectives.remove(objective_id)
|
| 254 |
+
|
| 255 |
+
# Save updated profile
|
| 256 |
+
await self._save_profile(profile)
|
| 257 |
+
self.profiles[student_id] = profile
|
| 258 |
+
|
| 259 |
+
return profile
|
| 260 |
+
|
| 261 |
+
async def add_current_objective(
|
| 262 |
+
self,
|
| 263 |
+
student_id: str,
|
| 264 |
+
objective_id: str
|
| 265 |
+
) -> Optional[StudentProfile]:
|
| 266 |
+
"""
|
| 267 |
+
Add a current learning objective to a student profile.
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
student_id: The ID of the student.
|
| 271 |
+
objective_id: The ID of the learning objective.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
The updated StudentProfile if found, None otherwise.
|
| 275 |
+
"""
|
| 276 |
+
profile = await self.get_profile(student_id)
|
| 277 |
+
if not profile:
|
| 278 |
+
return None
|
| 279 |
+
|
| 280 |
+
# Add to current objectives if not already there and not completed
|
| 281 |
+
if (objective_id not in profile.current_objectives and
|
| 282 |
+
objective_id not in profile.completed_objectives):
|
| 283 |
+
profile.current_objectives.append(objective_id)
|
| 284 |
+
|
| 285 |
+
# Save updated profile
|
| 286 |
+
await self._save_profile(profile)
|
| 287 |
+
self.profiles[student_id] = profile
|
| 288 |
+
|
| 289 |
+
return profile
|
src/mcp_core/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Core - Model Context Protocol implementation for EdTech applications.
|
| 3 |
+
"""
|
src/mcp_core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (262 Bytes). View file
|
|
|
src/mcp_core/__pycache__/context.cpython-311.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
src/mcp_core/__pycache__/model_adapters.cpython-311.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|
src/mcp_core/__pycache__/processor.cpython-311.pyc
ADDED
|
Binary file (7.31 kB). View file
|
|
|
src/mcp_core/__pycache__/protocol.cpython-311.pyc
ADDED
|
Binary file (16.7 kB). View file
|
|
|
src/mcp_core/context.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from typing import Dict, List, Optional, Any, Union
|
| 3 |
+
from uuid import uuid4
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Context:
|
| 8 |
+
def __init__(self, context_id: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, max_history_length: int = 100):
|
| 9 |
+
self.context_id = context_id or str(uuid4())
|
| 10 |
+
self.metadata = metadata or {}
|
| 11 |
+
self.max_history_length = max_history_length
|
| 12 |
+
self.history: List[Dict[str, Any]] = []
|
| 13 |
+
self.state: Dict[str, Any] = {}
|
| 14 |
+
self.created_at = datetime.utcnow()
|
| 15 |
+
self.updated_at = self.created_at
|
| 16 |
+
|
| 17 |
+
def add_interaction(self, input_data: Dict[str, Any], output_data: Dict[str, Any]) -> None:
|
| 18 |
+
interaction = {
|
| 19 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 20 |
+
"input": input_data,
|
| 21 |
+
"output": output_data
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
self.history.append(interaction)
|
| 25 |
+
|
| 26 |
+
if len(self.history) > self.max_history_length:
|
| 27 |
+
self.history = self.history[-self.max_history_length:]
|
| 28 |
+
|
| 29 |
+
self.updated_at = datetime.utcnow()
|
| 30 |
+
|
| 31 |
+
def update_state(self, new_state: Dict[str, Any], merge: bool = True) -> None:
|
| 32 |
+
if merge:
|
| 33 |
+
self.state.update(new_state)
|
| 34 |
+
else:
|
| 35 |
+
self.state = new_state
|
| 36 |
+
|
| 37 |
+
self.updated_at = datetime.utcnow()
|
| 38 |
+
|
| 39 |
+
def get_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 40 |
+
if limit is None or limit >= len(self.history):
|
| 41 |
+
return self.history
|
| 42 |
+
return self.history[-limit:]
|
| 43 |
+
|
| 44 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 45 |
+
return {
|
| 46 |
+
"context_id": self.context_id,
|
| 47 |
+
"metadata": self.metadata,
|
| 48 |
+
"state": self.state,
|
| 49 |
+
"history": self.history,
|
| 50 |
+
"created_at": self.created_at.isoformat(),
|
| 51 |
+
"updated_at": self.updated_at.isoformat(),
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
def to_json(self) -> str:
|
| 55 |
+
return json.dumps(self.to_dict())
|
| 56 |
+
|
| 57 |
+
@classmethod
|
| 58 |
+
def from_dict(cls, data: Dict[str, Any]) -> 'Context':
|
| 59 |
+
context = cls(context_id=data.get("context_id"))
|
| 60 |
+
context.metadata = data.get("metadata", {})
|
| 61 |
+
context.state = data.get("state", {})
|
| 62 |
+
context.history = data.get("history", [])
|
| 63 |
+
|
| 64 |
+
if "created_at" in data:
|
| 65 |
+
context.created_at = datetime.fromisoformat(data["created_at"])
|
| 66 |
+
if "updated_at" in data:
|
| 67 |
+
context.updated_at = datetime.fromisoformat(data["updated_at"])
|
| 68 |
+
|
| 69 |
+
return context
|
| 70 |
+
|
| 71 |
+
@classmethod
|
| 72 |
+
def from_json(cls, json_str: str) -> 'Context':
|
| 73 |
+
data = json.loads(json_str)
|
| 74 |
+
return cls.from_dict(data)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class ContextManager:
|
| 78 |
+
def __init__(self):
|
| 79 |
+
self.contexts: Dict[str, Context] = {}
|
| 80 |
+
|
| 81 |
+
def create_context(self, metadata: Optional[Dict[str, Any]] = None, context_id: Optional[str] = None) -> Context:
|
| 82 |
+
context = Context(context_id=context_id, metadata=metadata)
|
| 83 |
+
self.contexts[context.context_id] = context
|
| 84 |
+
return context
|
| 85 |
+
|
| 86 |
+
def get_context(self, context_id: str) -> Optional[Context]:
|
| 87 |
+
return self.contexts.get(context_id)
|
| 88 |
+
|
| 89 |
+
def update_context(self, context_id: str, updates: Dict[str, Any]) -> Optional[Context]:
|
| 90 |
+
context = self.get_context(context_id)
|
| 91 |
+
if not context:
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
if "metadata" in updates:
|
| 95 |
+
context.metadata.update(updates["metadata"])
|
| 96 |
+
|
| 97 |
+
if "state" in updates:
|
| 98 |
+
context.update_state(updates["state"])
|
| 99 |
+
|
| 100 |
+
return context
|
| 101 |
+
|
| 102 |
+
def delete_context(self, context_id: str) -> bool:
|
| 103 |
+
if context_id in self.contexts:
|
| 104 |
+
del self.contexts[context_id]
|
| 105 |
+
return True
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
def list_contexts(self) -> List[str]:
|
| 109 |
+
return list(self.contexts.keys())
|
src/mcp_core/model_adapters.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Core - Model Interface Adapters
|
| 3 |
+
|
| 4 |
+
This module provides adapters for interfacing with various AI models,
|
| 5 |
+
allowing the MCP system to work with different model providers and types
|
| 6 |
+
while maintaining a consistent interface.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from abc import ABC, abstractmethod
|
| 10 |
+
from typing import Dict, Any, List, Optional, Union
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ModelAdapter(ABC):
|
| 14 |
+
"""
|
| 15 |
+
Abstract base class for model adapters.
|
| 16 |
+
|
| 17 |
+
Model adapters provide a consistent interface for interacting with different
|
| 18 |
+
AI models, abstracting away the specifics of each model's API.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
@abstractmethod
|
| 22 |
+
async def process(self, input_data: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
|
| 23 |
+
"""
|
| 24 |
+
Process an input with the model and return the output.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
input_data: The input data to process.
|
| 28 |
+
context: Optional context information to guide the model.
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
The model's output.
|
| 32 |
+
"""
|
| 33 |
+
pass
|
| 34 |
+
|
| 35 |
+
@abstractmethod
|
| 36 |
+
def get_capabilities(self) -> Dict[str, Any]:
|
| 37 |
+
"""
|
| 38 |
+
Get the capabilities of this model.
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
Dictionary describing the model's capabilities.
|
| 42 |
+
"""
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class MockModelAdapter(ModelAdapter):
|
| 47 |
+
"""
|
| 48 |
+
A mock model adapter for testing and demonstration purposes.
|
| 49 |
+
|
| 50 |
+
This adapter simulates responses without requiring an actual AI model.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def __init__(self, model_id: str = "mock-model", capabilities: Dict[str, Any] = None):
|
| 54 |
+
"""
|
| 55 |
+
Initialize a new MockModelAdapter.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
model_id: Identifier for this mock model.
|
| 59 |
+
capabilities: Dictionary of capabilities this mock model supports.
|
| 60 |
+
"""
|
| 61 |
+
self.model_id = model_id
|
| 62 |
+
self._capabilities = capabilities or {
|
| 63 |
+
"text_generation": True,
|
| 64 |
+
"question_answering": True,
|
| 65 |
+
"content_evaluation": True,
|
| 66 |
+
"max_input_length": 1000,
|
| 67 |
+
"supports_streaming": False
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
async def process(self, input_data: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
|
| 71 |
+
"""
|
| 72 |
+
Process an input with the mock model.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
input_data: The input data to process.
|
| 76 |
+
context: Optional context information.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
A mock response based on the input.
|
| 80 |
+
"""
|
| 81 |
+
# Extract the input text or use a default
|
| 82 |
+
input_text = input_data.get("text", "")
|
| 83 |
+
|
| 84 |
+
# Generate a mock response based on the input
|
| 85 |
+
if "question" in input_text.lower():
|
| 86 |
+
response = f"This is a mock answer to your question about {input_text.split('?')[0]}."
|
| 87 |
+
elif "explain" in input_text.lower():
|
| 88 |
+
topic = input_text.lower().split("explain")[1].strip()
|
| 89 |
+
response = f"Here's a mock explanation of {topic}. This would contain educational content in a real implementation."
|
| 90 |
+
else:
|
| 91 |
+
response = f"I've processed your input: '{input_text}'. This is a mock response that would be more relevant in a real implementation."
|
| 92 |
+
|
| 93 |
+
# Include some educational metadata in the response
|
| 94 |
+
return {
|
| 95 |
+
"text": response,
|
| 96 |
+
"model_id": self.model_id,
|
| 97 |
+
"confidence": 0.85,
|
| 98 |
+
"educational_metadata": {
|
| 99 |
+
"complexity_level": "intermediate",
|
| 100 |
+
"topics": ["sample", "mock", "demonstration"],
|
| 101 |
+
"suggested_follow_ups": [
|
| 102 |
+
"Tell me more about this topic",
|
| 103 |
+
"Can you provide an example?",
|
| 104 |
+
"What are the key concepts I should understand?"
|
| 105 |
+
]
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
def get_capabilities(self) -> Dict[str, Any]:
|
| 110 |
+
"""
|
| 111 |
+
Get the capabilities of this mock model.
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
Dictionary describing the mock model's capabilities.
|
| 115 |
+
"""
|
| 116 |
+
return self._capabilities
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class OpenAIAdapter(ModelAdapter):
|
| 120 |
+
"""
|
| 121 |
+
Adapter for OpenAI models.
|
| 122 |
+
|
| 123 |
+
This adapter provides an interface to OpenAI's models through their API.
|
| 124 |
+
Note: This is a placeholder implementation. In a real application, you would
|
| 125 |
+
need to implement the actual API calls to OpenAI.
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
def __init__(self, model_name: str = "gpt-3.5-turbo", api_key: Optional[str] = None):
|
| 129 |
+
"""
|
| 130 |
+
Initialize a new OpenAIAdapter.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
model_name: The name of the OpenAI model to use.
|
| 134 |
+
api_key: OpenAI API key. If None, will attempt to use environment variable.
|
| 135 |
+
"""
|
| 136 |
+
self.model_name = model_name
|
| 137 |
+
self.api_key = api_key
|
| 138 |
+
# In a real implementation, you would initialize the OpenAI client here
|
| 139 |
+
|
| 140 |
+
async def process(self, input_data: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
|
| 141 |
+
"""
|
| 142 |
+
Process an input with an OpenAI model.
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
input_data: The input data to process.
|
| 146 |
+
context: Optional context information.
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
The model's output.
|
| 150 |
+
"""
|
| 151 |
+
# This is a placeholder. In a real implementation, you would:
|
| 152 |
+
# 1. Format the input and context for the OpenAI API
|
| 153 |
+
# 2. Make the API call
|
| 154 |
+
# 3. Process and return the response
|
| 155 |
+
|
| 156 |
+
# Placeholder response
|
| 157 |
+
return {
|
| 158 |
+
"text": f"This is a placeholder response from {self.model_name}. In a real implementation, this would be the actual model output.",
|
| 159 |
+
"model": self.model_name,
|
| 160 |
+
"usage": {
|
| 161 |
+
"prompt_tokens": 10,
|
| 162 |
+
"completion_tokens": 20,
|
| 163 |
+
"total_tokens": 30
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
def get_capabilities(self) -> Dict[str, Any]:
|
| 168 |
+
"""
|
| 169 |
+
Get the capabilities of this OpenAI model.
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
Dictionary describing the model's capabilities.
|
| 173 |
+
"""
|
| 174 |
+
# This would be more accurate in a real implementation
|
| 175 |
+
capabilities = {
|
| 176 |
+
"text_generation": True,
|
| 177 |
+
"question_answering": True,
|
| 178 |
+
"content_evaluation": True,
|
| 179 |
+
"supports_streaming": True
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
# Different capabilities based on model
|
| 183 |
+
if "gpt-4" in self.model_name:
|
| 184 |
+
capabilities.update({
|
| 185 |
+
"max_input_length": 8000,
|
| 186 |
+
"reasoning_ability": "advanced"
|
| 187 |
+
})
|
| 188 |
+
else:
|
| 189 |
+
capabilities.update({
|
| 190 |
+
"max_input_length": 4000,
|
| 191 |
+
"reasoning_ability": "intermediate"
|
| 192 |
+
})
|
| 193 |
+
|
| 194 |
+
return capabilities
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class HuggingFaceAdapter(ModelAdapter):
|
| 198 |
+
"""
|
| 199 |
+
Adapter for Hugging Face models.
|
| 200 |
+
|
| 201 |
+
This adapter provides an interface to models hosted on Hugging Face's model hub.
|
| 202 |
+
Note: This is a placeholder implementation. In a real application, you would
|
| 203 |
+
need to implement the actual API calls to Hugging Face.
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
def __init__(self, model_id: str, api_key: Optional[str] = None):
|
| 207 |
+
"""
|
| 208 |
+
Initialize a new HuggingFaceAdapter.
|
| 209 |
+
|
| 210 |
+
Args:
|
| 211 |
+
model_id: The ID of the Hugging Face model to use.
|
| 212 |
+
api_key: Hugging Face API key. If None, will attempt to use environment variable.
|
| 213 |
+
"""
|
| 214 |
+
self.model_id = model_id
|
| 215 |
+
self.api_key = api_key
|
| 216 |
+
# In a real implementation, you would initialize the Hugging Face client here
|
| 217 |
+
|
| 218 |
+
async def process(self, input_data: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
|
| 219 |
+
"""
|
| 220 |
+
Process an input with a Hugging Face model.
|
| 221 |
+
|
| 222 |
+
Args:
|
| 223 |
+
input_data: The input data to process.
|
| 224 |
+
context: Optional context information.
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
The model's output.
|
| 228 |
+
"""
|
| 229 |
+
# This is a placeholder. In a real implementation, you would:
|
| 230 |
+
# 1. Format the input and context for the Hugging Face API
|
| 231 |
+
# 2. Make the API call
|
| 232 |
+
# 3. Process and return the response
|
| 233 |
+
|
| 234 |
+
# Placeholder response
|
| 235 |
+
return {
|
| 236 |
+
"text": f"This is a placeholder response from Hugging Face model {self.model_id}. In a real implementation, this would be the actual model output.",
|
| 237 |
+
"model": self.model_id
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
def get_capabilities(self) -> Dict[str, Any]:
|
| 241 |
+
"""
|
| 242 |
+
Get the capabilities of this Hugging Face model.
|
| 243 |
+
|
| 244 |
+
Returns:
|
| 245 |
+
Dictionary describing the model's capabilities.
|
| 246 |
+
"""
|
| 247 |
+
# This would be determined dynamically in a real implementation
|
| 248 |
+
return {
|
| 249 |
+
"text_generation": True,
|
| 250 |
+
"question_answering": True,
|
| 251 |
+
"content_evaluation": True,
|
| 252 |
+
"max_input_length": 2000,
|
| 253 |
+
"supports_streaming": False
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
class ModelRegistry:
|
| 258 |
+
"""
|
| 259 |
+
Registry for model adapters.
|
| 260 |
+
|
| 261 |
+
This class manages the available model adapters and provides methods
|
| 262 |
+
for registering, retrieving, and listing them.
|
| 263 |
+
"""
|
| 264 |
+
|
| 265 |
+
def __init__(self):
|
| 266 |
+
"""Initialize a new ModelRegistry."""
|
| 267 |
+
self.adapters: Dict[str, ModelAdapter] = {}
|
| 268 |
+
|
| 269 |
+
def register_adapter(self, name: str, adapter: ModelAdapter) -> None:
|
| 270 |
+
"""
|
| 271 |
+
Register a model adapter.
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
name: Name to register the adapter under.
|
| 275 |
+
adapter: The ModelAdapter instance to register.
|
| 276 |
+
"""
|
| 277 |
+
self.adapters[name] = adapter
|
| 278 |
+
|
| 279 |
+
def get_adapter(self, name: str) -> Optional[ModelAdapter]:
|
| 280 |
+
"""
|
| 281 |
+
Get a registered adapter by name.
|
| 282 |
+
|
| 283 |
+
Args:
|
| 284 |
+
name: Name of the adapter to retrieve.
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
The ModelAdapter if found, None otherwise.
|
| 288 |
+
"""
|
| 289 |
+
return self.adapters.get(name)
|
| 290 |
+
|
| 291 |
+
def list_adapters(self) -> List[str]:
|
| 292 |
+
"""
|
| 293 |
+
List all registered adapter names.
|
| 294 |
+
|
| 295 |
+
Returns:
|
| 296 |
+
List of adapter names.
|
| 297 |
+
"""
|
| 298 |
+
return list(self.adapters.keys())
|
| 299 |
+
|
| 300 |
+
def get_adapter_capabilities(self, name: str) -> Optional[Dict[str, Any]]:
|
| 301 |
+
"""
|
| 302 |
+
Get the capabilities of a registered adapter.
|
| 303 |
+
|
| 304 |
+
Args:
|
| 305 |
+
name: Name of the adapter.
|
| 306 |
+
|
| 307 |
+
Returns:
|
| 308 |
+
Dictionary of capabilities if the adapter exists, None otherwise.
|
| 309 |
+
"""
|
| 310 |
+
adapter = self.get_adapter(name)
|
| 311 |
+
if adapter:
|
| 312 |
+
return adapter.get_capabilities()
|
| 313 |
+
return None
|
src/mcp_core/persistence.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Core - State Persistence Layer
|
| 3 |
+
|
| 4 |
+
This module provides functionality for persisting context state across sessions,
|
| 5 |
+
allowing for long-term storage and retrieval of context data.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import sqlite3
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import Dict, List, Optional, Any, Union
|
| 13 |
+
from abc import ABC, abstractmethod
|
| 14 |
+
|
| 15 |
+
from .context import Context
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class PersistenceProvider(ABC):
|
| 19 |
+
"""
|
| 20 |
+
Abstract base class for persistence providers.
|
| 21 |
+
|
| 22 |
+
Persistence providers handle the storage and retrieval of context data,
|
| 23 |
+
allowing contexts to be persisted across sessions.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
@abstractmethod
|
| 27 |
+
async def save_context(self, context: Context) -> bool:
|
| 28 |
+
"""
|
| 29 |
+
Save a context to persistent storage.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
context: The Context object to save.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
True if the save was successful, False otherwise.
|
| 36 |
+
"""
|
| 37 |
+
pass
|
| 38 |
+
|
| 39 |
+
@abstractmethod
|
| 40 |
+
async def load_context(self, context_id: str) -> Optional[Context]:
|
| 41 |
+
"""
|
| 42 |
+
Load a context from persistent storage.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
context_id: The ID of the context to load.
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
The loaded Context object if found, None otherwise.
|
| 49 |
+
"""
|
| 50 |
+
pass
|
| 51 |
+
|
| 52 |
+
@abstractmethod
|
| 53 |
+
async def delete_context(self, context_id: str) -> bool:
|
| 54 |
+
"""
|
| 55 |
+
Delete a context from persistent storage.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
context_id: The ID of the context to delete.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
True if the deletion was successful, False otherwise.
|
| 62 |
+
"""
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
@abstractmethod
|
| 66 |
+
async def list_contexts(self, filter_criteria: Optional[Dict[str, Any]] = None) -> List[str]:
|
| 67 |
+
"""
|
| 68 |
+
List context IDs in persistent storage, optionally filtered.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
filter_criteria: Optional criteria to filter contexts by.
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
List of context IDs matching the filter criteria.
|
| 75 |
+
"""
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class FileSystemProvider(PersistenceProvider):
|
| 80 |
+
"""
|
| 81 |
+
File system-based persistence provider.
|
| 82 |
+
|
| 83 |
+
This provider stores contexts as JSON files in the file system.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
def __init__(self, storage_dir: str = "./storage/contexts"):
|
| 87 |
+
"""
|
| 88 |
+
Initialize a new FileSystemProvider.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
storage_dir: Directory to store context files in.
|
| 92 |
+
"""
|
| 93 |
+
self.storage_dir = storage_dir
|
| 94 |
+
os.makedirs(storage_dir, exist_ok=True)
|
| 95 |
+
|
| 96 |
+
def _get_context_path(self, context_id: str) -> str:
|
| 97 |
+
"""
|
| 98 |
+
Get the file path for a context.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
context_id: The ID of the context.
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
The file path for the context.
|
| 105 |
+
"""
|
| 106 |
+
return os.path.join(self.storage_dir, f"{context_id}.json")
|
| 107 |
+
|
| 108 |
+
async def save_context(self, context: Context) -> bool:
|
| 109 |
+
"""
|
| 110 |
+
Save a context to a JSON file.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
context: The Context object to save.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
True if the save was successful, False otherwise.
|
| 117 |
+
"""
|
| 118 |
+
try:
|
| 119 |
+
with open(self._get_context_path(context.context_id), 'w') as f:
|
| 120 |
+
json.dump(context.to_dict(), f, indent=2)
|
| 121 |
+
return True
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"Error saving context: {e}")
|
| 124 |
+
return False
|
| 125 |
+
|
| 126 |
+
async def load_context(self, context_id: str) -> Optional[Context]:
|
| 127 |
+
"""
|
| 128 |
+
Load a context from a JSON file.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
context_id: The ID of the context to load.
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
The loaded Context object if found, None otherwise.
|
| 135 |
+
"""
|
| 136 |
+
try:
|
| 137 |
+
path = self._get_context_path(context_id)
|
| 138 |
+
if not os.path.exists(path):
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
with open(path, 'r') as f:
|
| 142 |
+
data = json.load(f)
|
| 143 |
+
|
| 144 |
+
return Context.from_dict(data)
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"Error loading context: {e}")
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
async def delete_context(self, context_id: str) -> bool:
|
| 150 |
+
"""
|
| 151 |
+
Delete a context JSON file.
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
context_id: The ID of the context to delete.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
True if the deletion was successful, False otherwise.
|
| 158 |
+
"""
|
| 159 |
+
try:
|
| 160 |
+
path = self._get_context_path(context_id)
|
| 161 |
+
if os.path.exists(path):
|
| 162 |
+
os.remove(path)
|
| 163 |
+
return True
|
| 164 |
+
return False
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"Error deleting context: {e}")
|
| 167 |
+
return False
|
| 168 |
+
|
| 169 |
+
async def list_contexts(self, filter_criteria: Optional[Dict[str, Any]] = None) -> List[str]:
|
| 170 |
+
"""
|
| 171 |
+
List context IDs in the storage directory, optionally filtered.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
filter_criteria: Optional criteria to filter contexts by.
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
List of context IDs matching the filter criteria.
|
| 178 |
+
"""
|
| 179 |
+
try:
|
| 180 |
+
context_ids = []
|
| 181 |
+
for filename in os.listdir(self.storage_dir):
|
| 182 |
+
if filename.endswith('.json'):
|
| 183 |
+
context_id = filename[:-5] # Remove .json extension
|
| 184 |
+
|
| 185 |
+
# Apply filtering if criteria provided
|
| 186 |
+
if filter_criteria:
|
| 187 |
+
try:
|
| 188 |
+
with open(self._get_context_path(context_id), 'r') as f:
|
| 189 |
+
data = json.load(f)
|
| 190 |
+
|
| 191 |
+
# Check if context matches all filter criteria
|
| 192 |
+
matches = True
|
| 193 |
+
for key, value in filter_criteria.items():
|
| 194 |
+
# Handle nested keys with dot notation
|
| 195 |
+
if '.' in key:
|
| 196 |
+
parts = key.split('.')
|
| 197 |
+
current = data
|
| 198 |
+
for part in parts:
|
| 199 |
+
if part not in current:
|
| 200 |
+
matches = False
|
| 201 |
+
break
|
| 202 |
+
current = current[part]
|
| 203 |
+
if matches and current != value:
|
| 204 |
+
matches = False
|
| 205 |
+
elif key not in data or data[key] != value:
|
| 206 |
+
matches = False
|
| 207 |
+
|
| 208 |
+
if matches:
|
| 209 |
+
context_ids.append(context_id)
|
| 210 |
+
except:
|
| 211 |
+
# Skip contexts that can't be loaded or don't match
|
| 212 |
+
continue
|
| 213 |
+
else:
|
| 214 |
+
context_ids.append(context_id)
|
| 215 |
+
|
| 216 |
+
return context_ids
|
| 217 |
+
except Exception as e:
|
| 218 |
+
print(f"Error listing contexts: {e}")
|
| 219 |
+
return []
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
class SQLiteProvider(PersistenceProvider):
|
| 223 |
+
"""
|
| 224 |
+
SQLite-based persistence provider.
|
| 225 |
+
|
| 226 |
+
This provider stores contexts in an SQLite database.
|
| 227 |
+
"""
|
| 228 |
+
|
| 229 |
+
def __init__(self, db_path: str = "./storage/contexts.db"):
|
| 230 |
+
"""
|
| 231 |
+
Initialize a new SQLiteProvider.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
db_path: Path to the SQLite database file.
|
| 235 |
+
"""
|
| 236 |
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
| 237 |
+
self.db_path = db_path
|
| 238 |
+
self._init_db()
|
| 239 |
+
|
| 240 |
+
def _init_db(self):
|
| 241 |
+
"""Initialize the database schema if it doesn't exist."""
|
| 242 |
+
conn = sqlite3.connect(self.db_path)
|
| 243 |
+
cursor = conn.cursor()
|
| 244 |
+
|
| 245 |
+
# Create contexts table
|
| 246 |
+
cursor.execute('''
|
| 247 |
+
CREATE TABLE IF NOT EXISTS contexts (
|
| 248 |
+
context_id TEXT PRIMARY KEY,
|
| 249 |
+
metadata TEXT,
|
| 250 |
+
state TEXT,
|
| 251 |
+
history TEXT,
|
| 252 |
+
created_at TEXT,
|
| 253 |
+
updated_at TEXT
|
| 254 |
+
)
|
| 255 |
+
''')
|
| 256 |
+
|
| 257 |
+
conn.commit()
|
| 258 |
+
conn.close()
|
| 259 |
+
|
| 260 |
+
async def save_context(self, context: Context) -> bool:
|
| 261 |
+
"""
|
| 262 |
+
Save a context to the SQLite database.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
context: The Context object to save.
|
| 266 |
+
|
| 267 |
+
Returns:
|
| 268 |
+
True if the save was successful, False otherwise.
|
| 269 |
+
"""
|
| 270 |
+
try:
|
| 271 |
+
conn = sqlite3.connect(self.db_path)
|
| 272 |
+
cursor = conn.cursor()
|
| 273 |
+
|
| 274 |
+
# Convert complex objects to JSON strings
|
| 275 |
+
context_data = context.to_dict()
|
| 276 |
+
metadata = json.dumps(context_data.get("metadata", {}))
|
| 277 |
+
state = json.dumps(context_data.get("state", {}))
|
| 278 |
+
history = json.dumps(context_data.get("history", []))
|
| 279 |
+
|
| 280 |
+
# Check if context already exists
|
| 281 |
+
cursor.execute("SELECT 1 FROM contexts WHERE context_id = ?", (context.context_id,))
|
| 282 |
+
exists = cursor.fetchone() is not None
|
| 283 |
+
|
| 284 |
+
if exists:
|
| 285 |
+
# Update existing context
|
| 286 |
+
cursor.execute('''
|
| 287 |
+
UPDATE contexts
|
| 288 |
+
SET metadata = ?, state = ?, history = ?, updated_at = ?
|
| 289 |
+
WHERE context_id = ?
|
| 290 |
+
''', (metadata, state, history, context.updated_at.isoformat(), context.context_id))
|
| 291 |
+
else:
|
| 292 |
+
# Insert new context
|
| 293 |
+
cursor.execute('''
|
| 294 |
+
INSERT INTO contexts (context_id, metadata, state, history, created_at, updated_at)
|
| 295 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 296 |
+
''', (
|
| 297 |
+
context.context_id,
|
| 298 |
+
metadata,
|
| 299 |
+
state,
|
| 300 |
+
history,
|
| 301 |
+
context.created_at.isoformat(),
|
| 302 |
+
context.updated_at.isoformat()
|
| 303 |
+
))
|
| 304 |
+
|
| 305 |
+
conn.commit()
|
| 306 |
+
conn.close()
|
| 307 |
+
return True
|
| 308 |
+
except Exception as e:
|
| 309 |
+
print(f"Error saving context to SQLite: {e}")
|
| 310 |
+
return False
|
| 311 |
+
|
| 312 |
+
async def load_context(self, context_id: str) -> Optional[Context]:
|
| 313 |
+
"""
|
| 314 |
+
Load a context from the SQLite database.
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
context_id: The ID of the context to load.
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
The loaded Context object if found, None otherwise.
|
| 321 |
+
"""
|
| 322 |
+
try:
|
| 323 |
+
conn = sqlite3.connect(self.db_path)
|
| 324 |
+
cursor = conn.cursor()
|
| 325 |
+
|
| 326 |
+
cursor.execute('''
|
| 327 |
+
SELECT context_id, metadata, state, history, created_at, updated_at
|
| 328 |
+
FROM contexts
|
| 329 |
+
WHERE context_id = ?
|
| 330 |
+
''', (context_id,))
|
| 331 |
+
|
| 332 |
+
row = cursor.fetchone()
|
| 333 |
+
conn.close()
|
| 334 |
+
|
| 335 |
+
if not row:
|
| 336 |
+
return None
|
| 337 |
+
|
| 338 |
+
# Reconstruct context from database row
|
| 339 |
+
context_data = {
|
| 340 |
+
"context_id": row[0],
|
| 341 |
+
"metadata": json.loads(row[1]),
|
| 342 |
+
"state": json.loads(row[2]),
|
| 343 |
+
"history": json.loads(row[3]),
|
| 344 |
+
"created_at": row[4],
|
| 345 |
+
"updated_at": row[5]
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
return Context.from_dict(context_data)
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f"Error loading context from SQLite: {e}")
|
| 351 |
+
return None
|
| 352 |
+
|
| 353 |
+
async def delete_context(self, context_id: str) -> bool:
|
| 354 |
+
"""
|
| 355 |
+
Delete a context from the SQLite database.
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
context_id: The ID of the context to delete.
|
| 359 |
+
|
| 360 |
+
Returns:
|
| 361 |
+
True if the deletion was successful, False otherwise.
|
| 362 |
+
"""
|
| 363 |
+
try:
|
| 364 |
+
conn = sqlite3.connect(self.db_path)
|
| 365 |
+
cursor = conn.cursor()
|
| 366 |
+
|
| 367 |
+
cursor.execute("DELETE FROM contexts WHERE context_id = ?", (context_id,))
|
| 368 |
+
deleted = cursor.rowcount > 0
|
| 369 |
+
|
| 370 |
+
conn.commit()
|
| 371 |
+
conn.close()
|
| 372 |
+
|
| 373 |
+
return deleted
|
| 374 |
+
except Exception as e:
|
| 375 |
+
print(f"Error deleting context from SQLite: {e}")
|
| 376 |
+
return False
|
| 377 |
+
|
| 378 |
+
async def list_contexts(self, filter_criteria: Optional[Dict[str, Any]] = None) -> List[str]:
|
| 379 |
+
"""
|
| 380 |
+
List context IDs in the SQLite database, optionally filtered.
|
| 381 |
+
|
| 382 |
+
Args:
|
| 383 |
+
filter_criteria: Optional criteria to filter contexts by.
|
| 384 |
+
|
| 385 |
+
Returns:
|
| 386 |
+
List of context IDs matching the filter criteria.
|
| 387 |
+
"""
|
| 388 |
+
try:
|
| 389 |
+
conn = sqlite3.connect(self.db_path)
|
| 390 |
+
cursor = conn.cursor()
|
| 391 |
+
|
| 392 |
+
if not filter_criteria:
|
| 393 |
+
# Simple case: no filtering
|
| 394 |
+
cursor.execute("SELECT context_id FROM contexts")
|
| 395 |
+
context_ids = [row[0] for row in cursor.fetchall()]
|
| 396 |
+
else:
|
| 397 |
+
# Complex case: need to load and filter each context
|
| 398 |
+
cursor.execute("SELECT context_id, metadata, state FROM contexts")
|
| 399 |
+
rows = cursor.fetchall()
|
| 400 |
+
|
| 401 |
+
context_ids = []
|
| 402 |
+
for row in rows:
|
| 403 |
+
context_id = row[0]
|
| 404 |
+
metadata = json.loads(row[1])
|
| 405 |
+
state = json.loads(row[2])
|
| 406 |
+
|
| 407 |
+
# Check if context matches all filter criteria
|
| 408 |
+
matches = True
|
| 409 |
+
for key, value in filter_criteria.items():
|
| 410 |
+
if key.startswith("metadata."):
|
| 411 |
+
# Filter on metadata field
|
| 412 |
+
field = key[9:] # Remove "metadata." prefix
|
| 413 |
+
if field not in metadata or metadata[field] != value:
|
| 414 |
+
matches = False
|
| 415 |
+
break
|
| 416 |
+
elif key.startswith("state."):
|
| 417 |
+
# Filter on state field
|
| 418 |
+
field = key[6:] # Remove "state." prefix
|
| 419 |
+
if field not in state or state[field] != value:
|
| 420 |
+
matches = False
|
| 421 |
+
break
|
| 422 |
+
|
| 423 |
+
if matches:
|
| 424 |
+
context_ids.append(context_id)
|
| 425 |
+
|
| 426 |
+
conn.close()
|
| 427 |
+
return context_ids
|
| 428 |
+
except Exception as e:
|
| 429 |
+
print(f"Error listing contexts from SQLite: {e}")
|
| 430 |
+
return []
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
class PersistenceManager:
|
| 434 |
+
"""
|
| 435 |
+
Manages context persistence operations.
|
| 436 |
+
|
| 437 |
+
This class provides a unified interface for working with different
|
| 438 |
+
persistence providers.
|
| 439 |
+
"""
|
| 440 |
+
|
| 441 |
+
def __init__(self, provider: PersistenceProvider):
|
| 442 |
+
"""
|
| 443 |
+
Initialize a new PersistenceManager.
|
| 444 |
+
|
| 445 |
+
Args:
|
| 446 |
+
provider: The persistence provider to use.
|
| 447 |
+
"""
|
| 448 |
+
self.provider = provider
|
| 449 |
+
|
| 450 |
+
async def save_context(self, context: Context) -> bool:
|
| 451 |
+
"""
|
| 452 |
+
Save a context using the configured provider.
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
context: The Context object to save.
|
| 456 |
+
|
| 457 |
+
Returns:
|
| 458 |
+
True if the save was successful, False otherwise.
|
| 459 |
+
"""
|
| 460 |
+
return await self.provider.save_context(context)
|
| 461 |
+
|
| 462 |
+
async def load_context(self, context_id: str) -> Optional[Context]:
|
| 463 |
+
"""
|
| 464 |
+
Load a context using the configured provider.
|
| 465 |
+
|
| 466 |
+
Args:
|
| 467 |
+
context_id: The ID of the context to load.
|
| 468 |
+
|
| 469 |
+
Returns:
|
| 470 |
+
The loaded Context object if found, None otherwise.
|
| 471 |
+
"""
|
| 472 |
+
return await self.provider.load_context(context_id)
|
| 473 |
+
|
| 474 |
+
async def delete_context(self, context_id: str) -> bool:
|
| 475 |
+
"""
|
| 476 |
+
Delete a context using the configured provider.
|
| 477 |
+
|
| 478 |
+
Args:
|
| 479 |
+
context_id: The ID of the context to delete.
|
| 480 |
+
|
| 481 |
+
Returns:
|
| 482 |
+
True if the deletion was successful, False otherwise.
|
| 483 |
+
"""
|
| 484 |
+
return await self.provider.delete_context(context_id)
|
| 485 |
+
|
| 486 |
+
async def list_contexts(self, filter_criteria: Optional[Dict[str, Any]] = None) -> List[str]:
|
| 487 |
+
"""
|
| 488 |
+
List context IDs using the configured provider, optionally filtered.
|
| 489 |
+
|
| 490 |
+
Args:
|
| 491 |
+
filter_criteria: Optional criteria to filter contexts by.
|
| 492 |
+
|
| 493 |
+
Returns:
|
| 494 |
+
List of context IDs matching the filter criteria.
|
| 495 |
+
"""
|
| 496 |
+
return await self.provider.list_contexts(filter_criteria)
|
| 497 |
+
|
| 498 |
+
def change_provider(self, provider: PersistenceProvider) -> None:
|
| 499 |
+
"""
|
| 500 |
+
Change the persistence provider.
|
| 501 |
+
|
| 502 |
+
Args:
|
| 503 |
+
provider: The new persistence provider to use.
|
| 504 |
+
"""
|
| 505 |
+
self.provider = provider
|
src/mcp_core/processor.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Core - Interaction Processor
|
| 3 |
+
|
| 4 |
+
This module provides the core functionality for processing interactions
|
| 5 |
+
between users and models through the Model Context Protocol.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import Dict, Any, Optional, List, Union
|
| 9 |
+
import asyncio
|
| 10 |
+
|
| 11 |
+
from .context import Context, ContextManager
|
| 12 |
+
from .model_adapters import ModelAdapter, ModelRegistry
|
| 13 |
+
from .protocol import MCPRequest, MCPResponse, InteractionType
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class InteractionProcessor:
|
| 17 |
+
"""
|
| 18 |
+
Processes interactions between users and models.
|
| 19 |
+
|
| 20 |
+
This class handles the flow of interactions through the MCP system,
|
| 21 |
+
managing context, routing to appropriate models, and formatting responses.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
context_manager: ContextManager,
|
| 27 |
+
model_registry: ModelRegistry
|
| 28 |
+
):
|
| 29 |
+
"""
|
| 30 |
+
Initialize a new InteractionProcessor.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
context_manager: The ContextManager to use for context operations.
|
| 34 |
+
model_registry: The ModelRegistry to use for model operations.
|
| 35 |
+
"""
|
| 36 |
+
self.context_manager = context_manager
|
| 37 |
+
self.model_registry = model_registry
|
| 38 |
+
|
| 39 |
+
async def process_interaction(
|
| 40 |
+
self,
|
| 41 |
+
request: MCPRequest,
|
| 42 |
+
model_name: str
|
| 43 |
+
) -> MCPResponse:
|
| 44 |
+
"""
|
| 45 |
+
Process an interaction request.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
request: The MCPRequest to process.
|
| 49 |
+
model_name: Name of the model adapter to use.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
An MCPResponse containing the model's response.
|
| 53 |
+
|
| 54 |
+
Raises:
|
| 55 |
+
ValueError: If the context ID is invalid or the model is not found.
|
| 56 |
+
"""
|
| 57 |
+
# Get or create context
|
| 58 |
+
context_id = request.context_id
|
| 59 |
+
context = None
|
| 60 |
+
|
| 61 |
+
if context_id:
|
| 62 |
+
# Use existing context
|
| 63 |
+
context = self.context_manager.get_context(context_id)
|
| 64 |
+
if not context:
|
| 65 |
+
raise ValueError(f"Context with ID {context_id} not found")
|
| 66 |
+
else:
|
| 67 |
+
# Create new context
|
| 68 |
+
context = self.context_manager.create_context(
|
| 69 |
+
metadata={"interaction_type": request.interaction_type}
|
| 70 |
+
)
|
| 71 |
+
context_id = context.context_id
|
| 72 |
+
|
| 73 |
+
# Get model adapter
|
| 74 |
+
model_adapter = self.model_registry.get_adapter(model_name)
|
| 75 |
+
if not model_adapter:
|
| 76 |
+
raise ValueError(f"Model adapter '{model_name}' not found")
|
| 77 |
+
|
| 78 |
+
# Process the request with the model
|
| 79 |
+
model_input = {
|
| 80 |
+
"text": request.content.get("text", ""),
|
| 81 |
+
"interaction_type": request.interaction_type,
|
| 82 |
+
"format": request.format,
|
| 83 |
+
**request.metadata
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# Include context state in model input
|
| 87 |
+
model_context = {
|
| 88 |
+
"state": context.state,
|
| 89 |
+
"history": context.get_history(5) # Last 5 interactions
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# Process with model
|
| 93 |
+
model_output = await model_adapter.process(model_input, model_context)
|
| 94 |
+
|
| 95 |
+
# Update context with this interaction
|
| 96 |
+
context.add_interaction(request.dict(), model_output)
|
| 97 |
+
|
| 98 |
+
# Extract educational metadata if available
|
| 99 |
+
educational_metadata = model_output.get("educational_metadata", {})
|
| 100 |
+
if educational_metadata:
|
| 101 |
+
# Update context state with educational information
|
| 102 |
+
context.update_state({
|
| 103 |
+
"educational": {
|
| 104 |
+
"last_topics": educational_metadata.get("topics", []),
|
| 105 |
+
"complexity_level": educational_metadata.get("complexity_level", ""),
|
| 106 |
+
"suggested_follow_ups": educational_metadata.get("suggested_follow_ups", [])
|
| 107 |
+
}
|
| 108 |
+
})
|
| 109 |
+
|
| 110 |
+
# Create response
|
| 111 |
+
response = MCPResponse(
|
| 112 |
+
context_id=context_id,
|
| 113 |
+
interaction_type=request.interaction_type,
|
| 114 |
+
content={
|
| 115 |
+
"text": model_output.get("text", ""),
|
| 116 |
+
"educational_metadata": educational_metadata
|
| 117 |
+
},
|
| 118 |
+
format=request.format,
|
| 119 |
+
metadata=request.metadata,
|
| 120 |
+
model_info={
|
| 121 |
+
"model_id": model_output.get("model_id", model_name),
|
| 122 |
+
"confidence": model_output.get("confidence", 1.0)
|
| 123 |
+
}
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
return response
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class BatchProcessor:
|
| 130 |
+
"""
|
| 131 |
+
Processes batches of interactions.
|
| 132 |
+
|
| 133 |
+
This class provides functionality for processing multiple interactions
|
| 134 |
+
in parallel or sequence.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
def __init__(self, interaction_processor: InteractionProcessor):
|
| 138 |
+
"""
|
| 139 |
+
Initialize a new BatchProcessor.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
interaction_processor: The InteractionProcessor to use.
|
| 143 |
+
"""
|
| 144 |
+
self.interaction_processor = interaction_processor
|
| 145 |
+
|
| 146 |
+
async def process_batch(
|
| 147 |
+
self,
|
| 148 |
+
requests: List[Dict[str, Any]],
|
| 149 |
+
model_name: str,
|
| 150 |
+
parallel: bool = True
|
| 151 |
+
) -> List[Dict[str, Any]]:
|
| 152 |
+
"""
|
| 153 |
+
Process a batch of interaction requests.
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
requests: List of request dictionaries.
|
| 157 |
+
model_name: Name of the model adapter to use.
|
| 158 |
+
parallel: Whether to process requests in parallel.
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
List of response dictionaries.
|
| 162 |
+
"""
|
| 163 |
+
if parallel:
|
| 164 |
+
# Process requests in parallel
|
| 165 |
+
tasks = [
|
| 166 |
+
self.interaction_processor.process_interaction(
|
| 167 |
+
MCPRequest(**request),
|
| 168 |
+
model_name
|
| 169 |
+
)
|
| 170 |
+
for request in requests
|
| 171 |
+
]
|
| 172 |
+
responses = await asyncio.gather(*tasks)
|
| 173 |
+
else:
|
| 174 |
+
# Process requests sequentially
|
| 175 |
+
responses = []
|
| 176 |
+
for request in requests:
|
| 177 |
+
response = await self.interaction_processor.process_interaction(
|
| 178 |
+
MCPRequest(**request),
|
| 179 |
+
model_name
|
| 180 |
+
)
|
| 181 |
+
responses.append(response)
|
| 182 |
+
|
| 183 |
+
# Convert responses to dictionaries
|
| 184 |
+
return [response.dict() for response in responses]
|
src/mcp_core/protocol.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from typing import Dict, List, Any, Optional, Union, Literal
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class InteractionType(str, Enum):
|
| 7 |
+
TEXT = "text"
|
| 8 |
+
QUESTION = "question"
|
| 9 |
+
ASSESSMENT = "assessment"
|
| 10 |
+
EXPLANATION = "explanation"
|
| 11 |
+
FEEDBACK = "feedback"
|
| 12 |
+
SUMMARY = "summary"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class ContentFormat(str, Enum):
|
| 16 |
+
TEXT = "text"
|
| 17 |
+
MARKDOWN = "markdown"
|
| 18 |
+
HTML = "html"
|
| 19 |
+
JSON = "json"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class EducationalLevel(str, Enum):
|
| 23 |
+
ELEMENTARY = "elementary"
|
| 24 |
+
MIDDLE_SCHOOL = "middle_school"
|
| 25 |
+
HIGH_SCHOOL = "high_school"
|
| 26 |
+
UNDERGRADUATE = "undergraduate"
|
| 27 |
+
GRADUATE = "graduate"
|
| 28 |
+
PROFESSIONAL = "professional"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class MCPRequest(BaseModel):
|
| 32 |
+
context_id: Optional[str] = Field(None)
|
| 33 |
+
interaction_type: InteractionType = Field(...)
|
| 34 |
+
content: Dict[str, Any] = Field(...)
|
| 35 |
+
format: ContentFormat = Field(ContentFormat.TEXT)
|
| 36 |
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class MCPResponse(BaseModel):
|
| 40 |
+
context_id: str = Field(...)
|
| 41 |
+
interaction_type: InteractionType = Field(...)
|
| 42 |
+
content: Dict[str, Any] = Field(...)
|
| 43 |
+
format: ContentFormat = Field(...)
|
| 44 |
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
| 45 |
+
model_info: Dict[str, Any] = Field(default_factory=dict)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TextRequest(MCPRequest):
|
| 49 |
+
interaction_type: Literal[InteractionType.TEXT] = Field(InteractionType.TEXT)
|
| 50 |
+
content: Dict[str, Any] = Field(...)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class TextResponse(MCPResponse):
|
| 54 |
+
interaction_type: Literal[InteractionType.TEXT] = Field(InteractionType.TEXT)
|
| 55 |
+
content: Dict[str, Any] = Field(...)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class QuestionRequest(MCPRequest):
|
| 59 |
+
interaction_type: Literal[InteractionType.QUESTION] = Field(InteractionType.QUESTION)
|
| 60 |
+
content: Dict[str, Any] = Field(...)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class QuestionResponse(MCPResponse):
|
| 64 |
+
interaction_type: Literal[InteractionType.QUESTION] = Field(InteractionType.QUESTION)
|
| 65 |
+
content: Dict[str, Any] = Field(...)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class AssessmentRequest(MCPRequest):
|
| 69 |
+
interaction_type: Literal[InteractionType.ASSESSMENT] = Field(InteractionType.ASSESSMENT)
|
| 70 |
+
content: Dict[str, Any] = Field(...)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class AssessmentResponse(MCPResponse):
|
| 74 |
+
interaction_type: Literal[InteractionType.ASSESSMENT] = Field(InteractionType.ASSESSMENT)
|
| 75 |
+
content: Dict[str, Any] = Field(...)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class ExplanationRequest(MCPRequest):
|
| 79 |
+
interaction_type: Literal[InteractionType.EXPLANATION] = Field(InteractionType.EXPLANATION)
|
| 80 |
+
content: Dict[str, Any] = Field(...)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class ExplanationResponse(MCPResponse):
|
| 84 |
+
interaction_type: Literal[InteractionType.EXPLANATION] = Field(InteractionType.EXPLANATION)
|
| 85 |
+
content: Dict[str, Any] = Field(...)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class FeedbackRequest(MCPRequest):
|
| 89 |
+
interaction_type: Literal[InteractionType.FEEDBACK] = Field(InteractionType.FEEDBACK)
|
| 90 |
+
content: Dict[str, Any] = Field(...)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class FeedbackResponse(MCPResponse):
|
| 94 |
+
interaction_type: Literal[InteractionType.FEEDBACK] = Field(InteractionType.FEEDBACK)
|
| 95 |
+
content: Dict[str, Any] = Field(...)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class SummaryRequest(MCPRequest):
|
| 99 |
+
interaction_type: Literal[InteractionType.SUMMARY] = Field(InteractionType.SUMMARY)
|
| 100 |
+
content: Dict[str, Any] = Field(...)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class SummaryResponse(MCPResponse):
|
| 104 |
+
interaction_type: Literal[InteractionType.SUMMARY] = Field(InteractionType.SUMMARY)
|
| 105 |
+
content: Dict[str, Any] = Field(...)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class LearningObjective(BaseModel):
|
| 109 |
+
id: str = Field(...)
|
| 110 |
+
description: str = Field(...)
|
| 111 |
+
taxonomy_level: str = Field(...)
|
| 112 |
+
subject_area: str = Field(...)
|
| 113 |
+
prerequisites: List[str] = Field(default_factory=list)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class StudentProfile(BaseModel):
|
| 117 |
+
id: str = Field(...)
|
| 118 |
+
name: str = Field(...)
|
| 119 |
+
educational_level: EducationalLevel = Field(...)
|
| 120 |
+
learning_style: str = Field(None)
|
| 121 |
+
interests: List[str] = Field(default_factory=list)
|
| 122 |
+
strengths: List[str] = Field(default_factory=list)
|
| 123 |
+
areas_for_improvement: List[str] = Field(default_factory=list)
|
| 124 |
+
completed_objectives: List[str] = Field(default_factory=list)
|
| 125 |
+
current_objectives: List[str] = Field(default_factory=list)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class ContentAdaptationRequest(MCPRequest):
|
| 129 |
+
interaction_type: Literal[InteractionType.TEXT] = Field(InteractionType.TEXT)
|
| 130 |
+
content: Dict[str, Any] = Field(...)
|
| 131 |
+
student_profile: StudentProfile = Field(...)
|
| 132 |
+
learning_objectives: List[LearningObjective] = Field(...)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class ContentAdaptationResponse(MCPResponse):
|
| 136 |
+
interaction_type: Literal[InteractionType.TEXT] = Field(InteractionType.TEXT)
|
| 137 |
+
content: Dict[str, Any] = Field(...)
|
| 138 |
+
adaptations_applied: List[str] = Field(...)
|
| 139 |
+
recommended_activities: List[Dict[str, Any]] = Field(default_factory=list)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
MCP_VERSION = "1.0.0"
|
| 143 |
+
MCP_SPEC_URL = "https://example.com/mcp-specification"
|
| 144 |
+
|
| 145 |
+
MCP_CAPABILITIES = {
|
| 146 |
+
"version": MCP_VERSION,
|
| 147 |
+
"supported_interaction_types": [e.value for e in InteractionType],
|
| 148 |
+
"supported_content_formats": [e.value for e in ContentFormat],
|
| 149 |
+
"supported_educational_levels": [e.value for e in EducationalLevel],
|
| 150 |
+
"features": {
|
| 151 |
+
"context_management": True,
|
| 152 |
+
"state_persistence": True,
|
| 153 |
+
"content_adaptation": True,
|
| 154 |
+
"assessment": True,
|
| 155 |
+
"feedback": True,
|
| 156 |
+
"learning_path_generation": True
|
| 157 |
+
}
|
| 158 |
+
}
|
src/static/index.html
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>MCP EdTech Demo</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
padding-top: 20px;
|
| 12 |
+
background-color: #f8f9fa;
|
| 13 |
+
}
|
| 14 |
+
.navbar {
|
| 15 |
+
background-color: #4a6fa5 !important;
|
| 16 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 17 |
+
}
|
| 18 |
+
.navbar-brand {
|
| 19 |
+
font-weight: bold;
|
| 20 |
+
color: white !important;
|
| 21 |
+
}
|
| 22 |
+
.nav-link {
|
| 23 |
+
color: rgba(255,255,255,0.85) !important;
|
| 24 |
+
}
|
| 25 |
+
.nav-link:hover {
|
| 26 |
+
color: white !important;
|
| 27 |
+
}
|
| 28 |
+
.card {
|
| 29 |
+
border-radius: 10px;
|
| 30 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
transition: transform 0.3s;
|
| 33 |
+
}
|
| 34 |
+
.card:hover {
|
| 35 |
+
transform: translateY(-5px);
|
| 36 |
+
}
|
| 37 |
+
.card-header {
|
| 38 |
+
background-color: #4a6fa5;
|
| 39 |
+
color: white;
|
| 40 |
+
font-weight: bold;
|
| 41 |
+
border-radius: 10px 10px 0 0 !important;
|
| 42 |
+
}
|
| 43 |
+
.btn-primary {
|
| 44 |
+
background-color: #4a6fa5;
|
| 45 |
+
border-color: #4a6fa5;
|
| 46 |
+
}
|
| 47 |
+
.btn-primary:hover {
|
| 48 |
+
background-color: #3a5a8f;
|
| 49 |
+
border-color: #3a5a8f;
|
| 50 |
+
}
|
| 51 |
+
.api-url {
|
| 52 |
+
font-family: monospace;
|
| 53 |
+
background-color: #f1f1f1;
|
| 54 |
+
padding: 5px;
|
| 55 |
+
border-radius: 4px;
|
| 56 |
+
}
|
| 57 |
+
.response-area {
|
| 58 |
+
background-color: #f8f9fa;
|
| 59 |
+
border: 1px solid #dee2e6;
|
| 60 |
+
border-radius: 5px;
|
| 61 |
+
padding: 15px;
|
| 62 |
+
max-height: 300px;
|
| 63 |
+
overflow-y: auto;
|
| 64 |
+
font-family: monospace;
|
| 65 |
+
}
|
| 66 |
+
.footer {
|
| 67 |
+
margin-top: 50px;
|
| 68 |
+
padding: 20px 0;
|
| 69 |
+
background-color: #343a40;
|
| 70 |
+
color: white;
|
| 71 |
+
}
|
| 72 |
+
#loading {
|
| 73 |
+
display: none;
|
| 74 |
+
}
|
| 75 |
+
.spinner-border {
|
| 76 |
+
width: 1rem;
|
| 77 |
+
height: 1rem;
|
| 78 |
+
}
|
| 79 |
+
</style>
|
| 80 |
+
</head>
|
| 81 |
+
<body>
|
| 82 |
+
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
| 83 |
+
<div class="container">
|
| 84 |
+
<a class="navbar-brand" href="#">MCP EdTech</a>
|
| 85 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
| 86 |
+
<span class="navbar-toggler-icon"></span>
|
| 87 |
+
</button>
|
| 88 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 89 |
+
<ul class="navbar-nav">
|
| 90 |
+
<li class="nav-item">
|
| 91 |
+
<a class="nav-link active" href="#home">Home</a>
|
| 92 |
+
</li>
|
| 93 |
+
<li class="nav-item">
|
| 94 |
+
<a class="nav-link" href="#api-demo">API Demo</a>
|
| 95 |
+
</li>
|
| 96 |
+
<li class="nav-item">
|
| 97 |
+
<a class="nav-link" href="#documentation">Documentation</a>
|
| 98 |
+
</li>
|
| 99 |
+
</ul>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</nav>
|
| 103 |
+
|
| 104 |
+
<div class="container" id="home">
|
| 105 |
+
<div class="row mb-4">
|
| 106 |
+
<div class="col-12">
|
| 107 |
+
<div class="card">
|
| 108 |
+
<div class="card-header">
|
| 109 |
+
Model Context Protocol (MCP) for EdTech
|
| 110 |
+
</div>
|
| 111 |
+
<div class="card-body">
|
| 112 |
+
<h5 class="card-title">Welcome to the MCP EdTech Demo</h5>
|
| 113 |
+
<p class="card-text">
|
| 114 |
+
This demo showcases the Model Context Protocol (MCP) implementation for educational technology applications.
|
| 115 |
+
MCP provides a standardized way for EdTech applications to interact with various AI models while maintaining
|
| 116 |
+
context and state across interactions.
|
| 117 |
+
</p>
|
| 118 |
+
<p>
|
| 119 |
+
Key features of this implementation:
|
| 120 |
+
</p>
|
| 121 |
+
<ul>
|
| 122 |
+
<li>Context management for stateful conversations</li>
|
| 123 |
+
<li>Student profile management</li>
|
| 124 |
+
<li>Learning progress tracking</li>
|
| 125 |
+
<li>Content adaptation based on student profiles</li>
|
| 126 |
+
<li>Assessment and feedback systems</li>
|
| 127 |
+
</ul>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<div class="row mb-4" id="api-demo">
|
| 134 |
+
<div class="col-12">
|
| 135 |
+
<div class="card">
|
| 136 |
+
<div class="card-header">
|
| 137 |
+
API Demo
|
| 138 |
+
</div>
|
| 139 |
+
<div class="card-body">
|
| 140 |
+
<h5 class="card-title">Try the MCP API</h5>
|
| 141 |
+
|
| 142 |
+
<div class="mb-4">
|
| 143 |
+
<h6>1. Create a Context</h6>
|
| 144 |
+
<p>First, let's create a new context for our interaction:</p>
|
| 145 |
+
<div class="mb-3">
|
| 146 |
+
<button id="createContextBtn" class="btn btn-primary">Create Context</button>
|
| 147 |
+
<span id="loading-context" class="ms-2" style="display: none;">
|
| 148 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 149 |
+
</span>
|
| 150 |
+
</div>
|
| 151 |
+
<div class="response-area" id="contextResponse">
|
| 152 |
+
<!-- Context response will appear here -->
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<div class="mb-4">
|
| 157 |
+
<h6>2. Process an Interaction</h6>
|
| 158 |
+
<p>Now, let's process an interaction using the context we created:</p>
|
| 159 |
+
<div class="mb-3">
|
| 160 |
+
<label for="interactionText" class="form-label">Enter your question or prompt:</label>
|
| 161 |
+
<input type="text" class="form-control" id="interactionText"
|
| 162 |
+
placeholder="e.g., Explain the concept of photosynthesis">
|
| 163 |
+
</div>
|
| 164 |
+
<div class="mb-3">
|
| 165 |
+
<button id="interactBtn" class="btn btn-primary">Process Interaction</button>
|
| 166 |
+
<span id="loading-interaction" class="ms-2" style="display: none;">
|
| 167 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 168 |
+
</span>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="response-area" id="interactionResponse">
|
| 171 |
+
<!-- Interaction response will appear here -->
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<div class="mb-4">
|
| 176 |
+
<h6>3. Create a Student Profile</h6>
|
| 177 |
+
<p>Let's create a student profile:</p>
|
| 178 |
+
<div class="mb-3">
|
| 179 |
+
<label for="studentName" class="form-label">Student Name:</label>
|
| 180 |
+
<input type="text" class="form-control" id="studentName" placeholder="John Doe">
|
| 181 |
+
</div>
|
| 182 |
+
<div class="mb-3">
|
| 183 |
+
<label for="educationalLevel" class="form-label">Educational Level:</label>
|
| 184 |
+
<select class="form-select" id="educationalLevel">
|
| 185 |
+
<option value="elementary">Elementary</option>
|
| 186 |
+
<option value="middle_school">Middle School</option>
|
| 187 |
+
<option value="high_school">High School</option>
|
| 188 |
+
<option value="undergraduate">Undergraduate</option>
|
| 189 |
+
<option value="graduate">Graduate</option>
|
| 190 |
+
<option value="professional">Professional</option>
|
| 191 |
+
</select>
|
| 192 |
+
</div>
|
| 193 |
+
<div class="mb-3">
|
| 194 |
+
<button id="createStudentBtn" class="btn btn-primary">Create Student Profile</button>
|
| 195 |
+
<span id="loading-student" class="ms-2" style="display: none;">
|
| 196 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 197 |
+
</span>
|
| 198 |
+
</div>
|
| 199 |
+
<div class="response-area" id="studentResponse">
|
| 200 |
+
<!-- Student profile response will appear here -->
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<div class="mb-4">
|
| 205 |
+
<h6>4. Adapt Content for Student</h6>
|
| 206 |
+
<p>Now, let's adapt some content for the student:</p>
|
| 207 |
+
<div class="mb-3">
|
| 208 |
+
<label for="contentText" class="form-label">Content to adapt:</label>
|
| 209 |
+
<textarea class="form-control" id="contentText" rows="3"
|
| 210 |
+
placeholder="The process of photosynthesis involves the conversion of light energy into chemical energy that can be used by plants and other organisms."></textarea>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="mb-3">
|
| 213 |
+
<button id="adaptContentBtn" class="btn btn-primary">Adapt Content</button>
|
| 214 |
+
<span id="loading-content" class="ms-2" style="display: none;">
|
| 215 |
+
<span class="spinner-border spinner-border-sm"></span> Processing...
|
| 216 |
+
</span>
|
| 217 |
+
</div>
|
| 218 |
+
<div class="response-area" id="contentResponse">
|
| 219 |
+
<!-- Content adaptation response will appear here -->
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<div class="row mb-4" id="documentation">
|
| 228 |
+
<div class="col-12">
|
| 229 |
+
<div class="card">
|
| 230 |
+
<div class="card-header">
|
| 231 |
+
API Documentation
|
| 232 |
+
</div>
|
| 233 |
+
<div class="card-body">
|
| 234 |
+
<h5 class="card-title">Available Endpoints</h5>
|
| 235 |
+
|
| 236 |
+
<div class="mb-3">
|
| 237 |
+
<h6>Context Management</h6>
|
| 238 |
+
<ul>
|
| 239 |
+
<li><span class="api-url">POST /api/v1/context/create</span> - Create a new context</li>
|
| 240 |
+
<li><span class="api-url">GET /api/v1/context/{context_id}</span> - Get context information</li>
|
| 241 |
+
<li><span class="api-url">PUT /api/v1/context/{context_id}</span> - Update context</li>
|
| 242 |
+
<li><span class="api-url">DELETE /api/v1/context/{context_id}</span> - Delete context</li>
|
| 243 |
+
</ul>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<div class="mb-3">
|
| 247 |
+
<h6>Interaction</h6>
|
| 248 |
+
<ul>
|
| 249 |
+
<li><span class="api-url">POST /api/v1/interact</span> - Process an interaction</li>
|
| 250 |
+
</ul>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
+
<div class="mb-3">
|
| 254 |
+
<h6>Student Profiles</h6>
|
| 255 |
+
<ul>
|
| 256 |
+
<li><span class="api-url">POST /api/v1/students</span> - Create student profile</li>
|
| 257 |
+
<li><span class="api-url">GET /api/v1/students/{student_id}</span> - Get student information</li>
|
| 258 |
+
<li><span class="api-url">PUT /api/v1/students/{student_id}</span> - Update student information</li>
|
| 259 |
+
</ul>
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<div class="mb-3">
|
| 263 |
+
<h6>Progress Tracking</h6>
|
| 264 |
+
<ul>
|
| 265 |
+
<li><span class="api-url">GET /api/v1/students/{student_id}/progress</span> - Get learning progress</li>
|
| 266 |
+
<li><span class="api-url">POST /api/v1/assessments</span> - Create assessment</li>
|
| 267 |
+
<li><span class="api-url">GET /api/v1/students/{student_id}/assessments</span> - List student assessments</li>
|
| 268 |
+
</ul>
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<div class="mb-3">
|
| 272 |
+
<h6>Content Adaptation</h6>
|
| 273 |
+
<ul>
|
| 274 |
+
<li><span class="api-url">POST /api/v1/content/adapt</span> - Adapt content for a student</li>
|
| 275 |
+
</ul>
|
| 276 |
+
</div>
|
| 277 |
+
|
| 278 |
+
<div class="mb-3">
|
| 279 |
+
<h6>Learning Paths</h6>
|
| 280 |
+
<ul>
|
| 281 |
+
<li><span class="api-url">GET /api/v1/learning-paths/{student_id}</span> - Get personalized learning path</li>
|
| 282 |
+
</ul>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<div class="mb-3">
|
| 286 |
+
<h6>Models</h6>
|
| 287 |
+
<ul>
|
| 288 |
+
<li><span class="api-url">GET /api/v1/models</span> - List available models</li>
|
| 289 |
+
<li><span class="api-url">GET /api/v1/models/{model_name}/capabilities</span> - Get model capabilities</li>
|
| 290 |
+
</ul>
|
| 291 |
+
</div>
|
| 292 |
+
|
| 293 |
+
<p class="mt-4">
|
| 294 |
+
For complete API documentation, visit the <a href="/docs" target="_blank">Swagger UI</a> or
|
| 295 |
+
<a href="/redoc" target="_blank">ReDoc</a> pages.
|
| 296 |
+
</p>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
|
| 303 |
+
<footer class="footer">
|
| 304 |
+
<div class="container">
|
| 305 |
+
<div class="row">
|
| 306 |
+
<div class="col-md-6">
|
| 307 |
+
<h5>MCP EdTech Project</h5>
|
| 308 |
+
<p>An open-source implementation of the Model Context Protocol for educational technology applications.</p>
|
| 309 |
+
</div>
|
| 310 |
+
<div class="col-md-6 text-md-end">
|
| 311 |
+
<p>© 2025 MCP EdTech Project</p>
|
| 312 |
+
<p>Licensed under MIT License</p>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
</footer>
|
| 317 |
+
|
| 318 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
| 319 |
+
<script>
|
| 320 |
+
// Store context and student IDs
|
| 321 |
+
let currentContextId = null;
|
| 322 |
+
let currentStudentId = null;
|
| 323 |
+
|
| 324 |
+
// Function to format JSON responses
|
| 325 |
+
function formatJSON(json) {
|
| 326 |
+
return JSON.stringify(json, null, 2);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// Create Context
|
| 330 |
+
document.getElementById('createContextBtn').addEventListener('click', async () => {
|
| 331 |
+
const loadingElement = document.getElementById('loading-context');
|
| 332 |
+
loadingElement.style.display = 'inline';
|
| 333 |
+
|
| 334 |
+
try {
|
| 335 |
+
const response = await fetch('/api/v1/context/create', {
|
| 336 |
+
method: 'POST',
|
| 337 |
+
headers: {
|
| 338 |
+
'Content-Type': 'application/json'
|
| 339 |
+
},
|
| 340 |
+
body: JSON.stringify({
|
| 341 |
+
metadata: {
|
| 342 |
+
source: 'demo-ui',
|
| 343 |
+
created_by: 'demo-user'
|
| 344 |
+
}
|
| 345 |
+
})
|
| 346 |
+
});
|
| 347 |
+
|
| 348 |
+
const data = await response.json();
|
| 349 |
+
currentContextId = data.context_id;
|
| 350 |
+
|
| 351 |
+
document.getElementById('contextResponse').innerText = formatJSON(data);
|
| 352 |
+
} catch (error) {
|
| 353 |
+
document.getElementById('contextResponse').innerText = `Error: ${error.message}`;
|
| 354 |
+
} finally {
|
| 355 |
+
loadingElement.style.display = 'none';
|
| 356 |
+
}
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
// Process Interaction
|
| 360 |
+
document.getElementById('interactBtn').addEventListener('click', async () => {
|
| 361 |
+
if (!currentContextId) {
|
| 362 |
+
alert('Please create a context first!');
|
| 363 |
+
return;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
const interactionText = document.getElementById('interactionText').value;
|
| 367 |
+
if (!interactionText) {
|
| 368 |
+
alert('Please enter a question or prompt!');
|
| 369 |
+
return;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
const loadingElement = document.getElementById('loading-interaction');
|
| 373 |
+
loadingElement.style.display = 'inline';
|
| 374 |
+
|
| 375 |
+
try {
|
| 376 |
+
const response = await fetch('/api/v1/interact', {
|
| 377 |
+
method: 'POST',
|
| 378 |
+
headers: {
|
| 379 |
+
'Content-Type': 'application/json'
|
| 380 |
+
},
|
| 381 |
+
body: JSON.stringify({
|
| 382 |
+
context_id: currentContextId,
|
| 383 |
+
interaction_type: 'text',
|
| 384 |
+
content: {
|
| 385 |
+
text: interactionText
|
| 386 |
+
},
|
| 387 |
+
format: 'text',
|
| 388 |
+
model_name: 'mock'
|
| 389 |
+
})
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
const data = await response.json();
|
| 393 |
+
document.getElementById('interactionResponse').innerText = formatJSON(data);
|
| 394 |
+
} catch (error) {
|
| 395 |
+
document.getElementById('interactionResponse').innerText = `Error: ${error.message}`;
|
| 396 |
+
} finally {
|
| 397 |
+
loadingElement.style.display = 'none';
|
| 398 |
+
}
|
| 399 |
+
});
|
| 400 |
+
|
| 401 |
+
// Create Student Profile
|
| 402 |
+
document.getElementById('createStudentBtn').addEventListener('click', async () => {
|
| 403 |
+
const studentName = document.getElementById('studentName').value;
|
| 404 |
+
if (!studentName) {
|
| 405 |
+
alert('Please enter a student name!');
|
| 406 |
+
return;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
const educationalLevel = document.getElementById('educationalLevel').value;
|
| 410 |
+
|
| 411 |
+
const loadingElement = document.getElementById('loading-student');
|
| 412 |
+
loadingElement.style.display = 'inline';
|
| 413 |
+
|
| 414 |
+
try {
|
| 415 |
+
const response = await fetch('/api/v1/students', {
|
| 416 |
+
method: 'POST',
|
| 417 |
+
headers: {
|
| 418 |
+
'Content-Type': 'application/json'
|
| 419 |
+
},
|
| 420 |
+
body: JSON.stringify({
|
| 421 |
+
name: studentName,
|
| 422 |
+
educational_level: educationalLevel,
|
| 423 |
+
learning_style: 'visual',
|
| 424 |
+
interests: ['science', 'technology', 'art'],
|
| 425 |
+
strengths: ['problem-solving', 'creativity'],
|
| 426 |
+
areas_for_improvement: ['time-management']
|
| 427 |
+
})
|
| 428 |
+
});
|
| 429 |
+
|
| 430 |
+
const data = await response.json();
|
| 431 |
+
currentStudentId = data.id;
|
| 432 |
+
|
| 433 |
+
document.getElementById('studentResponse').innerText = formatJSON(data);
|
| 434 |
+
} catch (error) {
|
| 435 |
+
document.getElementById('studentResponse').innerText = `Error: ${error.message}`;
|
| 436 |
+
} finally {
|
| 437 |
+
loadingElement.style.display = 'none';
|
| 438 |
+
}
|
| 439 |
+
});
|
| 440 |
+
|
| 441 |
+
// Adapt Content
|
| 442 |
+
document.getElementById('adaptContentBtn').addEventListener('click', async () => {
|
| 443 |
+
if (!currentStudentId) {
|
| 444 |
+
alert('Please create a student profile first!');
|
| 445 |
+
return;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
const contentText = document.getElementById('contentText').value;
|
| 449 |
+
if (!contentText) {
|
| 450 |
+
alert('Please enter content to adapt!');
|
| 451 |
+
return;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
const loadingElement = document.getElementById('loading-content');
|
| 455 |
+
loadingElement.style.display = 'inline';
|
| 456 |
+
|
| 457 |
+
try {
|
| 458 |
+
const response = await fetch('/api/v1/content/adapt', {
|
| 459 |
+
method: 'POST',
|
| 460 |
+
headers: {
|
| 461 |
+
'Content-Type': 'application/json'
|
| 462 |
+
},
|
| 463 |
+
body: JSON.stringify({
|
| 464 |
+
content: {
|
| 465 |
+
text: contentText
|
| 466 |
+
},
|
| 467 |
+
student_id: currentStudentId,
|
| 468 |
+
learning_objectives: ['obj-1', 'obj-2'],
|
| 469 |
+
content_format: 'text'
|
| 470 |
+
})
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
const data = await response.json();
|
| 474 |
+
document.getElementById('contentResponse').innerText = formatJSON(data);
|
| 475 |
+
} catch (error) {
|
| 476 |
+
document.getElementById('contentResponse').innerText = `Error: ${error.message}`;
|
| 477 |
+
} finally {
|
| 478 |
+
loadingElement.style.display = 'none';
|
| 479 |
+
}
|
| 480 |
+
});
|
| 481 |
+
</script>
|
| 482 |
+
</body>
|
| 483 |
+
</html>
|
static/index.html
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>MCP EdTech Demo</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--primary-color: #4a6fa5;
|
| 10 |
+
--secondary-color: #6c757d;
|
| 11 |
+
--accent-color: #28a745;
|
| 12 |
+
--light-bg: #f8f9fa;
|
| 13 |
+
--dark-bg: #343a40;
|
| 14 |
+
--text-color: #333;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
body {
|
| 18 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 19 |
+
line-height: 1.6;
|
| 20 |
+
color: var(--text-color);
|
| 21 |
+
margin: 0;
|
| 22 |
+
padding: 0;
|
| 23 |
+
background-color: #f5f7fa;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.navbar {
|
| 27 |
+
background-color: var(--primary-color);
|
| 28 |
+
color: white;
|
| 29 |
+
padding: 1rem;
|
| 30 |
+
display: flex;
|
| 31 |
+
justify-content: space-between;
|
| 32 |
+
align-items: center;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.navbar-brand {
|
| 36 |
+
font-size: 1.5rem;
|
| 37 |
+
font-weight: bold;
|
| 38 |
+
color: white;
|
| 39 |
+
text-decoration: none;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.navbar-nav {
|
| 43 |
+
display: flex;
|
| 44 |
+
list-style: none;
|
| 45 |
+
margin: 0;
|
| 46 |
+
padding: 0;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.nav-item {
|
| 50 |
+
margin-left: 1.5rem;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.nav-link {
|
| 54 |
+
color: white;
|
| 55 |
+
text-decoration: none;
|
| 56 |
+
transition: color 0.3s;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.nav-link:hover {
|
| 60 |
+
color: #e0e0e0;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.container {
|
| 64 |
+
max-width: 1200px;
|
| 65 |
+
margin: 0 auto;
|
| 66 |
+
padding: 2rem;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.card {
|
| 70 |
+
background-color: white;
|
| 71 |
+
border-radius: 8px;
|
| 72 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 73 |
+
padding: 1.5rem;
|
| 74 |
+
margin-bottom: 2rem;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.card-header {
|
| 78 |
+
background-color: var(--primary-color);
|
| 79 |
+
color: white;
|
| 80 |
+
padding: 1rem;
|
| 81 |
+
border-radius: 8px 8px 0 0;
|
| 82 |
+
margin: -1.5rem -1.5rem 1.5rem -1.5rem;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
h1, h2, h3, h4 {
|
| 86 |
+
color: var(--primary-color);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.btn {
|
| 90 |
+
display: inline-block;
|
| 91 |
+
background-color: var(--primary-color);
|
| 92 |
+
color: white;
|
| 93 |
+
padding: 0.5rem 1rem;
|
| 94 |
+
border: none;
|
| 95 |
+
border-radius: 4px;
|
| 96 |
+
cursor: pointer;
|
| 97 |
+
font-size: 1rem;
|
| 98 |
+
transition: background-color 0.3s;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.btn:hover {
|
| 102 |
+
background-color: #3a5a8a;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.btn-success {
|
| 106 |
+
background-color: var(--accent-color);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.btn-success:hover {
|
| 110 |
+
background-color: #218838;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.form-group {
|
| 114 |
+
margin-bottom: 1rem;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
label {
|
| 118 |
+
display: block;
|
| 119 |
+
margin-bottom: 0.5rem;
|
| 120 |
+
font-weight: 500;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
input, textarea {
|
| 124 |
+
width: 100%;
|
| 125 |
+
padding: 0.5rem;
|
| 126 |
+
border: 1px solid #ddd;
|
| 127 |
+
border-radius: 4px;
|
| 128 |
+
font-size: 1rem;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.response-area {
|
| 132 |
+
background-color: var(--light-bg);
|
| 133 |
+
border: 1px solid #ddd;
|
| 134 |
+
border-radius: 4px;
|
| 135 |
+
padding: 1rem;
|
| 136 |
+
margin-top: 1rem;
|
| 137 |
+
min-height: 100px;
|
| 138 |
+
white-space: pre-wrap;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.endpoints-list {
|
| 142 |
+
list-style-type: none;
|
| 143 |
+
padding: 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.endpoints-list li {
|
| 147 |
+
padding: 0.5rem 0;
|
| 148 |
+
border-bottom: 1px solid #eee;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.context-id {
|
| 152 |
+
font-family: monospace;
|
| 153 |
+
background-color: #f1f1f1;
|
| 154 |
+
padding: 0.2rem 0.4rem;
|
| 155 |
+
border-radius: 4px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.loading {
|
| 159 |
+
display: none;
|
| 160 |
+
text-align: center;
|
| 161 |
+
margin: 1rem 0;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.loading::after {
|
| 165 |
+
content: "...";
|
| 166 |
+
animation: dots 1.5s steps(5, end) infinite;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
@keyframes dots {
|
| 170 |
+
0%, 20% { content: "."; }
|
| 171 |
+
40% { content: ".."; }
|
| 172 |
+
60%, 100% { content: "..."; }
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.error-message {
|
| 176 |
+
color: #dc3545;
|
| 177 |
+
margin-top: 0.5rem;
|
| 178 |
+
display: none;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.success-message {
|
| 182 |
+
color: var(--accent-color);
|
| 183 |
+
margin-top: 0.5rem;
|
| 184 |
+
display: none;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
code {
|
| 188 |
+
font-family: 'Courier New', Courier, monospace;
|
| 189 |
+
background-color: #f5f5f5;
|
| 190 |
+
padding: 0.2rem 0.4rem;
|
| 191 |
+
border-radius: 3px;
|
| 192 |
+
font-size: 0.9em;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
@media (max-width: 768px) {
|
| 196 |
+
.container {
|
| 197 |
+
padding: 1rem;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.navbar {
|
| 201 |
+
flex-direction: column;
|
| 202 |
+
align-items: flex-start;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.navbar-nav {
|
| 206 |
+
margin-top: 1rem;
|
| 207 |
+
flex-direction: column;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.nav-item {
|
| 211 |
+
margin-left: 0;
|
| 212 |
+
margin-top: 0.5rem;
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
</style>
|
| 216 |
+
</head>
|
| 217 |
+
<body>
|
| 218 |
+
<nav class="navbar">
|
| 219 |
+
<a href="#" class="navbar-brand">MCP EdTech</a>
|
| 220 |
+
<ul class="navbar-nav">
|
| 221 |
+
<li class="nav-item"><a href="#" class="nav-link">Home</a></li>
|
| 222 |
+
<li class="nav-item"><a href="#api-demo" class="nav-link">API Demo</a></li>
|
| 223 |
+
<li class="nav-item"><a href="/docs" class="nav-link">Documentation</a></li>
|
| 224 |
+
<li class="nav-item"><a href="#" class="nav-link">Developed by M.Habib Agrebi</a></li>
|
| 225 |
+
</ul>
|
| 226 |
+
</nav>
|
| 227 |
+
|
| 228 |
+
<div class="container">
|
| 229 |
+
<div class="card">
|
| 230 |
+
<h1>Model Context Protocol (MCP) for EdTech</h1>
|
| 231 |
+
|
| 232 |
+
<h2>Welcome to the MCP EdTech Demo</h2>
|
| 233 |
+
<p>This demo showcases the Model Context Protocol (MCP) implementation for educational technology applications. MCP provides a standardized way for EdTech applications to interact with various AI models while maintaining context and state across interactions.</p>
|
| 234 |
+
|
| 235 |
+
<h3>Key features of this implementation:</h3>
|
| 236 |
+
<ul>
|
| 237 |
+
<li>Context management for stateful conversations</li>
|
| 238 |
+
<li>Student profile management</li>
|
| 239 |
+
<li>Learning progress tracking</li>
|
| 240 |
+
<li>Content adaptation based on student profiles</li>
|
| 241 |
+
<li>Assessment and feedback systems</li>
|
| 242 |
+
</ul>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<div id="api-demo" class="card">
|
| 246 |
+
<div class="card-header">
|
| 247 |
+
<h2>API Demo</h2>
|
| 248 |
+
</div>
|
| 249 |
+
|
| 250 |
+
<h3>Try the MCP API</h3>
|
| 251 |
+
<p>1. Create a Context</p>
|
| 252 |
+
<p>First, let's create a new context for our interaction:</p>
|
| 253 |
+
<button id="createContextBtn" class="btn">Create Context</button>
|
| 254 |
+
<div id="contextLoading" class="loading">Creating context</div>
|
| 255 |
+
<div id="contextError" class="error-message"></div>
|
| 256 |
+
<div id="contextSuccess" class="success-message"></div>
|
| 257 |
+
<div id="contextResponse" class="response-area" style="display: none;"></div>
|
| 258 |
+
|
| 259 |
+
<p>2. Process an Interaction</p>
|
| 260 |
+
<p>Now, let's process an interaction using the context we created:</p>
|
| 261 |
+
<div class="form-group">
|
| 262 |
+
<label for="interactionInput">Enter your question or prompt:</label>
|
| 263 |
+
<input type="text" id="interactionInput" placeholder="e.g., Explain the concept of photosynthesis">
|
| 264 |
+
</div>
|
| 265 |
+
<button id="processInteractionBtn" class="btn">Process Interaction</button>
|
| 266 |
+
<div id="interactionLoading" class="loading">Processing interaction</div>
|
| 267 |
+
<div id="interactionError" class="error-message"></div>
|
| 268 |
+
<div id="interactionResponse" class="response-area" style="display: none;"></div>
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<div class="card">
|
| 272 |
+
<h3>Available Endpoints</h3>
|
| 273 |
+
<ul class="endpoints-list">
|
| 274 |
+
<li><strong>Interaction</strong>
|
| 275 |
+
<ul>
|
| 276 |
+
<li>POST /interact - Process an interaction</li>
|
| 277 |
+
</ul>
|
| 278 |
+
</li>
|
| 279 |
+
<li><strong>Student Profiles</strong>
|
| 280 |
+
<ul>
|
| 281 |
+
<li>POST /students - Create student profile</li>
|
| 282 |
+
<li>GET /students/{student_id} - Get student information</li>
|
| 283 |
+
<li>PUT /students/{student_id} - Update student information</li>
|
| 284 |
+
</ul>
|
| 285 |
+
</li>
|
| 286 |
+
<li><strong>Content Adaptation</strong>
|
| 287 |
+
<ul>
|
| 288 |
+
<li>POST /content/adapt - Adapt content for a student</li>
|
| 289 |
+
</ul>
|
| 290 |
+
</li>
|
| 291 |
+
<li><strong>Models</strong>
|
| 292 |
+
<ul>
|
| 293 |
+
<li>GET /models - List available models</li>
|
| 294 |
+
<li>GET /models/{model_name}/capabilities - Get model capabilities</li>
|
| 295 |
+
</ul>
|
| 296 |
+
</li>
|
| 297 |
+
</ul>
|
| 298 |
+
<p>For complete API documentation, visit the <a href="/docs">Swagger UI</a> or <a href="/redoc">ReDoc</a> pages.</p>
|
| 299 |
+
</div>
|
| 300 |
+
</div>
|
| 301 |
+
|
| 302 |
+
<footer style="background-color: var(--primary-color); color: white; text-align: center; padding: 1rem; margin-top: 2rem;">
|
| 303 |
+
<p>Developed by M.Habib Agrebi</p>
|
| 304 |
+
</footer>
|
| 305 |
+
|
| 306 |
+
<script>
|
| 307 |
+
let currentContextId = null;
|
| 308 |
+
|
| 309 |
+
document.getElementById('createContextBtn').addEventListener('click', createContext);
|
| 310 |
+
document.getElementById('processInteractionBtn').addEventListener('click', processInteraction);
|
| 311 |
+
|
| 312 |
+
async function createContext() {
|
| 313 |
+
const contextLoading = document.getElementById('contextLoading');
|
| 314 |
+
const contextError = document.getElementById('contextError');
|
| 315 |
+
const contextSuccess = document.getElementById('contextSuccess');
|
| 316 |
+
const contextResponse = document.getElementById('contextResponse');
|
| 317 |
+
|
| 318 |
+
contextLoading.style.display = 'block';
|
| 319 |
+
contextError.style.display = 'none';
|
| 320 |
+
contextSuccess.style.display = 'none';
|
| 321 |
+
contextResponse.style.display = 'none';
|
| 322 |
+
|
| 323 |
+
try {
|
| 324 |
+
const response = await fetch('/context/create', {
|
| 325 |
+
method: 'POST',
|
| 326 |
+
headers: {
|
| 327 |
+
'Content-Type': 'application/json'
|
| 328 |
+
},
|
| 329 |
+
body: JSON.stringify({
|
| 330 |
+
metadata: {
|
| 331 |
+
source: 'demo-ui',
|
| 332 |
+
created_by: 'demo-user'
|
| 333 |
+
}
|
| 334 |
+
})
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
+
const data = await response.json();
|
| 338 |
+
|
| 339 |
+
if (!response.ok) {
|
| 340 |
+
throw new Error(data.detail || 'Failed to create context');
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
currentContextId = data.context_id;
|
| 344 |
+
|
| 345 |
+
contextResponse.textContent = JSON.stringify(data, null, 2);
|
| 346 |
+
contextResponse.style.display = 'block';
|
| 347 |
+
contextSuccess.textContent = `Context created successfully! Context ID: ${currentContextId}`;
|
| 348 |
+
contextSuccess.style.display = 'block';
|
| 349 |
+
} catch (error) {
|
| 350 |
+
contextError.textContent = `Error: ${error.message}`;
|
| 351 |
+
contextError.style.display = 'block';
|
| 352 |
+
} finally {
|
| 353 |
+
contextLoading.style.display = 'none';
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
async function processInteraction() {
|
| 358 |
+
if (!currentContextId) {
|
| 359 |
+
alert('Please create a context first!');
|
| 360 |
+
return;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
const interactionInput = document.getElementById('interactionInput');
|
| 364 |
+
const interactionLoading = document.getElementById('interactionLoading');
|
| 365 |
+
const interactionError = document.getElementById('interactionError');
|
| 366 |
+
const interactionResponse = document.getElementById('interactionResponse');
|
| 367 |
+
|
| 368 |
+
if (!interactionInput.value.trim()) {
|
| 369 |
+
alert('Please enter a question or prompt!');
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
interactionLoading.style.display = 'block';
|
| 374 |
+
interactionError.style.display = 'none';
|
| 375 |
+
interactionResponse.style.display = 'none';
|
| 376 |
+
|
| 377 |
+
try {
|
| 378 |
+
const response = await fetch('/interact', {
|
| 379 |
+
method: 'POST',
|
| 380 |
+
headers: {
|
| 381 |
+
'Content-Type': 'application/json'
|
| 382 |
+
},
|
| 383 |
+
body: JSON.stringify({
|
| 384 |
+
context_id: currentContextId,
|
| 385 |
+
interaction_type: 'text',
|
| 386 |
+
content: {
|
| 387 |
+
text: interactionInput.value.trim()
|
| 388 |
+
},
|
| 389 |
+
format: 'text',
|
| 390 |
+
model_name: 'mock'
|
| 391 |
+
})
|
| 392 |
+
});
|
| 393 |
+
|
| 394 |
+
const data = await response.json();
|
| 395 |
+
|
| 396 |
+
if (!response.ok) {
|
| 397 |
+
throw new Error(data.detail || 'Failed to process interaction');
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
interactionResponse.textContent = data.content.text || JSON.stringify(data, null, 2);
|
| 401 |
+
interactionResponse.style.display = 'block';
|
| 402 |
+
} catch (error) {
|
| 403 |
+
interactionError.textContent = `Error: ${error.message}`;
|
| 404 |
+
interactionError.style.display = 'block';
|
| 405 |
+
} finally {
|
| 406 |
+
interactionLoading.style.display = 'none';
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
</script>
|
| 410 |
+
</body>
|
| 411 |
+
</html>
|
storage/students/293fc89c-8969-43ed-9a8f-f3e2131d97cd.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "293fc89c-8969-43ed-9a8f-f3e2131d97cd",
|
| 3 |
+
"name": "habib",
|
| 4 |
+
"educational_level": "graduate",
|
| 5 |
+
"learning_style": "visual",
|
| 6 |
+
"interests": [
|
| 7 |
+
"science",
|
| 8 |
+
"technology",
|
| 9 |
+
"art"
|
| 10 |
+
],
|
| 11 |
+
"strengths": [
|
| 12 |
+
"problem-solving",
|
| 13 |
+
"creativity"
|
| 14 |
+
],
|
| 15 |
+
"areas_for_improvement": [
|
| 16 |
+
"time-management"
|
| 17 |
+
],
|
| 18 |
+
"completed_objectives": [],
|
| 19 |
+
"current_objectives": []
|
| 20 |
+
}
|