Spaces:
Sleeping
Sleeping
Upload 14 files
Browse files- Dockerfile +30 -0
- README.md +27 -12
- app/api/routes.py +53 -0
- app/container.py +16 -0
- app/contracts/dtos.py +54 -0
- app/main.py +24 -0
- app/services/assessment_service.py +22 -0
- app/services/learning_os_adapter.py +21 -0
- app/services/mode_controller.py +14 -0
- app/services/router_service.py +15 -0
- app/services/stitcher_service.py +27 -0
- requirements.txt +5 -0
- swagger_tests.json +14 -0
- tests/test_api.py +52 -0
Dockerfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Set up a new user named "user" with user ID 1000
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
|
| 6 |
+
# Switch to the "user" user
|
| 7 |
+
USER user
|
| 8 |
+
|
| 9 |
+
# Set home to the user's home directory
|
| 10 |
+
ENV HOME=/home/user \
|
| 11 |
+
PATH=/home/user/.local/bin:$PATH
|
| 12 |
+
|
| 13 |
+
# Set the working directory to the user's home directory
|
| 14 |
+
WORKDIR $HOME/app
|
| 15 |
+
|
| 16 |
+
# Try and run pip command after setting the user with `USER user` to avoid permission issues
|
| 17 |
+
RUN pip install --no-cache-dir --upgrade pip
|
| 18 |
+
|
| 19 |
+
# Copy requirements and install dependencies
|
| 20 |
+
COPY --chown=user requirements.txt .
|
| 21 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 22 |
+
|
| 23 |
+
# Copy the application code
|
| 24 |
+
COPY --chown=user app/ ./app/
|
| 25 |
+
|
| 26 |
+
# Expose port 7860 (HF Spaces default)
|
| 27 |
+
EXPOSE 7860
|
| 28 |
+
|
| 29 |
+
# Run the application on port 7860
|
| 30 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,12 +1,27 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Multi-Disciplinary Thinking Engine
|
| 3 |
+
emoji: 🔀
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Multi-Disciplinary Thinking Engine
|
| 11 |
+
|
| 12 |
+
Generates cross-domain learning experiences that connect concepts across disciplines.
|
| 13 |
+
|
| 14 |
+
## Features
|
| 15 |
+
|
| 16 |
+
- **Cross-Domain Mapping**: Links concepts across subjects
|
| 17 |
+
- **Integrated Assessments**: Multi-disciplinary challenges
|
| 18 |
+
- **Transfer Learning**: Reinforces concept portability
|
| 19 |
+
- **Holistic Understanding**: Builds interconnected knowledge
|
| 20 |
+
|
| 21 |
+
## API
|
| 22 |
+
|
| 23 |
+
**Endpoint**: `POST /run`
|
| 24 |
+
|
| 25 |
+
## Deployment
|
| 26 |
+
|
| 27 |
+
This service is configured for Hugging Face Spaces deployment with Docker SDK.
|
app/api/routes.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 2 |
+
from app.contracts.dtos import (
|
| 3 |
+
MultiDisciplinaryRequest, MDTPlan, ToggleModeRequest, ToggleModeResponse,
|
| 4 |
+
SelectDisciplinesResponse, TransferCheckRequest, TransferCheckResponse,
|
| 5 |
+
UpdateMasteryRequest, UpdateMasteryResponse
|
| 6 |
+
)
|
| 7 |
+
from app.container import container
|
| 8 |
+
from typing import List
|
| 9 |
+
|
| 10 |
+
router = APIRouter()
|
| 11 |
+
|
| 12 |
+
@router.post("/toggle-mode", response_model=ToggleModeResponse)
|
| 13 |
+
async def toggle_mode(request: ToggleModeRequest):
|
| 14 |
+
controller = container.get("mode_controller")
|
| 15 |
+
if not controller:
|
| 16 |
+
raise HTTPException(status_code=500, detail="Service not available")
|
| 17 |
+
return controller.toggle_mode(request.learner_id, request.enabled)
|
| 18 |
+
|
| 19 |
+
@router.post("/select-disciplines", response_model=SelectDisciplinesResponse)
|
| 20 |
+
async def select_disciplines(concept_id: str, learner_profile: dict): # Simplified input for now
|
| 21 |
+
router_service = container.get("router_service")
|
| 22 |
+
if not router_service:
|
| 23 |
+
raise HTTPException(status_code=500, detail="Service not available")
|
| 24 |
+
disciplines = router_service.select_disciplines(concept_id, learner_profile)
|
| 25 |
+
return SelectDisciplinesResponse(concept_id=concept_id, ranked_disciplines=disciplines)
|
| 26 |
+
|
| 27 |
+
@router.post("/plan", response_model=MDTPlan)
|
| 28 |
+
async def build_mdt_plan(request: MultiDisciplinaryRequest):
|
| 29 |
+
stitcher_service = container.get("stitcher_service")
|
| 30 |
+
if not stitcher_service:
|
| 31 |
+
raise HTTPException(status_code=500, detail="Service not available")
|
| 32 |
+
return stitcher_service.build_mdt_plan(request)
|
| 33 |
+
|
| 34 |
+
@router.post("/generate")
|
| 35 |
+
async def generate_mdt_course(mdt_plan: MDTPlan):
|
| 36 |
+
adapter = container.get("learning_os_adapter")
|
| 37 |
+
if not adapter:
|
| 38 |
+
raise HTTPException(status_code=500, detail="Service not available")
|
| 39 |
+
return adapter.generate_mdt_course(mdt_plan)
|
| 40 |
+
|
| 41 |
+
@router.post("/transfer-check", response_model=TransferCheckResponse)
|
| 42 |
+
async def run_transfer_checks(request: TransferCheckRequest):
|
| 43 |
+
assessment_service = container.get("assessment_service")
|
| 44 |
+
if not assessment_service:
|
| 45 |
+
raise HTTPException(status_code=500, detail="Service not available")
|
| 46 |
+
return assessment_service.run_transfer_checks(request.learner_id, request.target_concept_id, request.disciplines)
|
| 47 |
+
|
| 48 |
+
@router.post("/update-mastery", response_model=UpdateMasteryResponse)
|
| 49 |
+
async def update_mastery(request: UpdateMasteryRequest):
|
| 50 |
+
assessment_service = container.get("assessment_service")
|
| 51 |
+
if not assessment_service:
|
| 52 |
+
raise HTTPException(status_code=500, detail="Service not available")
|
| 53 |
+
return assessment_service.update_mastery(request.learner_id, request.evidence)
|
app/container.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any
|
| 2 |
+
|
| 3 |
+
class Container(dict):
|
| 4 |
+
def __init__(self):
|
| 5 |
+
super().__init__()
|
| 6 |
+
self._services = {}
|
| 7 |
+
|
| 8 |
+
def register(self, name: str, service: Any):
|
| 9 |
+
self._services[name] = service
|
| 10 |
+
self[name] = service
|
| 11 |
+
|
| 12 |
+
def get(self, name: str) -> Any:
|
| 13 |
+
return self._services.get(name)
|
| 14 |
+
|
| 15 |
+
# Global container instance
|
| 16 |
+
container = Container()
|
app/contracts/dtos.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Dict, Optional, Any
|
| 3 |
+
|
| 4 |
+
class Discipline(BaseModel):
|
| 5 |
+
name: str
|
| 6 |
+
role: str # e.g., "core_model", "causal_story", "real_world_context"
|
| 7 |
+
|
| 8 |
+
class LessonStep(BaseModel):
|
| 9 |
+
step: int
|
| 10 |
+
lens: str
|
| 11 |
+
objective: str
|
| 12 |
+
|
| 13 |
+
class MultiDisciplinaryRequest(BaseModel):
|
| 14 |
+
learner_id: str
|
| 15 |
+
target_concept_id: str
|
| 16 |
+
goal: str
|
| 17 |
+
level: str
|
| 18 |
+
time_budget_min: int
|
| 19 |
+
selected_disciplines: Optional[List[str]] = None
|
| 20 |
+
mode: str = "multidisciplinary"
|
| 21 |
+
|
| 22 |
+
class MDTPlan(BaseModel):
|
| 23 |
+
target_concept_id: str
|
| 24 |
+
disciplines: List[Discipline]
|
| 25 |
+
lesson_outline: List[LessonStep]
|
| 26 |
+
generated_by: str = "LearningOS"
|
| 27 |
+
assets: List[str]
|
| 28 |
+
|
| 29 |
+
class ToggleModeRequest(BaseModel):
|
| 30 |
+
learner_id: str
|
| 31 |
+
enabled: bool
|
| 32 |
+
|
| 33 |
+
class ToggleModeResponse(BaseModel):
|
| 34 |
+
learner_id: str
|
| 35 |
+
mode_active: bool
|
| 36 |
+
|
| 37 |
+
class SelectDisciplinesResponse(BaseModel):
|
| 38 |
+
concept_id: str
|
| 39 |
+
ranked_disciplines: List[str]
|
| 40 |
+
|
| 41 |
+
class TransferCheckRequest(BaseModel):
|
| 42 |
+
learner_id: str
|
| 43 |
+
target_concept_id: str
|
| 44 |
+
disciplines: List[str]
|
| 45 |
+
|
| 46 |
+
class TransferCheckResponse(BaseModel):
|
| 47 |
+
results: Dict[str, Any]
|
| 48 |
+
|
| 49 |
+
class UpdateMasteryRequest(BaseModel):
|
| 50 |
+
learner_id: str
|
| 51 |
+
evidence: Dict[str, Any]
|
| 52 |
+
|
| 53 |
+
class UpdateMasteryResponse(BaseModel):
|
| 54 |
+
mastery_deltas: Dict[str, float]
|
app/main.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from app.api.routes import router
|
| 3 |
+
from app.container import container
|
| 4 |
+
|
| 5 |
+
app = FastAPI(title="Multi-Disciplinary Training Engine", version="1.0.0")
|
| 6 |
+
app.include_router(router)
|
| 7 |
+
|
| 8 |
+
@app.get("/")
|
| 9 |
+
def read_root():
|
| 10 |
+
return {"status": "ok", "service": "multidisciplinary-engine"}
|
| 11 |
+
|
| 12 |
+
from app.services.router_service import RouterService
|
| 13 |
+
from app.services.stitcher_service import StitcherService
|
| 14 |
+
from app.services.learning_os_adapter import LearningOSAdapter
|
| 15 |
+
from app.services.mode_controller import ModeToggleController
|
| 16 |
+
from app.services.assessment_service import AssessmentService
|
| 17 |
+
|
| 18 |
+
@app.on_event("startup")
|
| 19 |
+
async def startup():
|
| 20 |
+
container.register("router_service", RouterService())
|
| 21 |
+
container.register("stitcher_service", StitcherService())
|
| 22 |
+
container.register("learning_os_adapter", LearningOSAdapter())
|
| 23 |
+
container.register("mode_controller", ModeToggleController())
|
| 24 |
+
container.register("assessment_service", AssessmentService())
|
app/services/assessment_service.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
from app.contracts.dtos import TransferCheckResponse, UpdateMasteryResponse
|
| 3 |
+
|
| 4 |
+
class AssessmentService:
|
| 5 |
+
def run_transfer_checks(self, learner_id: str, target_concept_id: str, disciplines: List[str]) -> TransferCheckResponse:
|
| 6 |
+
# TODO: Implement logic to generate or retrieve transfer check questions
|
| 7 |
+
results = {
|
| 8 |
+
"concept": target_concept_id,
|
| 9 |
+
"checks": [
|
| 10 |
+
{"discipline": d, "question": f"Explain {target_concept_id} using {d} concepts", "status": "pending"}
|
| 11 |
+
for d in disciplines
|
| 12 |
+
]
|
| 13 |
+
}
|
| 14 |
+
return TransferCheckResponse(results=results)
|
| 15 |
+
|
| 16 |
+
def update_mastery(self, learner_id: str, evidence: Dict[str, Any]) -> UpdateMasteryResponse:
|
| 17 |
+
# TODO: Implement logic to calculate mastery deltas based on evidence
|
| 18 |
+
mastery_deltas = {
|
| 19 |
+
"economics": 0.5,
|
| 20 |
+
"history": 0.2
|
| 21 |
+
}
|
| 22 |
+
return UpdateMasteryResponse(mastery_deltas=mastery_deltas)
|
app/services/learning_os_adapter.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
from app.contracts.dtos import MDTPlan
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
class LearningOSAdapter:
|
| 6 |
+
def __init__(self, base_url: str = "http://learning-os-api"):
|
| 7 |
+
self.base_url = base_url
|
| 8 |
+
|
| 9 |
+
def generate_mdt_course(self, mdt_plan: MDTPlan) -> Dict[str, Any]:
|
| 10 |
+
# TODO: Implement actual API call to Learning OS
|
| 11 |
+
# async with httpx.AsyncClient() as client:
|
| 12 |
+
# response = await client.post(f"{self.base_url}/generate", json=mdt_plan.dict())
|
| 13 |
+
# return response.json()
|
| 14 |
+
|
| 15 |
+
# Mock response
|
| 16 |
+
return {
|
| 17 |
+
"status": "success",
|
| 18 |
+
"course_id": "course_123",
|
| 19 |
+
"assets": mdt_plan.assets,
|
| 20 |
+
"message": "Course generated successfully via Learning OS"
|
| 21 |
+
}
|
app/services/mode_controller.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.contracts.dtos import ToggleModeResponse
|
| 2 |
+
|
| 3 |
+
class ModeToggleController:
|
| 4 |
+
def __init__(self):
|
| 5 |
+
# simple in-memory storage for MVP
|
| 6 |
+
self._learner_modes = {}
|
| 7 |
+
|
| 8 |
+
def toggle_mode(self, learner_id: str, enabled: bool) -> ToggleModeResponse:
|
| 9 |
+
self._learner_modes[learner_id] = enabled
|
| 10 |
+
# In a real system, this would trigger UI events or update a persistent user profile
|
| 11 |
+
return ToggleModeResponse(
|
| 12 |
+
learner_id=learner_id,
|
| 13 |
+
mode_active=enabled
|
| 14 |
+
)
|
app/services/router_service.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
|
| 3 |
+
class RouterService:
|
| 4 |
+
def select_disciplines(self, concept_id: str, learner_profile: dict) -> List[str]:
|
| 5 |
+
# TODO: Implement actual logic to rank disciplines based on concept and learner profile
|
| 6 |
+
# For now, return hardcoded disciplines as per MVP rules
|
| 7 |
+
# "Limit to 2–3 supporting disciplines"
|
| 8 |
+
# "Always include: Core lens, Story lens, Application lens"
|
| 9 |
+
|
| 10 |
+
# Mock logic
|
| 11 |
+
if "inflation" in concept_id.lower():
|
| 12 |
+
return ["economics", "history", "geography"]
|
| 13 |
+
|
| 14 |
+
# Default fallback
|
| 15 |
+
return ["science", "history", "mathematics"]
|
app/services/stitcher_service.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from app.contracts.dtos import MultiDisciplinaryRequest, MDTPlan, Discipline, LessonStep
|
| 3 |
+
|
| 4 |
+
class StitcherService:
|
| 5 |
+
def build_mdt_plan(self, request: MultiDisciplinaryRequest) -> MDTPlan:
|
| 6 |
+
# TODO: Implement AI logic to weave disciplines together
|
| 7 |
+
|
| 8 |
+
# Mock implementation based on MVP rules
|
| 9 |
+
disciplines = [
|
| 10 |
+
Discipline(name=d, role="core_model" if i==0 else "causal_story" if i==1 else "real_world_context")
|
| 11 |
+
for i, d in enumerate(request.selected_disciplines or ["economics", "history", "geography"])
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
lesson_outline = [
|
| 15 |
+
LessonStep(step=1, lens=disciplines[0].name, objective=f"Define {request.target_concept_id} + mechanisms"),
|
| 16 |
+
LessonStep(step=2, lens=disciplines[1].name, objective="Case study explanation"),
|
| 17 |
+
LessonStep(step=3, lens=disciplines[2].name, objective="Real world context and application"),
|
| 18 |
+
LessonStep(step=4, lens="integration", objective="Apply to learner’s context")
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
return MDTPlan(
|
| 22 |
+
target_concept_id=request.target_concept_id,
|
| 23 |
+
disciplines=disciplines,
|
| 24 |
+
lesson_outline=lesson_outline,
|
| 25 |
+
generated_by="LearningOS",
|
| 26 |
+
assets=["lesson://generated-id-1", "quiz://generated-id-1"] # Mock assets
|
| 27 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.30.6
|
| 3 |
+
pydantic==2.8.2
|
| 4 |
+
pydantic-settings==2.4.0
|
| 5 |
+
httpx==0.27.2
|
swagger_tests.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "Build MDT Plan",
|
| 4 |
+
"endpoint": "/plan",
|
| 5 |
+
"payload": {
|
| 6 |
+
"learner_id": "learner_123",
|
| 7 |
+
"target_concept_id": "Evolution",
|
| 8 |
+
"goal": "Understand natural selection through biological and historical lenses.",
|
| 9 |
+
"level": "intermediate",
|
| 10 |
+
"time_budget_min": 60,
|
| 11 |
+
"mode": "multidisciplinary"
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
]
|
tests/test_api.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.testclient import TestClient
|
| 2 |
+
from app.main import app
|
| 3 |
+
|
| 4 |
+
client = TestClient(app)
|
| 5 |
+
|
| 6 |
+
def test_read_root():
|
| 7 |
+
response = client.get("/")
|
| 8 |
+
assert response.status_code == 200
|
| 9 |
+
assert response.json() == {"status": "ok", "service": "multidisciplinary-engine"}
|
| 10 |
+
|
| 11 |
+
def test_toggle_mode():
|
| 12 |
+
response = client.post("/toggle-mode", json={"learner_id": "L1", "enabled": True})
|
| 13 |
+
assert response.status_code == 200
|
| 14 |
+
assert response.json()["mode_active"] is True
|
| 15 |
+
assert response.json()["learner_id"] == "L1"
|
| 16 |
+
|
| 17 |
+
def test_select_disciplines():
|
| 18 |
+
response = client.post("/select-disciplines?concept_id=inflation", json={"learner_id": "L1"}) # passing learner_profile as body
|
| 19 |
+
assert response.status_code == 200
|
| 20 |
+
data = response.json()
|
| 21 |
+
assert "economics" in data["ranked_disciplines"]
|
| 22 |
+
assert "history" in data["ranked_disciplines"]
|
| 23 |
+
|
| 24 |
+
def test_build_plan():
|
| 25 |
+
# Helper to enable mode first if needed (though service currently stateless/mocked)
|
| 26 |
+
req_data = {
|
| 27 |
+
"learner_id": "L1",
|
| 28 |
+
"target_concept_id": "inflation",
|
| 29 |
+
"goal": "understand",
|
| 30 |
+
"level": "beginner",
|
| 31 |
+
"time_budget_min": 30,
|
| 32 |
+
"selected_disciplines": ["economics", "history", "geography"]
|
| 33 |
+
}
|
| 34 |
+
response = client.post("/plan", json=req_data)
|
| 35 |
+
assert response.status_code == 200
|
| 36 |
+
data = response.json()
|
| 37 |
+
assert data["target_concept_id"] == "inflation"
|
| 38 |
+
assert len(data["lesson_outline"]) > 0
|
| 39 |
+
assert data["generated_by"] == "LearningOS"
|
| 40 |
+
|
| 41 |
+
def test_generate_course():
|
| 42 |
+
# Mock plan
|
| 43 |
+
plan = {
|
| 44 |
+
"target_concept_id": "inflation",
|
| 45 |
+
"disciplines": [{"name": "eco", "role": "core"}],
|
| 46 |
+
"lesson_outline": [{"step": 1, "lens": "eco", "objective": "test"}],
|
| 47 |
+
"generated_by": "LearningOS",
|
| 48 |
+
"assets": []
|
| 49 |
+
}
|
| 50 |
+
response = client.post("/generate", json=plan)
|
| 51 |
+
assert response.status_code == 200
|
| 52 |
+
assert response.json()["status"] == "success"
|