archai-adaptive-engine / adaptive_engine.py
Builder-Neekhil's picture
Upload adaptive_engine.py with huggingface_hub
3054503 verified
"""
ARCHAI Adaptive AI Assessment Engine
===================================
SOTA: 2PL-IRT adaptive selection + Bayesian knowledge tracing + LLM learning paths
Plug-and-play backend for your-ai-arch.netlify.app
Replaces static question bank with adaptive, intelligent assessment.
"""
import json
import math
import random
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, field
from enum import Enum
import numpy as np
from scipy.optimize import minimize_scalar
# ============================================================================
# DATA MODELS — compatible with existing archai frontend
# ============================================================================
class Dimension(Enum):
LITERACY = "literacy"
TOOLING = "tooling"
STRATEGY = "strategy"
IMPLEMENTATION = "implementation"
GOVERNANCE = "governance"
DATA = "data"
DIMENSION_LABELS = {
Dimension.LITERACY: "AI Literacy",
Dimension.TOOLING: "Tool Proficiency",
Dimension.STRATEGY: "Strategic Thinking",
Dimension.IMPLEMENTATION: "Implementation",
Dimension.GOVERNANCE: "Governance & Ethics",
Dimension.DATA: "Data Fluency",
}
DIMENSION_COLORS = {
"literacy": "#FB7185",
"tooling": "#10B981",
"strategy": "#F97316",
"implementation": "#14B8A6",
"governance": "#F43F5E",
"data": "#34D399",
}
@dataclass
class Question:
id: str
dimension: Dimension
text: str
options: List[str]
difficulty: float # b parameter in IRT (higher = harder)
discrimination: float # a parameter in IRT
concept_tags: List[str] = field(default_factory=list)
@dataclass
class StudentState:
"""Bayesian knowledge state per dimension."""
session_id: str
theta: Dict[Dimension, float] # latent ability estimate per dimension
theta_variance: Dict[Dimension, float]
asked_questions: List[str] = field(default_factory=list)
responses: Dict[str, int] = field(default_factory=dict) # question_id -> option_index
response_history: List[Dict] = field(default_factory=list)
start_time: str = field(default_factory=lambda: datetime.utcnow().isoformat())
def get_unasked(self, question_bank: List[Question]) -> List[Question]:
return [q for q in question_bank if q.id not in self.asked_questions]
# ============================================================================
# QUESTION BANK — Calibrated with IRT parameters
# ============================================================================
def build_question_bank() -> List[Question]:
"""Calibrated question bank mapped to archai's 6 dimensions."""
bank = []
# --- LITERACY ---
bank.extend([
Question("lit_1", Dimension.LITERACY,
"How well can you explain the difference between machine learning, deep learning, and generative AI?",
["Not at all", "Basic overview", "Clearly with examples", "Could teach a workshop"],
-2.0, 1.2, ["ml_basics", "dl_vs_ml", "gen_ai"]),
Question("lit_2", Dimension.LITERACY,
"How familiar are you with concepts like tokens, context windows, fine-tuning, and RAG?",
["Never heard of them", "Heard the terms", "Understand conceptually", "Use them in practice"],
-1.0, 1.5, ["tokens", "rag", "fine_tuning"]),
Question("lit_3", Dimension.LITERACY,
"Can you explain what a transformer architecture is and how attention mechanisms work?",
["No idea", "Vague understanding", "Can explain to a peer", "Can implement from scratch"],
0.5, 1.8, ["transformers", "attention", "architecture"]),
Question("lit_4", Dimension.LITERACY,
"How well do you understand the scaling laws that govern LLM performance?",
["Never heard", "Basic awareness", "Can discuss tradeoffs", "Can apply to model selection"],
1.5, 2.0, ["scaling_laws", "compute", "model_selection"]),
])
# --- TOOLING ---
bank.extend([
Question("tool_1", Dimension.TOOLING,
"How frequently do you use AI tools (ChatGPT, Copilot, Claude, etc.) in your work?",
["Never", "Occasionally", "Weekly", "Daily, core to workflow"],
-2.0, 1.0, ["chatgpt", "copilot", "claude", "usage_frequency"]),
Question("tool_2", Dimension.TOOLING,
"Can you chain multiple AI tools or prompts to complete a complex task end-to-end?",
["No", "Tried once or twice", "Sometimes successfully", "Regularly with custom workflows"],
-0.5, 1.3, ["prompt_chaining", "tool_orchestration", "workflows"]),
Question("tool_3", Dimension.TOOLING,
"Have you set up API integrations with LLM providers (OpenAI, Anthropic, local models)?",
["Never", "Used a no-code tool", "Wrote code for it", "Built production integrations"],
0.5, 1.5, ["api_integration", "openai_api", "local_models"]),
Question("tool_4", Dimension.TOOLING,
"How comfortable are you running open-source models locally with Ollama, LM Studio, or vLLM?",
["Don't know what those are", "Installed one once", "Run models regularly", "Optimize inference for production"],
1.5, 1.8, ["ollama", "lm_studio", "vllm", "local_inference"]),
])
# --- STRATEGY ---
bank.extend([
Question("strat_1", Dimension.STRATEGY,
"When evaluating a new project, do you assess where AI could add value or reduce effort?",
["Never consider it", "Occasionally think about it", "Systematically evaluate", "Lead AI-first ideation"],
-1.5, 1.1, ["ai_opportunity", "value_assessment", "project_evaluation"]),
Question("strat_2", Dimension.STRATEGY,
"Can you articulate the ROI or business case for an AI initiative to stakeholders?",
["Wouldn't know where to start", "Could outline rough benefits", "Can build a structured case", "Have done this successfully"],
-0.5, 1.2, ["roi", "business_case", "stakeholder_communication"]),
Question("strat_3", Dimension.STRATEGY,
"Do you have a framework for prioritizing AI initiatives by feasibility vs impact?",
["No framework", "Informal mental model", "Structured scoring system", "Organization-wide prioritization process"],
0.8, 1.6, ["prioritization", "feasibility", "impact_matrix"]),
Question("strat_4", Dimension.STRATEGY,
"Can you identify competitive moats and differentiation through AI capabilities?",
["Not applicable to my role", "Basic understanding", "Can analyze for my industry", "Have built AI-driven differentiation"],
1.8, 2.0, ["competitive_moat", "differentiation", "ai_strategy"]),
])
# --- IMPLEMENTATION ---
bank.extend([
Question("impl_1", Dimension.IMPLEMENTATION,
"Have you built, deployed, or significantly configured an AI-powered solution?",
["Never", "Followed a tutorial", "Built a working prototype", "Deployed to production"],
-1.5, 1.2, ["deployment", "prototype", "production"]),
Question("impl_2", Dimension.IMPLEMENTATION,
"How comfortable are you with prompt engineering, API integration, or model evaluation?",
["Not at all", "Basic awareness", "Can do with guidance", "Highly proficient"],
-0.5, 1.4, ["prompt_engineering", "api_integration", "model_eval"]),
Question("impl_3", Dimension.IMPLEMENTATION,
"Have you built a RAG system or fine-tuned a model for a specific domain?",
["Don't know what RAG is", "Used a no-code RAG tool", "Built custom RAG pipeline", "Fine-tuned and deployed domain model"],
0.8, 1.6, ["rag", "fine_tuning", "domain_adaptation"]),
Question("impl_4", Dimension.IMPLEMENTATION,
"Can you architect a multi-agent system or design LLM orchestration workflows?",
["No idea", "Understand conceptually", "Built a simple agent", "Production multi-agent system"],
1.8, 1.9, ["agents", "orchestration", "langgraph", "crewai"]),
])
# --- GOVERNANCE ---
bank.extend([
Question("gov_1", Dimension.GOVERNANCE,
"How well do you understand AI risks like hallucination, bias, data privacy, and IP exposure?",
["Not aware", "Heard about them", "Understand key risks", "Can design mitigations"],
-1.5, 1.0, ["hallucination", "bias", "privacy", "ip_risk"]),
Question("gov_2", Dimension.GOVERNANCE,
"Does your workflow include checks for AI output accuracy, fairness, or compliance?",
["No checks", "Occasional review", "Standard process", "Systematic governance framework"],
-0.3, 1.2, ["accuracy_checks", "fairness", "compliance"]),
Question("gov_3", Dimension.GOVERNANCE,
"Are you familiar with AI regulations (EU AI Act, NIST AI RMF, ISO 42001)?",
["Never heard", "Aware they exist", "Can navigate requirements", "Implemented compliance program"],
0.8, 1.5, ["eu_ai_act", "nist_rmf", "iso_42001", "regulation"]),
Question("gov_4", Dimension.GOVERNANCE,
"Can you design an AI governance framework covering data lineage, model cards, and audit trails?",
["Not my area", "Understand components", "Can design for a team", "Enterprise-wide implementation"],
1.8, 1.8, ["governance_framework", "model_cards", "audit_trail", "data_lineage"]),
])
# --- DATA ---
bank.extend([
Question("data_1", Dimension.DATA,
"How comfortable are you working with structured and unstructured data for AI use cases?",
["Uncomfortable", "Can read simple reports", "Can clean and prep data", "Can architect data pipelines"],
-1.5, 1.1, ["structured_data", "unstructured_data", "data_prep"]),
Question("data_2", Dimension.DATA,
"Can you evaluate whether data is sufficient and appropriate for training or prompting an AI system?",
["No", "Vaguely", "With guidance", "Yes, independently"],
-0.3, 1.3, ["data_quality", "data_sufficiency", "training_data"]),
Question("data_3", Dimension.DATA,
"Have you worked with embeddings, vector databases, or data augmentation for AI?",
["No experience", "Used a vector DB via UI", "Built embedding pipelines", "Optimized retrieval systems"],
0.8, 1.5, ["embeddings", "vector_db", "data_augmentation", "retrieval"]),
Question("data_4", Dimension.DATA,
"Can you design data collection strategies and evaluate dataset bias for model training?",
["Not applicable", "Basic awareness", "Can assess existing datasets", "Design collection from scratch"],
1.6, 1.7, ["data_collection", "dataset_bias", "training_strategy"]),
])
return bank
# ============================================================================
# IRT ENGINE — 2PL Model with Fisher Information
# ============================================================================
class IRTEngine:
"""
Two-Parameter Logistic (2PL) IRT model.
P(correct|theta) = sigmoid(a * (theta - b))
"""
@staticmethod
def sigmoid(z: float) -> float:
return 1.0 / (1.0 + math.exp(-z))
@staticmethod
def probability(theta: float, a: float, b: float) -> float:
"""Probability of a correct (high-score) response."""
return IRTEngine.sigmoid(a * (theta - b))
@staticmethod
def fisher_information(theta: float, a: float, b: float) -> float:
"""Fisher information — measure of how precisely a question measures ability at theta."""
p = IRTEngine.probability(theta, a, b)
return (a ** 2) * p * (1 - p)
@staticmethod
def likelihood(theta: float, responses: List[Tuple[float, float, int]], max_option: int = 3) -> float:
"""
Compute likelihood of theta given responses.
responses: list of (a, b, option_index) tuples.
option_index 0 = lowest, max_option = highest.
We model this as a graded response model approximation.
"""
log_lik = 0.0
for a, b, opt_idx in responses:
# Map option to a "correctness weight" 0.0 to 1.0
weight = opt_idx / max_option
# Expected probability of this weighted response
p = IRTEngine.probability(theta, a, b)
# Weighted likelihood: blend of correct and incorrect
# Higher option → closer to p=1, lower option → closer to p=0
expected = weight * p + (1 - weight) * (1 - p)
expected = max(expected, 1e-10) # avoid log(0)
log_lik += math.log(expected)
return log_lik
@staticmethod
def estimate_theta(responses: List[Tuple[float, float, int]], prior_mean: float = 0.0, prior_std: float = 1.0) -> Tuple[float, float]:
"""
MAP estimate of theta given responses.
Returns (theta_estimate, standard_error).
"""
if not responses:
return prior_mean, prior_std
# Prior contribution to log-posterior
def neg_log_posterior(theta):
log_prior = -0.5 * ((theta - prior_mean) / prior_std) ** 2
log_lik = IRTEngine.likelihood(theta, responses)
return -(log_prior + log_lik)
result = minimize_scalar(neg_log_posterior, bounds=(-4.0, 4.0), method='bounded')
theta_hat = result.x
# Approximate standard error from Fisher information at MAP
fisher = sum(IRTEngine.fisher_information(theta_hat, a, b) for a, b, _ in responses)
se = 1.0 / math.sqrt(fisher + 1.0 / (prior_std ** 2))
return theta_hat, se
# ============================================================================
# ADAPTIVE SELECTOR — Fisher Information Maximization
# ============================================================================
class AdaptiveSelector:
"""
Selects next question maximizing Fisher information at current ability estimate.
Implements content balancing (ensures all dimensions are covered).
"""
def __init__(self, min_per_dimension: int = 1, max_total: int = 12, target_precision: float = 0.3):
self.min_per_dimension = min_per_dimension
self.max_total = max_total
self.target_precision = target_precision
def select_next(
self,
state: StudentState,
question_bank: List[Question],
balance_penalty: float = 2.0
) -> Optional[Question]:
"""
Select next question using Fisher information with content balancing.
"""
unasked = state.get_unasked(question_bank)
if not unasked:
return None
# Count questions per dimension already asked
dim_counts = {d: 0 for d in Dimension}
for qid in state.asked_questions:
q = next((qq for qq in question_bank if qq.id == qid), None)
if q:
dim_counts[q.dimension] += 1
# Information scores
scores = []
for q in unasked:
theta = state.theta.get(q.dimension, 0.0)
info = IRTEngine.fisher_information(theta, q.discrimination, q.difficulty)
# Content balancing: boost under-represented dimensions
count = dim_counts[q.dimension]
if count < self.min_per_dimension:
info *= balance_penalty * (self.min_per_dimension - count + 1)
# Precision stopping: if SE is already good, slightly deprioritize
se = state.theta_variance.get(q.dimension, 1.0)
if se < self.target_precision:
info *= 0.7
scores.append((info, q))
scores.sort(key=lambda x: x[0], reverse=True)
# Return top-scoring question
return scores[0][1] if scores else None
def should_stop(self, state: StudentState) -> bool:
"""Stop when max questions reached or all dimensions have sufficient precision."""
if len(state.asked_questions) >= self.max_total:
return True
# Stop early if all dimensions have good precision and minimum coverage
dim_coverage = {d: 0 for d in Dimension}
dim_precision = {d: float('inf') for d in Dimension}
for qid in state.asked_questions:
q = next((qq for qq in build_question_bank() if qq.id == qid), None)
if q:
dim_coverage[q.dimension] += 1
dim_precision[q.dimension] = min(
dim_precision[q.dimension],
state.theta_variance.get(q.dimension, 1.0)
)
all_covered = all(c >= self.min_per_dimension for c in dim_coverage.values())
all_precise = all(se < self.target_precision for se in dim_precision.values() if se != float('inf'))
return all_covered and all_precise and len(state.asked_questions) >= 6
# ============================================================================
# KNOWLEDGE TRACING — Bayesian Update
# ============================================================================
class KnowledgeTracer:
"""
Bayesian knowledge tracing per dimension.
Updates latent ability (theta) after each response.
"""
def __init__(self, prior_mean: float = 0.0, prior_std: float = 1.0):
self.prior_mean = prior_mean
self.prior_std = prior_std
self.irt = IRTEngine()
def update(
self,
state: StudentState,
question: Question,
option_index: int,
max_option: int = 3
) -> StudentState:
"""Update student state with new response using Bayesian IRT."""
dim = question.dimension
# Add to history
state.asked_questions.append(question.id)
state.responses[question.id] = option_index
state.response_history.append({
"question_id": question.id,
"dimension": dim.value,
"option_index": option_index,
"timestamp": datetime.utcnow().isoformat(),
})
# Gather all responses for this dimension
dim_responses = []
for qid, opt in state.responses.items():
q = next((qq for qq in build_question_bank() if qq.id == qid), None)
if q and q.dimension == dim:
dim_responses.append((q.discrimination, q.difficulty, opt))
# Re-estimate theta for this dimension
theta, se = self.irt.estimate_theta(dim_responses, self.prior_mean, self.prior_std)
state.theta[dim] = theta
state.theta_variance[dim] = se
return state
def get_dimension_scores(self, state: StudentState) -> Dict[str, int]:
"""Convert latent theta to 0-100 scores (archai-compatible)."""
scores = {}
for dim in Dimension:
theta = state.theta.get(dim, 0.0)
# Convert theta (-4 to 4) to 0-100 with sigmoid
# theta=0 → 50%, theta=2 → ~88%, theta=-2 → ~12%
score = int(round(100 * self.irt.sigmoid(theta * 0.8 + 0.1) * 1.1))
score = max(5, min(95, score))
scores[dim.value] = score
return scores
def get_overall_score(self, state: StudentState) -> int:
scores = self.get_dimension_scores(state)
return round(sum(scores.values()) / len(scores))
# ============================================================================
# LEARNING PATH GENERATOR — Structured day/week/month actionables
# ============================================================================
class LearningPathGenerator:
"""
Generates granular learning paths with day/week/month actionables.
Uses rule-based logic aligned with archai's action plan structure.
"""
def __init__(self):
self.stages = [
{"id": "awareness", "label": "Awareness", "threshold": 20, "desc": "You recognize AI's potential"},
{"id": "understanding", "label": "Understanding", "threshold": 40, "desc": "You grasp core concepts"},
{"id": "application", "label": "Application", "threshold": 60, "desc": "You use AI daily"},
{"id": "integration", "label": "Integration", "threshold": 75, "desc": "AI is embedded in your work"},
{"id": "mastery", "label": "Mastery", "threshold": 90, "desc": "You architect AI systems"},
]
self.archetypes = [
{"id": "pioneer", "label": "The Pioneer", "desc": "High across the board — charting new territory",
"condition": lambda s: all(v >= 70 for v in s.values())},
{"id": "responsible-builder", "label": "The Responsible Builder", "desc": "Balances capability with caution",
"condition": lambda s: s.get("governance", 0) >= 60 and s.get("implementation", 0) >= 50},
{"id": "data-craftsman", "label": "The Data Craftsman", "desc": "Data-first, builds from evidence",
"condition": lambda s: s.get("data", 0) >= 60 and s.get("implementation", 0) >= 50},
{"id": "power-user", "label": "The Power User", "desc": "Fluent with tools, ready to strategize next",
"condition": lambda s: s.get("tooling", 0) >= 60 and s.get("strategy", 0) < 50},
{"id": "vision-caster", "label": "The Vision Caster", "desc": "Strategic thinker — hands-on comes next",
"condition": lambda s: s.get("strategy", 0) >= 60 and s.get("implementation", 0) < 50},
{"id": "integrator", "label": "The Integrator", "desc": "Well-rounded across every dimension",
"condition": lambda s: (avg := sum(s.values())/len(s.values()), sd := (sum((v-avg)**2 for v in s.values())/len(s.values()))**0.5, sd < 18 and avg >= 50)[2]},
{"id": "explorer", "label": "The Explorer", "desc": "Curious and ready to dive in",
"condition": lambda s: all(v < 45 for v in s.values())},
{"id": "apprentice", "label": "The Apprentice", "desc": "Building foundational fluency",
"condition": lambda s: True}, # fallback
]
def determine_stage(self, overall_score: int) -> Dict:
stage = self.stages[0]
for s in self.stages:
if overall_score >= s["threshold"]:
stage = s
return stage
def determine_archetype(self, scores: Dict[str, int]) -> Dict:
for arch in self.archetypes:
try:
if arch["condition"](scores):
return {"id": arch["id"], "label": arch["label"], "desc": arch["desc"]}
except:
continue
return {"id": "apprentice", "label": "The Apprentice", "desc": "Building foundational fluency"}
def generate_learning_path(
self,
scores: Dict[str, int],
persona_id: str,
hours_per_week: int,
budget_usd: int,
hardware_id: Optional[str] = None,
preference: Optional[str] = None
) -> Dict[str, Any]:
"""
Generate a comprehensive learning path with day/week/month granularity.
"""
overall = round(sum(scores.values()) / len(scores))
stage = self.determine_stage(overall)
archetype = self.determine_archetype(scores)
# Identify weakest dimensions (gaps to close)
sorted_dims = sorted(scores.items(), key=lambda x: x[1])
# Build time-bucketed actionables
days = self._generate_days(sorted_dims, persona_id, hours_per_week, budget_usd)
weeks = self._generate_weeks(sorted_dims, persona_id, hours_per_week, budget_usd, stage)
months = self._generate_months(sorted_dims, persona_id, hours_per_week, budget_usd, stage, hardware_id)
return {
"overall_score": overall,
"stage": stage,
"archetype": archetype,
"dimension_scores": scores,
"gaps": [{"dimension": d, "score": s, "priority": i+1} for i, (d, s) in enumerate(sorted_dims[:3])],
"strengths": [{"dimension": d, "score": s} for d, s in sorted_dims[-2:]],
"learning_path": {
"days": days,
"weeks": weeks,
"months": months,
},
"projections": self._compute_projections(overall, stage, hours_per_week),
"meta": {
"total_hours": sum(a.get("estimated_hours", 0) for w in weeks for a in w["actions"]),
"estimated_weeks": max(1, round(sum(a.get("estimated_hours", 0) for w in weeks for a in w["actions"]) / hours_per_week)) if hours_per_week else None,
"generated_at": datetime.utcnow().isoformat(),
}
}
def _generate_days(self, sorted_dims: List[Tuple[str, int]], persona_id: str, hours: int, budget: int) -> List[Dict]:
"""Day 1-7 granular actionables — immediate, bite-sized wins."""
weakest = sorted_dims[0][0]
# Day 1: Always start with the weakest dimension
day1_actions = {
"literacy": {
"title": "Read the Anthropic Prompt Engineering Guide",
"desc": "The highest-ROI single hour. Changes how you talk to every model.",
"link": "https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview",
"time": "45 min",
"type": "reading",
},
"tooling": {
"title": "Try Google AI Studio with a real work task",
"desc": "Open AI Studio. Paste any work email or doc. Ask: 'What am I missing?'",
"link": "https://aistudio.google.com",
"time": "15 min",
"type": "hands_on",
},
"strategy": {
"title": "List 3 weekly tasks you hate",
"desc": "Open Notes. Write the 3 most repetitive things you did this week.",
"link": None,
"time": "10 min",
"type": "worksheet",
},
"implementation": {
"title": "Install Claude Code or Cursor",
"desc": "One terminal command. You'll have an AI pair programmer before lunch.",
"link": "https://docs.anthropic.com/en/docs/claude-code",
"time": "10 min",
"type": "setup",
},
"governance": {
"title": "Skim the NIST AI RMF index",
"desc": "Five minutes tells you what you don't know. The framework is free.",
"link": "https://www.nist.gov/itl/ai-risk-management-framework",
"time": "15 min",
"type": "reading",
},
"data": {
"title": "Ask Gemini about your spreadsheet",
"desc": "Open any Google Sheet. Use the side panel: 'Summarize this for me.'",
"link": "https://workspace.google.com/products/gemini/",
"time": "5 min",
"type": "hands_on",
},
}
days = []
# Day 1: Close biggest gap
action = day1_actions.get(weakest, day1_actions["literacy"])
days.append({
"day": 1,
"focus": f"Close your {weakest} gap",
"title": action["title"],
"description": action["desc"],
"action_type": action["type"],
"estimated_time": action["time"],
"resource_link": action["link"],
"why": f"Your {weakest} score is lowest. A small win here unlocks everything else.",
"quick_win": True,
})
# Day 2-7: Rotating through dimensions
day_templates = [
("tooling", "Daily AI tool practice", "Use an AI tool for one real work task today.", "15 min"),
("literacy", "Watch one AI explainer", "Pick a 10-min video on YouTube about LLMs, RAG, or agents.", "15 min"),
("implementation", "Build something tiny", "Create a prompt template or simple automation.", "30 min"),
("strategy", "Map one AI opportunity", "Pick a work process. Ask: how could AI help?", "20 min"),
("governance", "Review one AI risk", "Read about one AI failure case. What went wrong?", "15 min"),
("data", "Explore your data", "Open a dataset you use. What patterns could AI find?", "20 min"),
]
for i, (dim, title, desc, time) in enumerate(day_templates, start=2):
days.append({
"day": i,
"focus": dim,
"title": title,
"description": desc,
"action_type": "practice",
"estimated_time": time,
"resource_link": None,
"why": f"Building muscle memory in {dim} through consistent micro-practice.",
"quick_win": False,
})
return days
def _generate_weeks(self, sorted_dims: List[Tuple[str, int]], persona_id: str, hours: int, budget: int, stage: Dict) -> List[Dict]:
"""Week-by-week structured plan with measurable milestones."""
# Determine how many weeks based on hours and stage
gap_to_next = max(0, self._next_stage_threshold(stage) - round(sum(s for _, s in sorted_dims)/len(sorted_dims)))
estimated_weeks = max(2, min(8, math.ceil(gap_to_next / max(5, hours * 0.3)))) if hours else 4
weeks = []
for week_num in range(1, estimated_weeks + 1):
# Rotate focus dimensions
focus_dims = [d for d, _ in sorted_dims[:2]]
focus = focus_dims[(week_num - 1) % len(focus_dims)] if focus_dims else "literacy"
actions = self._week_actions(week_num, focus, persona_id, hours, budget, stage)
weeks.append({
"week": week_num,
"focus_dimension": focus,
"theme": self._week_theme(week_num, stage),
"milestone": self._week_milestone(week_num, focus, stage),
"actions": actions,
"estimated_hours": sum(a.get("estimated_hours", 0) for a in actions),
"checkpoint": f"Score {min(95, 20 + week_num * 10)}% in {focus} dimension",
})
return weeks
def _week_actions(self, week: int, focus: str, persona_id: str, hours: int, budget: int, stage: Dict) -> List[Dict]:
"""Generate specific actions for a week."""
actions = []
# Core learning block (always present)
if focus == "literacy":
actions.append({
"title": f"Week {week}: Deep-dive into AI fundamentals",
"description": "Study transformer architecture, attention mechanisms, and model families.",
"type": "course",
"resource": "HuggingFace NLP Course",
"link": "https://huggingface.co/learn/nlp-course",
"estimated_hours": 2,
"deliverable": "Complete 2 chapters + quiz",
"cost": "$0",
})
elif focus == "tooling":
actions.append({
"title": f"Week {week}: Master one new AI tool",
"description": "Deep exploration of one tool: Claude, Cursor, or a local model runner.",
"type": "lab",
"resource": "Tool documentation + 3 real tasks",
"link": None,
"estimated_hours": 2,
"deliverable": "Complete 3 real work tasks using the tool",
"cost": "$0" if budget == 0 else "$0-20",
})
elif focus == "strategy":
actions.append({
"title": f"Week {week}: Evaluate 2 AI opportunities",
"description": "Map processes at work. Score by feasibility × impact. Present to one colleague.",
"type": "workshop",
"resource": "AI Use Case Canvas",
"link": "https://aiusecase.io",
"estimated_hours": 2,
"deliverable": "One-page opportunity brief",
"cost": "$0",
})
elif focus == "implementation":
actions.append({
"title": f"Week {week}: Build a working prototype",
"description": "Create a RAG pipeline, agent, or API integration. Ship to a friend for feedback.",
"type": "lab",
"resource": "Dify or Flowise for no-code; LangChain for code",
"link": "https://dify.ai",
"estimated_hours": 3,
"deliverable": "Working prototype + demo video",
"cost": "$0",
})
elif focus == "governance":
actions.append({
"title": f"Week {week}: Draft your AI policy",
"description": "Cover approved tools, data classification, review requirements.",
"type": "workshop",
"resource": "NIST AI RMF Template",
"link": "https://www.nist.gov/artificial-intelligence/ai-risk-management-framework",
"estimated_hours": 2,
"deliverable": "1-page team AI policy draft",
"cost": "$0",
})
elif focus == "data":
actions.append({
"title": f"Week {week}: Data pipeline practice",
"description": "Clean a dataset, build embeddings, or set up a vector DB.",
"type": "lab",
"resource": "ChromaDB or Weaviate tutorials",
"link": "https://docs.trychroma.com",
"estimated_hours": 2,
"deliverable": "Working vector search over your documents",
"cost": "$0",
})
# Reflection action (every week)
actions.append({
"title": f"Week {week} reflection",
"description": "Review what worked. Note one thing that surprised you. Adjust next week's plan.",
"type": "reflection",
"resource": "Personal learning journal",
"link": None,
"estimated_hours": 0.5,
"deliverable": "3 bullet journal entries",
"cost": "$0",
})
return actions
def _week_theme(self, week: int, stage: Dict) -> str:
themes = [
"Foundation & Discovery",
"Building Core Skills",
"Expanding Your Toolkit",
"Applying to Real Work",
"Deepening Specialization",
"Integration & Scale",
"Governance & Safety",
"Mastery & Teaching",
]
return themes[(week - 1) % len(themes)]
def _week_milestone(self, week: int, focus: str, stage: Dict) -> str:
return f"Complete {week} week(s) of focused practice in {focus}"
def _next_stage_threshold(self, current_stage: Dict) -> int:
thresholds = [20, 40, 60, 75, 90, 100]
current = current_stage["threshold"]
for t in thresholds:
if t > current:
return t
return 100
def _generate_months(self, sorted_dims: List[Tuple[str, int]], persona_id: str, hours: int, budget: int, stage: Dict, hardware_id: Optional[str]) -> List[Dict]:
"""Month-level strategic goals with outcomes."""
months = []
for month_num in range(1, 4): # 3-month horizon
goals = []
if month_num == 1:
goals = [
{"title": "Close weakest gap to 50%", "metric": f"{sorted_dims[0][0]} >= 50%", "tactics": ["Daily micro-practice", "One course completion", "Peer discussion"]}
]
if persona_id in ["ml-eng", "swe", "data-sci"]:
goals.append({"title": "Ship one AI-assisted code project", "metric": "1 repo with AI integration", "tactics": ["Cursor/Claude Code", "API integration", "Document your approach"]})
elif month_num == 2:
goals = [
{"title": "Build cross-dimensional fluency", "metric": "All dimensions >= 45%", "tactics": ["Rotate weekly focus", "Interdisciplinary projects", "Teach a colleague"]}
]
if hardware_id:
goals.append({"title": "Run local models for 50% of AI tasks", "metric": "Local inference usage >= 50%", "tactics": ["Ollama setup", "Model comparison", "Latency optimization"]})
else: # month 3
goals = [
{"title": "Lead an AI initiative", "metric": "One shipped AI project or team workshop", "tactics": ["Identify opportunity", "Build consensus", "Execute with metrics"]}
]
if sum(s for _, s in sorted_dims) / len(sorted_dims) >= 60:
goals.append({"title": "Mentor 2 colleagues into AI fluency", "metric": "2 people show measurable improvement", "tactics": ["Weekly office hours", "Curated resources", "Accountability check-ins"]})
months.append({
"month": month_num,
"theme": ["Build Foundation", "Expand & Integrate", "Lead & Scale"][month_num - 1],
"strategic_goals": goals,
"checkpoint": f"Overall score target: {min(95, stage['threshold'] + month_num * 10)}%",
"review_questions": [
"What was the biggest surprise this month?",
"Which action had the highest ROI?",
"What gap still feels hardest to close?",
"Who can you teach what you learned?",
],
})
return months
def _compute_projections(self, overall: int, stage: Dict, hours_per_week: int) -> Dict:
"""Project timeline to next stage."""
next_threshold = self._next_stage_threshold(stage)
gap = max(0, next_threshold - overall)
if hours_per_week and gap > 0:
# Rough estimate: 1 point improvement per 2 focused hours
hours_needed = gap * 2
weeks_needed = max(1, math.ceil(hours_needed / hours_per_week))
target_date = datetime.utcnow() + timedelta(weeks=weeks_needed)
return {
"current_stage": stage["label"],
"next_stage": self.stages[min(self.stages.index(stage) + 1, len(self.stages) - 1)]["label"],
"gap_to_next": gap,
"estimated_weeks": weeks_needed,
"at_hours_per_week": hours_per_week,
"projected_reach_date": target_date.strftime("%b %d, %Y"),
}
return {
"current_stage": stage["label"],
"next_stage": self.stages[min(self.stages.index(stage) + 1, len(self.stages) - 1)]["label"],
"gap_to_next": gap,
"estimated_weeks": None,
"at_hours_per_week": hours_per_week,
"projected_reach_date": None,
}
# ============================================================================
# MAIN ENGINE — Orchestrator
# ============================================================================
class AdaptiveAssessmentEngine:
"""
Main orchestrator:
- Manages sessions
- Adaptive question selection via IRT
- Bayesian knowledge tracing
- Generates learning paths
"""
def __init__(self):
self.question_bank = build_question_bank()
self.irt = IRTEngine()
self.selector = AdaptiveSelector(min_per_dimension=1, max_total=12)
self.tracer = KnowledgeTracer()
self.path_gen = LearningPathGenerator()
self.sessions: Dict[str, StudentState] = {}
def start_session(self) -> Dict:
"""Initialize a new assessment session."""
session_id = str(uuid.uuid4())[:12]
state = StudentState(
session_id=session_id,
theta={d: 0.0 for d in Dimension},
theta_variance={d: 1.0 for d in Dimension},
)
self.sessions[session_id] = state
# Select first question (highest info at theta=0)
first_q = self.selector.select_next(state, self.question_bank)
return {
"session_id": session_id,
"question": self._question_to_dict(first_q) if first_q else None,
"progress": {"asked": 0, "total": 12, "dimensions_covered": []},
"status": "in_progress",
}
def submit_answer(self, session_id: str, question_id: str, option_index: int) -> Dict:
"""Submit an answer and get the next question or results."""
state = self.sessions.get(session_id)
if not state:
return {"error": "Session not found", "status": "error"}
question = next((q for q in self.question_bank if q.id == question_id), None)
if not question:
return {"error": "Question not found", "status": "error"}
# Update knowledge state
state = self.tracer.update(state, question, option_index)
# Check if we should stop
if self.selector.should_stop(state):
return self._finalize(state)
# Select next question
next_q = self.selector.select_next(state, self.question_bank)
# Calculate progress
dim_coverage = set()
for qid in state.asked_questions:
q = next((qq for qq in self.question_bank if qq.id == qid), None)
if q:
dim_coverage.add(q.dimension.value)
return {
"session_id": session_id,
"question": self._question_to_dict(next_q) if next_q else None,
"progress": {
"asked": len(state.asked_questions),
"total": 12,
"dimensions_covered": list(dim_coverage),
"current_dimension": next_q.dimension.value if next_q else None,
},
"interim_scores": self.tracer.get_dimension_scores(state),
"status": "in_progress" if next_q else "complete",
}
def get_results(self, session_id: str) -> Dict:
"""Get final assessment results."""
state = self.sessions.get(session_id)
if not state:
return {"error": "Session not found", "status": "error"}
return self._finalize(state)
def generate_path(self, session_id: str, persona_id: str, hours_per_week: int, budget_usd: int, hardware_id: Optional[str] = None, preference: Optional[str] = None) -> Dict:
"""Generate learning path from assessment results."""
state = self.sessions.get(session_id)
if not state:
return {"error": "Session not found", "status": "error"}
scores = self.tracer.get_dimension_scores(state)
path = self.path_gen.generate_learning_path(
scores, persona_id, hours_per_week, budget_usd, hardware_id, preference
)
path["session_id"] = session_id
return path
def _finalize(self, state: StudentState) -> Dict:
"""Generate final assessment report."""
scores = self.tracer.get_dimension_scores(state)
overall = self.tracer.get_overall_score(state)
stage = self.path_gen.determine_stage(overall)
archetype = self.path_gen.determine_archetype(scores)
# Strengths and gaps
sorted_scores = sorted(scores.items(), key=lambda x: x[1])
# Percentile estimation (simplified — can be calibrated with population data)
# Based on normal distribution assumption
import scipy.stats as stats
percentile = int(round(100 * stats.norm.cdf((overall - 50) / 20)))
percentile = max(1, min(99, percentile))
return {
"session_id": state.session_id,
"status": "complete",
"overall_score": overall,
"dimension_scores": scores,
"stage": stage,
"archetype": archetype,
"strengths": [
{"dimension": d, "label": DIMENSION_LABELS.get(Dimension(d), d), "score": s, "color": DIMENSION_COLORS.get(d, "#14B8A6")}
for d, s in sorted_scores[-2:]
],
"gaps": [
{"dimension": d, "label": DIMENSION_LABELS.get(Dimension(d), d), "score": s, "color": DIMENSION_COLORS.get(d, "#F43F5E")}
for d, s in sorted_scores[:2]
],
"percentile": percentile,
"questions_answered": len(state.asked_questions),
"response_history": state.response_history,
"latent_abilities": {d.value: round(t, 2) for d, t in state.theta.items()},
"measurement_precision": {d.value: round(v, 3) for d, v in state.theta_variance.items()},
}
def _question_to_dict(self, q: Optional[Question]) -> Optional[Dict]:
if not q:
return None
return {
"id": q.id,
"dimension": q.dimension.value,
"dimension_label": DIMENSION_LABELS.get(q.dimension, q.dimension.value),
"text": q.text,
"options": q.options,
"difficulty": round(q.difficulty, 2),
"discrimination": round(q.discrimination, 2),
"concept_tags": q.concept_tags,
}
# Singleton instance
engine = AdaptiveAssessmentEngine()