Spaces:
Configuration error
Configuration error
| """ | |
| 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", | |
| } | |
| 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) | |
| 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)) | |
| """ | |
| def sigmoid(z: float) -> float: | |
| return 1.0 / (1.0 + math.exp(-z)) | |
| def probability(theta: float, a: float, b: float) -> float: | |
| """Probability of a correct (high-score) response.""" | |
| return IRTEngine.sigmoid(a * (theta - b)) | |
| 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) | |
| 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 | |
| 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() | |