diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0a45403 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 3 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 3 diff --git a/docs/assessment-generation.md b/docs/assessment-generation.md new file mode 100644 index 0000000..ca21de5 --- /dev/null +++ b/docs/assessment-generation.md @@ -0,0 +1,28 @@ +# Assessment Generation Modes + +Lean AI Ops can produce a structured assessment in two ways: + +| Mode | When it is used | What it means | +|---|---|---| +| `llm` | A configured Anthropic client returns a response that matches the required structure | The assessment was generated from the live model request. Its recommendations still require evidence review and domain validation. | +| `deterministic_fallback` | No API key is configured, the client is unavailable, the live request fails, or the response cannot be parsed into the required structure | The app generated a predictable, evidence-tagged starter package from the project inputs. It is not a live-model result. | + +The CLI prints the generation mode before the report. `AssessmentResult` also records `generation_mode`, `model_name`, and `fallback_reason` for callers that want to render provenance in a UI or export. + +## Strict live-model mode + +Use `--require-llm` when a deterministic fallback would be misleading for your workflow: + +```bash +python run_demo.py \ + --input templates/sample_project.json \ + --mode dmaic \ + --audience pm \ + --require-llm +``` + +In strict mode, the command exits with a concise error when the live model result is unavailable instead of silently substituting fallback output. + +## Important limit + +A live-model result is still a structured draft. The app's evidence tags distinguish input-supported statements, hypotheses, and missing evidence; they do not validate root causes, statistical conclusions, implementation feasibility, or release decisions. diff --git a/run_demo.py b/run_demo.py index 9d2cd70..2e1f0ae 100644 --- a/run_demo.py +++ b/run_demo.py @@ -4,6 +4,7 @@ import json from pathlib import Path +from src.assessment_service import AssessmentGenerationError from src.engine import run_assessment from src.models import ProjectInput from src.renderers import render_markdown_summary @@ -14,6 +15,13 @@ def load_input(path: Path) -> ProjectInput: return ProjectInput(**data) +def _provenance_line(result) -> str: + if result.generation_mode == "llm": + return f"Generation: live model ({result.model_name})" + reason = result.fallback_reason or "Deterministic fallback was used." + return f"Generation: deterministic fallback — {reason}" + + def main() -> None: parser = argparse.ArgumentParser(description="Run the Lean Six Sigma AI copilot demo.") parser.add_argument("--input", type=Path, required=True, help="Path to project input JSON") @@ -29,10 +37,26 @@ def main() -> None: default="pm", help="Audience for summary emphasis", ) + parser.add_argument( + "--require-llm", + action="store_true", + help="Fail instead of using deterministic fallback when a live model result is unavailable.", + ) args = parser.parse_args() project = load_input(args.input) - result = run_assessment(project, mode=args.mode, audience=args.audience) + try: + result = run_assessment( + project, + mode=args.mode, + audience=args.audience, + require_llm=args.require_llm, + ) + except AssessmentGenerationError as exc: + parser.error(str(exc)) + + print(_provenance_line(result)) + print() print(render_markdown_summary(result)) diff --git a/src/assessment_service.py b/src/assessment_service.py new file mode 100644 index 0000000..288267a --- /dev/null +++ b/src/assessment_service.py @@ -0,0 +1,123 @@ +"""Assessment orchestration with explicit model/fallback provenance.""" + +from __future__ import annotations + +import json +import os +from dataclasses import replace +from typing import Any, Dict + +from src.models import AssessmentResult, ProjectInput +from src.phases import ( + _SYSTEM_PROMPT, + _build_user_message, + _deterministic_fallback, + _parse_dmaic, + _parse_items, +) + + +MODEL_NAME = "claude-sonnet-4-6" + + +class AssessmentGenerationError(RuntimeError): + """Raised when an LLM result is required but cannot be generated.""" + + +def _generation_note(mode: str, reason: str | None = None) -> str: + """Return a concise provenance note suitable for user-visible summaries.""" + if mode == "llm": + return f"Generation note: live model output ({MODEL_NAME})." + suffix = reason or "Deterministic fallback was used." + return f"Generation note: deterministic fallback. {suffix}" + + +def _fallback( + project: ProjectInput, + mode: str, + audience: str, + reason: str, +) -> AssessmentResult: + """Return a deterministic result that records why it was used.""" + result = _deterministic_fallback(project, mode, audience) + return replace( + result, + role_summary=f"{_generation_note('deterministic_fallback', reason)}\n\n{result.role_summary}", + generation_mode="deterministic_fallback", + model_name=None, + fallback_reason=reason, + ) + + +def _reason_from_exception(exc: Exception) -> str: + """Provide an actionable category without exposing credentials or payloads.""" + if isinstance(exc, json.JSONDecodeError): + return "The model response was not valid JSON for the required assessment structure." + if isinstance(exc, (KeyError, TypeError, ValueError, IndexError)): + return "The model response did not match the required assessment structure." + if isinstance(exc, ImportError): + return "The optional Anthropic client package is unavailable in this environment." + return "The live model request could not be completed." + + +def run_assessment_with_provenance( + project: ProjectInput, + mode: str, + audience: str, + *, + require_llm: bool = False, +) -> AssessmentResult: + """Generate an assessment and label whether it came from the LLM or fallback. + + The fallback remains useful for demonstrations and offline work. When + ``require_llm`` is true, an unavailable or malformed live response raises a + concise error instead of silently substituting deterministic output. + """ + api_key = os.environ.get("ANTHROPIC_API_KEY", "").strip() + if not api_key: + reason = "No ANTHROPIC_API_KEY is configured; deterministic fallback was used." + if require_llm: + raise AssessmentGenerationError(reason) + return _fallback(project, mode, audience, reason) + + try: + import anthropic + + client = anthropic.Anthropic(api_key=api_key) + message = client.messages.create( + model=MODEL_NAME, + max_tokens=4096, + system=_SYSTEM_PROMPT, + messages=[{"role": "user", "content": _build_user_message(project, mode, audience)}], + ) + raw = message.content[0].text.strip() + if raw.startswith("```"): + raw = raw.split("\n", 1)[1] + if raw.endswith("```"): + raw = raw[: raw.rfind("```")] + raw = raw.strip() + data: Dict[str, Any] = json.loads(raw) + return AssessmentResult( + project_name=project.project_name, + mode=mode, + audience=audience, + cleaned_problem_statement=data["cleaned_problem_statement"], + ctqs=_parse_items(data["ctqs"]), + sipoc=data["sipoc"], + dmaic_structure=_parse_dmaic(data["dmaic_structure"]), + root_causes=_parse_items(data["root_causes"]), + suggested_metrics=_parse_items(data["suggested_metrics"]), + improvement_actions=_parse_items(data["improvement_actions"]), + control_plan=_parse_items(data["control_plan"]), + action_tracker=data["action_tracker"], + project_memory=data["project_memory"], + role_summary=f"{_generation_note('llm')}\n\n{data['role_summary']}", + generation_mode="llm", + model_name=MODEL_NAME, + fallback_reason=None, + ) + except Exception as exc: + reason = _reason_from_exception(exc) + if require_llm: + raise AssessmentGenerationError(reason) from exc + return _fallback(project, mode, audience, reason) diff --git a/src/engine.py b/src/engine.py index 48e4f33..8fa3d0a 100644 --- a/src/engine.py +++ b/src/engine.py @@ -1,8 +1,20 @@ from __future__ import annotations +from src.assessment_service import run_assessment_with_provenance from src.models import AssessmentResult, ProjectInput -from src.phases import run_llm_assessment -def run_assessment(project: ProjectInput, mode: str, audience: str) -> AssessmentResult: - return run_llm_assessment(project, mode, audience) +def run_assessment( + project: ProjectInput, + mode: str, + audience: str, + *, + require_llm: bool = False, +) -> AssessmentResult: + """Generate an assessment with explicit model/fallback provenance.""" + return run_assessment_with_provenance( + project, + mode, + audience, + require_llm=require_llm, + ) diff --git a/src/models.py b/src/models.py index f5dd570..5657285 100644 --- a/src/models.py +++ b/src/models.py @@ -42,3 +42,6 @@ class AssessmentResult: action_tracker: List[Dict[str, str]] project_memory: Dict[str, List[str]] role_summary: str + generation_mode: str = "unknown" + model_name: str | None = None + fallback_reason: str | None = None diff --git a/tests/test_assessment_provenance.py b/tests/test_assessment_provenance.py new file mode 100644 index 0000000..fc9edaf --- /dev/null +++ b/tests/test_assessment_provenance.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import pytest + +from src.assessment_service import AssessmentGenerationError +from src.engine import run_assessment +from src.models import ProjectInput + + +def project() -> ProjectInput: + return ProjectInput( + project_name="Fictional intake delay", + problem_statement="A fictional request-intake process has avoidable delay and rework.", + current_symptoms=["requests wait for review", "some requests are reworked"], + current_metrics={"cycle_time_days": "12"}, + constraints=["No production change during the fictional pilot"], + stakeholder_concerns=["Operations: reduce waiting time"], + ) + + +def test_missing_api_key_is_disclosed_as_deterministic_fallback(monkeypatch) -> None: + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + + result = run_assessment(project(), mode="dmaic", audience="pm") + + assert result.generation_mode == "deterministic_fallback" + assert result.model_name is None + assert result.fallback_reason + assert "No ANTHROPIC_API_KEY" in result.fallback_reason + assert result.role_summary.startswith("Generation note: deterministic fallback.") + + +def test_require_llm_fails_instead_of_silently_falling_back(monkeypatch) -> None: + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + + with pytest.raises(AssessmentGenerationError, match="No ANTHROPIC_API_KEY"): + run_assessment(project(), mode="dmaic", audience="pm", require_llm=True) diff --git a/tests/test_tool_recommender.py b/tests/test_tool_recommender.py new file mode 100644 index 0000000..98f3736 --- /dev/null +++ b/tests/test_tool_recommender.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from ui.tool_recommender import _recommendation + + +DEFAULTS = { + "problem_type": "Not sure — I just know something is wrong", + "data_availability": "Some data — a few weeks / small sample", + "scope": "End-to-end process (multiple steps or departments)", + "urgency": "Medium-term — 1-3 months project", + "measurement_confidence": "We measure but haven't validated the measurement system", + "root_cause_status": "Have some hunches but not confirmed", + "experience": "Yellow Belt — familiar with basics", +} + + +def recommend(**overrides): + values = {**DEFAULTS, **overrides} + return _recommendation(**values) + + +def test_known_solution_routes_to_control_plan() -> None: + recommendation = recommend( + root_cause_status="Root cause and solution are known — need to implement and sustain" + ) + + assert recommendation["tool"] == "Control Plan" + + +def test_data_rich_defect_problem_requires_msa_before_capability() -> None: + recommendation = recommend( + problem_type="Too many defects or errors", + data_availability="Good data — months of history, 30+ data points", + measurement_confidence="We measure but haven't validated the measurement system", + ) + + assert recommendation["tool"] == "MSA / Gauge R&R" + + +def test_validated_measurement_routes_data_rich_defects_to_capability() -> None: + recommendation = recommend( + problem_type="Too many defects or errors", + data_availability="Lots of data — automated / ongoing process data", + measurement_confidence="Measurement system is validated (MSA / Gauge R&R done)", + ) + + assert recommendation["tool"] == "Process Capability" + + +def test_limited_data_variation_routes_to_root_cause_hypotheses() -> None: + recommendation = recommend( + problem_type="Results are inconsistent / too much variation", + data_availability="None yet — haven't started measuring", + ) + + assert recommendation["tool"] == "Root Cause Analysis" + + +def test_urgent_unspecified_problem_routes_to_kaizen() -> None: + recommendation = recommend( + urgency="Immediate — something needs fixing this week" + ) + + assert recommendation["tool"] == "Kaizen / Rapid Improvement" diff --git a/ui/tool_recommender.py b/ui/tool_recommender.py index 3756dd0..12891eb 100644 --- a/ui/tool_recommender.py +++ b/ui/tool_recommender.py @@ -1,852 +1,321 @@ -""" -tool_recommender.py -=================== -Streamlit UI module providing a "Tool Recommender Wizard" for the -LLM-powered Lean Six Sigma application. - -The wizard presents 7 diagnostic questions, then recommends the best-fit -LSS tool or approach plus up to 3 supporting tools — with rationale, -required inputs, expected outputs, effort estimate, and cautions. +"""Interactive Lean Six Sigma tool recommender. -Public API ----------- -render_tool_recommender() -> None - Renders the entire tool recommender UI. +The module intentionally keeps recommendation logic separate from Streamlit +rendering so the decision rules can be tested without relying on UI state. """ + from __future__ import annotations -import streamlit as st +from typing import Any -# --------------------------------------------------------------------------- -# Colour palette — matches the app's global design system -# --------------------------------------------------------------------------- -_BLUE = "#4361EE" -_NAVY = "#1E1B4B" -_GREEN = "#06D6A0" -_AMBER = "#FFB703" -_RED = "#EF233C" -_GRAY = "#94A3B8" -_BG = "#F1F4FB" +import streamlit as st -# --------------------------------------------------------------------------- -# CSS injection -# --------------------------------------------------------------------------- -_CSS = """ - -""" - - -# --------------------------------------------------------------------------- -# Internal helpers -# --------------------------------------------------------------------------- -def _pill(text: str, bg: str, color: str = "#fff") -> str: - """Return an HTML span styled as a pill badge.""" - return ( - f'{text}' - ) - - -def _bullet_list_html(items: list[str], color: str = "#1E293B") -> str: - """Return an HTML unordered list from a list of strings.""" - lis = "".join( - f'
  • {item}
  • ' - for item in items - ) - return f'' - - -# --------------------------------------------------------------------------- -# Recommendation engine -# --------------------------------------------------------------------------- -def _compute_recommendation( - q1: str, q2: str, q3: str, q4: str, q5: str, q6: str, q7: str -) -> dict: - """Pure-Python decision tree that maps diagnostic answers to the best LSS tool. - Parameters - ---------- - q1 : Problem type - q2 : Data availability - q3 : Scope - q4 : Speed needed - q5 : Measurement system confidence - q6 : Root cause status - q7 : Team LSS experience level - Returns - ------- - dict with keys: primary_tool, primary_icon, primary_rationale, - what_you_need, what_you_get, supporting_tools, estimated_effort, - cautions, next_step_in_app, next_step_label. - """ - - # ------------------------------------------------------------------ - # Shared defaults for DMAIC (reused in several branches) - # ------------------------------------------------------------------ - def _dmaic() -> dict: - return { - "primary_tool": "DMAIC Project", - "primary_icon": "🔄", - "primary_rationale": ( - "With limited data, a structured DMAIC project is the right approach. " - "The Measure phase will build your data collection plan and validate the " - "measurement system before any analysis." - ), - "what_you_need": [ - "Problem statement with scope and baseline metric", - "Process map or SIPOC", - "Data collection plan", - "Stakeholder list", - ], - "what_you_get": [ - "Structured improvement roadmap", - "Root cause analysis", - "Prioritised improvement actions", - "Control plan", - ], - "supporting_tools": [ - { - "name": "FMEA", - "icon": "⚠️", - "why": "Risk-rank potential failure modes before selecting solutions", - }, - { - "name": "MSA / Gauge R&R", - "icon": "🎯", - "why": "Validate measurement system in the Measure phase", - }, - ], - "estimated_effort": "4-12 weeks Green Belt project", - "cautions": [ - "Don't skip Measure — jumping to solutions is the #1 cause of DMAIC failure", - "Define the problem scope before starting", - ], - "next_step_in_app": "wizard", - "next_step_label": "Open Project Wizard → select 'DMAIC' mode", - } - - # ------------------------------------------------------------------ - # Shared defaults for Lean Flow (reused for slow process AND waste) - # ------------------------------------------------------------------ - def _lean_flow() -> dict: - return { - "primary_tool": "Lean Flow / Value Stream Analysis", - "primary_icon": "🌊", - "primary_rationale": ( - "Speed problems are almost always caused by non-value-added steps, " - "excessive wait times, and poor flow. Value stream mapping surfaces the " - "waste and Little's Law quantifies WIP and throughput." - ), - "what_you_need": [ - "List of all process steps", - "Cycle time for each step", - "Wait/queue time between steps", - "Customer demand rate (units/day or week)", - ], - "what_you_get": [ - "Value stream map with VA vs NVA time", - "Process Cycle Efficiency (PCE%)", - "Bottleneck identification", - "Takt time vs cycle time comparison", - "Specific waste elimination recommendations", - ], - "supporting_tools": [ - { - "name": "SPC Charts", - "icon": "📈", - "why": "Monitor lead time and cycle time after improvement", - }, - { - "name": "Hypothesis Testing", - "icon": "🔬", - "why": "Confirm lead time reduction is statistically significant", - }, - ], - "estimated_effort": "1-3 weeks (mapping + analysis)", - "cautions": [ - "Map the current state honestly — don't map the ideal state", - "Include wait times — they're usually 60-90% of lead time", - ], - "next_step_in_app": "workbench", - "next_step_label": "Open Analytics Workbench → Lean Flow tab", - } - - # ------------------------------------------------------------------ - # Shared defaults for Control Plan (reused for two q6/q1 branches) - # ------------------------------------------------------------------ - def _control_plan() -> dict: - return { - "primary_tool": "Control Plan", - "primary_icon": "🛡️", - "primary_rationale": ( - "You've done the hard work — now the priority is sustaining gains. " - "A control plan with defined owners, SPC monitoring, and escalation " - "triggers prevents regression." - ), - "what_you_need": [ - "Validated improvement actions", - "Defined control metrics and limits", - "Process owners identified", - "Audit/review schedule", - ], - "what_you_get": [ - "Structured control plan document", - "SPC monitoring setup", - "Escalation triggers", - "Sustainability roadmap", - ], - "supporting_tools": [ - { - "name": "SPC Charts", - "icon": "📈", - "why": "Monitor key metrics in real-time for out-of-control signals", - }, - { - "name": "Hypothesis Testing", - "icon": "🔬", - "why": "Confirm the improvement is statistically significant before closing out", - }, - ], - "estimated_effort": "1-2 weeks", - "cautions": [ - "Ensure measurement system is validated before setting control limits", - "Control limits ≠ specification limits", - ], - "next_step_in_app": "wizard", - "next_step_label": "Open Project Wizard → select 'Control Plan' mode", - } - - # ------------------------------------------------------------------ - # Decision tree — highest-priority checks first - # ------------------------------------------------------------------ - - # Root cause + solution known → Control Plan - if q6 == "Root cause and solution are known — need to implement and sustain": - return _control_plan() - - # Explicit intent to sustain/control - if q1 == "Need to sustain / control recent gains": - return _control_plan() - - # Defect/error problem - if q1 == "Too many defects or errors": - if q2 in [ - "Good data — months of history, 30+ data points", - "Lots of data — automated / ongoing process data", - ]: - # Check measurement system first - if q5 != "Measurement system is validated (MSA / Gauge R&R done)": - return { - "primary_tool": "MSA / Gauge R&R", - "primary_icon": "🎯", - "primary_rationale": ( - "Before analysing defect data, validate that your measurement " - "system is capable. If Gauge R&R is poor (>30% GRR), your defect " - "data is unreliable. Fix the measurement before the process." - ), - "what_you_need": [ - "2-3 operators", - "10 representative parts/samples", - "Measurement procedure documented", - "Spec limits (USL/LSL)", - ], - "what_you_get": [ - "%GRR (target <10% for critical, <30% acceptable)", - "Number of distinct categories (NDC ≥ 5)", - "Repeatability vs reproducibility breakdown", - "Go/no-go on measurement system", - ], - "supporting_tools": [ - { - "name": "Process Capability", - "icon": "📊", - "why": ( - "Run after MSA passes — Cp/Cpk tells you if the " - "process itself is capable" - ), - }, - { - "name": "SPC Charts", - "icon": "📈", - "why": ( - "Set up ongoing monitoring once measurement system " - "is validated" - ), - }, - ], - "estimated_effort": "1-3 days for study + 1 day analysis", - "cautions": [ - "Study under real production conditions (not ideal)", - "Include all sources of variation (shifts, operators)", - ], - "next_step_in_app": "workbench", - "next_step_label": "Open Analytics Workbench → MSA / Gauge R&R tab", - } - else: - # MSA already done — go straight to capability - return { - "primary_tool": "Process Capability", - "primary_icon": "📊", - "primary_rationale": ( - "With validated data, process capability analysis tells you " - "exactly how your process compares to specification. Cp/Cpk " - "quantifies the gap and Sigma level shows you where you stand " - "vs world class." - ), - "what_you_need": [ - "30+ measurements from stable process", - "Spec limits (USL and/or LSL)", - "Validated measurement system", - ], - "what_you_get": [ - "Cp, Cpk (within-subgroup capability)", - "Pp, Ppk (overall performance)", - "Sigma level and DPMO", - "Capability histogram with normal overlay", - ], - "supporting_tools": [ - { - "name": "SPC Charts", - "icon": "📈", - "why": ( - "Check process stability before running capability — " - "unstable process = meaningless Cpk" - ), - }, - { - "name": "Hypothesis Testing", - "icon": "🔬", - "why": ( - "Compare before vs after capability to confirm " - "improvement is real" - ), - }, - ], - "estimated_effort": "1-2 days with existing data", - "cautions": [ - "Process must be stable (in statistical control) before calculating Cpk", - "Check normality — non-normal data needs transformation or non-parametric Ppk", - ], - "next_step_in_app": "workbench", - "next_step_label": "Open Analytics Workbench → Process Capability tab", - } - else: - # Limited data — start a DMAIC project - return _dmaic() - - # Lead time / speed problem - if q1 == "Process is too slow / long lead times": - return _lean_flow() - - # Variation / inconsistency - if q1 == "Results are inconsistent / too much variation": - if q2 in [ - "Good data — months of history, 30+ data points", - "Lots of data — automated / ongoing process data", - ]: - return { - "primary_tool": "SPC Charts", - "primary_icon": "📈", - "primary_rationale": ( - "For variation problems with good data, SPC charts reveal whether " - "variation is common cause (inherent to the process) or special cause " - "(assignable, fixable). This distinction drives the right response." - ), - "what_you_need": [ - "Time-ordered measurement data (30+ points)", - "Subgroup structure (if applicable)", - "Measurement system validated", - ], - "what_you_get": [ - "I-MR, Xbar-R, or p-chart depending on data type", - "Nelson rule violations flagged", - "Common cause vs special cause classification", - "Control limits (UCL/LCL)", - ], - "supporting_tools": [ - { - "name": "Process Capability", - "icon": "📊", - "why": "Quantify how much variation exceeds spec limits", - }, - { - "name": "Regression", - "icon": "📉", - "why": "Identify which input variables are driving the output variation", - }, - ], - "estimated_effort": "1-3 days with existing data", - "cautions": [ - "SPC requires time-ordered data — don't re-sort it", - "Control limits are calculated from the data — don't use spec limits as substitutes", - ], - "next_step_in_app": "workbench", - "next_step_label": "Open Analytics Workbench → SPC Charts tab", - } - else: - # Limited data — start with root cause analysis - return { - "primary_tool": "Root Cause Analysis", - "primary_icon": "🌿", - "primary_rationale": ( - "Without solid data, structured root cause analysis is the right " - "starting point. A 5 Whys chain and fishbone diagram will guide your " - "measurement plan so you collect the right data." - ), - "what_you_need": [ - "Clear problem statement with a specific metric", - "Process knowledge from operators", - "Any available defect/incident records", - ], - "what_you_get": [ - "5 Whys chain to verified root cause", - "Fishbone diagram (6Ms)", - "Prioritised hypotheses for validation", - "Targeted data collection plan", - ], - "supporting_tools": [ - { - "name": "SPC Charts", - "icon": "📈", - "why": "Set up monitoring once you know what to measure", - }, - { - "name": "Hypothesis Testing", - "icon": "🔬", - "why": "Statistically confirm root cause hypotheses", - }, - ], - "estimated_effort": "1-2 weeks", - "cautions": [ - "Stop at the first 'why' that can be validated with data", - "Avoid jumping to solutions during root cause analysis", - ], - "next_step_in_app": "wizard", - "next_step_label": "Open Project Wizard → select 'Root Cause' mode", - } - - # Waste identification - if q1 == "We know there's waste but can't pinpoint it": - return _lean_flow() +_DATA_RICH = { + "Good data — months of history, 30+ data points", + "Lots of data — automated / ongoing process data", +} +_ADVANCED_EXPERIENCE = { + "Green Belt — can run structured projects", + "Black Belt — full statistical toolkit", +} - # Understanding drivers of an outcome - if q1 == "Need to understand what's driving an outcome (Y)": - if q7 in [ - "Green Belt — can run structured projects", - "Black Belt — full statistical toolkit", - ]: - return { - "primary_tool": "Regression Analysis", - "primary_icon": "📉", - "primary_rationale": ( - "When you have a clear output variable (Y) and suspect multiple input " - "variables (Xs), regression quantifies each X's contribution, tests " - "significance, and identifies which inputs actually move the needle." - ), - "what_you_need": [ - "Dataset with Y column and at least 2-3 X columns", - "20+ data points (more = better)", - "X variables that can realistically be changed", - ], - "what_you_get": [ - "Regression equation (Y = b0 + b1X1 + ...)", - "R² and adjusted R²", - "Statistical significance (p-values) for each X", - "Diagnostics (VIF for multicollinearity, residual plots)", - ], - "supporting_tools": [ - { - "name": "DOE", - "icon": "🧪", - "why": ( - "Confirm cause-and-effect by deliberately varying Xs " - "in a controlled experiment" - ), - }, - { - "name": "Hypothesis Testing", - "icon": "🔬", - "why": ( - "Test individual X vs Y relationships before running " - "full regression" - ), - }, - ], - "estimated_effort": "2-5 days with existing data", - "cautions": [ - "Correlation ≠ causation — use DOE to confirm", - "Check multicollinearity (VIF > 5 is a warning sign)", - ], - "next_step_in_app": "workbench", - "next_step_label": "Open Analytics Workbench → Regression tab", - } - else: - return { - "primary_tool": "Hypothesis Testing", - "primary_icon": "🔬", - "primary_rationale": ( - "For teams newer to statistics, hypothesis testing is the right " - "starting point. It answers specific yes/no questions: 'Is there a " - "difference between shifts?' 'Did the improvement work?' — with " - "statistical confidence." - ), - "what_you_need": [ - "Two or more groups/conditions to compare", - "Measurement data for each group (15+ per group)", - "A specific question to answer", - ], - "what_you_get": [ - "p-value (< 0.05 = statistically significant)", - "Confidence intervals", - "Plain-English interpretation", - "Effect size (practical significance)", - ], - "supporting_tools": [ - { - "name": "Regression", - "icon": "📉", - "why": "Once you know which Xs matter, regression quantifies how much", - }, - { - "name": "SPC Charts", - "icon": "📈", - "why": "Monitor the key driver over time", - }, - ], - "estimated_effort": "1-2 days", - "cautions": [ - "Statistical significance ≠ practical significance — check effect size", - "Sample size matters — small samples can miss real differences", - ], - "next_step_in_app": "workbench", - "next_step_label": "Open Analytics Workbench → Hypothesis Testing tab", - } - # Proactive failure prevention - if q1 == "Need to prevent future failures": +def _recommendation( + *, + problem_type: str, + data_availability: str, + scope: str, + urgency: str, + measurement_confidence: str, + root_cause_status: str, + experience: str, +) -> dict[str, Any]: + """Return a bounded recommendation from the seven diagnostic inputs.""" + def result( + tool: str, + icon: str, + rationale: str, + inputs: list[str], + outputs: list[str], + cautions: list[str], + next_step: str, + effort: str, + supporting: list[str], + ) -> dict[str, Any]: return { - "primary_tool": "FMEA", - "primary_icon": "⚠️", - "primary_rationale": ( - "FMEA (Failure Mode & Effects Analysis) is the proactive tool of choice. " - "It systematically identifies what could go wrong, rates " - "severity/occurrence/detection, and prioritises risk reduction actions." - ), - "what_you_need": [ - "Process steps or product functions listed", - "Team with process knowledge (3-5 people)", - "Historical failure data if available (improves Occurrence ratings)", - ], - "what_you_get": [ - "Risk Priority Numbers (RPN = S×O×D) for each failure mode", - "Risk matrix (high/medium/low)", - "Pareto of top risks", - "Recommended actions with responsibility and timing", - ], - "supporting_tools": [ - { - "name": "DOE", - "icon": "🧪", - "why": "Validate that recommended actions actually reduce failure rates", - }, - { - "name": "SPC Charts", - "icon": "📈", - "why": "Monitor high-RPN items after control actions are in place", - }, - ], - "estimated_effort": "2-5 days (team workshop)", - "cautions": [ - "Don't do FMEA alone — you'll miss failure modes", - "Update ratings after implementing actions — re-score the FMEA", - ], - "next_step_in_app": "workbench", - "next_step_label": "Open Analytics Workbench → FMEA tab", + "tool": tool, + "icon": icon, + "rationale": rationale, + "inputs": inputs, + "outputs": outputs, + "cautions": cautions, + "next_step": next_step, + "effort": effort, + "supporting": supporting, } - # "Not sure" or any other / fallback - return _dmaic() - + if ( + root_cause_status + == "Root cause and solution are known — need to implement and sustain" + or problem_type == "Need to sustain / control recent gains" + ): + return result( + "Control Plan", + "🛡️", + "The immediate need is to sustain an implemented improvement through named owners, review cadence, monitored signals, and escalation triggers.", + ["validated improvement action", "process owner", "control metric", "review cadence"], + ["control plan", "owner and escalation map", "monitoring checklist"], + ["Control limits and specification limits are not the same.", "Validate the measurement system before relying on any threshold."], + "Open Project Wizard → select Control Plan mode.", + "1–2 weeks", + ["SPC Charts", "Hypothesis Testing"], + ) -# --------------------------------------------------------------------------- -# Public render function -# --------------------------------------------------------------------------- -def render_tool_recommender() -> None: - """Render the Tool Recommender Wizard UI. + if problem_type == "Too many defects or errors": + if data_availability in _DATA_RICH: + if measurement_confidence != "Measurement system is validated (MSA / Gauge R&R done)": + return result( + "MSA / Gauge R&R", + "🎯", + "Before using defect data to choose a solution, check whether the measurement method itself is consistent enough for the decision.", + ["representative parts or samples", "operators", "measurement procedure", "specification limits where available"], + ["measurement-system variation view", "repeatability/reproducibility signals", "next measurement action"], + ["Do not treat an unvalidated measurement method as ground truth.", "Run studies under realistic operating conditions."], + "Open Analytics Workbench → MSA / Gauge R&R.", + "1–3 days", + ["Process Capability", "SPC Charts"], + ) + return result( + "Process Capability", + "📊", + "You have meaningful data and a validated measurement system, so the next question is whether the process can meet its stated specification limits consistently.", + ["time-ordered measurements", "specification limits", "validated measurement method"], + ["capability indicators", "distribution view", "evidence for further investigation"], + ["Check stability before interpreting capability metrics.", "Non-normal data may need a different analysis approach."], + "Open Analytics Workbench → Process Capability.", + "1–2 days", + ["SPC Charts", "Root Cause Analysis"], + ) + return _dmaic_recommendation(result) + + if problem_type in { + "Process is too slow / long lead times", + "We know there's waste but can't pinpoint it", + }: + return result( + "Lean Flow / Value Stream Analysis", + "🌊", + "For flow and waste problems, map the real current process first, including queue and wait time, before selecting improvement actions.", + ["process steps", "cycle and wait times", "handoffs", "demand or workload pattern"], + ["current-state flow view", "bottleneck hypotheses", "waste-reduction opportunities"], + ["Map the actual current state, not the intended process.", "Do not assume a faster local step improves end-to-end flow."], + "Open Analytics Workbench → Lean Flow.", + "1–3 weeks", + ["Process Waste mode", "SPC Charts"], + ) - Presents 7 diagnostic questions and, on submission, computes and displays - a primary LSS tool recommendation plus up to 3 supporting tools. - """ - # Inject CSS - st.markdown(_CSS, unsafe_allow_html=True) + if problem_type == "Results are inconsistent / too much variation": + if data_availability in _DATA_RICH: + return result( + "SPC Charts", + "📈", + "Time-ordered data can reveal whether variation is a stable common-cause pattern or a special-cause signal that needs investigation.", + ["time-ordered measurements", "process context", "measurement confidence"], + ["stability signals", "candidate special causes", "monitoring baseline"], + ["Control limits are not specification limits.", "Do not reorder the time series before analysis."], + "Open Analytics Workbench → SPC Charts.", + "1–3 days", + ["Process Capability", "Regression"], + ) + return result( + "Root Cause Analysis", + "🌿", + "With limited data, start by structuring hypotheses and a targeted data-collection plan rather than making a confident causal claim.", + ["problem statement", "operator and stakeholder observations", "available incident records"], + ["5 Whys or fishbone draft", "testable hypotheses", "data-collection plan"], + ["Treat every proposed cause as a hypothesis until evidence supports it.", "Avoid choosing a solution during the first workshop."], + "Open Project Wizard → select Root Cause mode.", + "1–2 weeks", + ["Hypothesis Testing", "SPC Charts"], + ) - # ----------------------------------------------------------------------- - # Header - # ----------------------------------------------------------------------- - st.markdown( - f'

    🧭 What problem are you solving?

    ' - f'

    ' - f'Answer a few questions and we\'ll recommend the best approach for your situation.' - f'

    ', - unsafe_allow_html=True, - ) + if problem_type == "Need to understand what's driving an outcome (Y)": + if data_availability in _DATA_RICH and experience in _ADVANCED_EXPERIENCE: + return result( + "Regression Analysis", + "📉", + "A structured regression can help quantify associations between a defined outcome and candidate input variables when the data and assumptions are appropriate.", + ["outcome variable", "candidate input variables", "sufficient representative observations"], + ["association estimates", "diagnostic plots", "candidate drivers for follow-up"], + ["Association does not prove causation.", "Review model assumptions and multicollinearity before acting."], + "Open Analytics Workbench → Regression.", + "2–5 days", + ["DOE", "Hypothesis Testing"], + ) + return result( + "Hypothesis Testing", + "🔬", + "A focused test answers a narrow comparison question and is usually a clearer starting point than a broad model when experience or data are limited.", + ["specific comparison question", "defined groups or conditions", "measurement data"], + ["test result", "confidence interval", "plain-language interpretation"], + ["Statistical significance is not the same as practical importance.", "Confirm sample adequacy and measurement quality."], + "Open Analytics Workbench → Hypothesis Testing.", + "1–2 days", + ["Regression", "SPC Charts"], + ) - # ----------------------------------------------------------------------- - # Initialise session state keys (avoids KeyError on first run) - # ----------------------------------------------------------------------- -if "rec_submitted" not in st.session_state: - st.session_state["rec_submitted"] = False + if problem_type == "Need to prevent future failures": + return result( + "FMEA", + "⚠️", + "Use a structured failure-mode review to identify where the process can fail, prioritize prevention work, and assign actions before a problem recurs.", + ["process steps or functions", "cross-functional knowledge", "known failures or near misses"], + ["prioritized failure modes", "risk-reduction actions", "owner tracker"], + ["Numeric risk scores support prioritization but do not override a serious non-negotiable control gap.", "Re-score after mitigation evidence exists."], + "Open Analytics Workbench → FMEA.", + "1–2 weeks", + ["Control Plan", "Root Cause Analysis"], + ) - # ----------------------------------------------------------------------- - # Step 1 — Diagnostic questions (shown when not yet submitted) - # ----------------------------------------------------------------------- -if not st.session_state["rec_submitted"]: - st.radio( - "What kind of problem are you dealing with?", - options=[ - "Too many defects or errors", - "Process is too slow / long lead times", - "Results are inconsistent / too much variation", - "We know there's waste but can't pinpoint it", - "Need to understand what's driving an outcome (Y)", - "Need to prevent future failures", - "Need to sustain / control recent gains", - "Not sure — I just know something is wrong", - ], - key="rec_q1", - ) + if urgency == "Immediate — something needs fixing this week": + return result( + "Kaizen / Rapid Improvement", + "⚡", + "For an urgent but bounded problem, run a short improvement cycle with a clear safety boundary, a named owner, and a measurable before/after signal.", + ["narrow scope", "owner", "safe-to-test change", "baseline signal"], + ["quick-win plan", "review checkpoint", "evidence for next decision"], + ["Do not let urgency remove necessary safety, quality, or approval controls.", "Escalate broader structural issues into a DMAIC project."], + "Open Project Wizard → select Kaizen mode.", + "Days to 2 weeks", + ["Process Waste mode", "Control Plan"], + ) - st.radio( - "How much data do you have?", - options=[ - "None yet — haven't started measuring", - "Some data — a few weeks / small sample", - "Good data — months of history, 30+ data points", - "Lots of data — automated / ongoing process data", - ], - key="rec_q2", - ) + return _dmaic_recommendation(result) - st.radio( - "What is the scope of the problem?", - options=[ - "Single machine, station, or step", - "End-to-end process (multiple steps or departments)", - "Product or service line", - "Organisation-wide", - ], - key="rec_q3", - ) - st.radio( - "How urgently do you need a result?", - options=[ - "Immediate — something needs fixing this week", - "Short-term — 1-4 weeks", - "Medium-term — 1-3 months project", - "Long-term — formal improvement programme", - ], - key="rec_q4", +def _dmaic_recommendation(result_builder) -> dict[str, Any]: + return result_builder( + "DMAIC Project", + "🔄", + "A structured DMAIC path is the safest default when the problem is unclear, data is incomplete, or the team needs a disciplined route from problem framing through control.", + ["clear problem statement", "initial stakeholder concerns", "available process data", "scope and constraints"], + ["Define–Measure–Analyze–Improve–Control roadmap", "evidence gaps", "hypotheses and actions", "control plan draft"], + ["Do not jump from symptoms to solutions.", "Validate measurements and root-cause hypotheses before broad rollout."], + "Open Project Wizard → select DMAIC mode.", + "4–12 weeks depending on scope", + ["SIPOC", "FMEA", "MSA / Gauge R&R"], ) - st.radio( - "How confident are you in your measurement system?", - options=[ - "We have no formal measurement", - "We measure but haven't validated the measurement system", - "We've done a basic gauge check", - "Measurement system is validated (MSA / Gauge R&R done)", - ], - key="rec_q5", - ) - st.radio( - "Do you know the root cause?", - options=[ - "No idea — haven't investigated yet", - "Have some hunches but not confirmed", - "Root cause is known but solution is unclear", - "Root cause and solution are known — need to implement and sustain", - ], - key="rec_q6", - ) +def _reset() -> None: + for key in ["rec_submitted", *[f"rec_{name}" for name in QUESTION_OPTIONS]]: + st.session_state.pop(key, None) - st.radio( - "What is your team's LSS experience level?", - options=[ - "No prior LSS training", - "Yellow Belt — familiar with basics", - "Green Belt — can run structured projects", - "Black Belt — full statistical toolkit", - ], - key="rec_q7", - ) - st.divider() - if st.button("🔍 Find my best approach →", type="primary"): +def render_tool_recommender() -> None: + """Render the seven-question recommendation workflow.""" + st.markdown("## 🧭 Lean Six Sigma Tool Recommender") + st.caption("A structured starting point, not a substitute for statistical, quality, safety, or domain review.") + + st.session_state.setdefault("rec_submitted", False) + if st.session_state["rec_submitted"]: + if st.button("Start a new recommendation", key="rec_reset"): + _reset() + st.rerun() + else: + with st.form("tool_recommender_form"): + q1 = st.radio("What kind of problem are you dealing with?", QUESTION_OPTIONS["q1"], key="rec_q1") + q2 = st.radio("How much data do you have?", QUESTION_OPTIONS["q2"], key="rec_q2") + q3 = st.radio("What is the scope of the problem?", QUESTION_OPTIONS["q3"], key="rec_q3") + q4 = st.radio("How urgently do you need a result?", QUESTION_OPTIONS["q4"], key="rec_q4") + q5 = st.radio("How confident are you in your measurement system?", QUESTION_OPTIONS["q5"], key="rec_q5") + q6 = st.radio("Do you know the root cause?", QUESTION_OPTIONS["q6"], key="rec_q6") + q7 = st.radio("What is your team's LSS experience level?", QUESTION_OPTIONS["q7"], key="rec_q7") + submitted = st.form_submit_button("Find my best approach", type="primary") + if submitted: st.session_state["rec_submitted"] = True st.rerun() - - return # Don't render the output section until submitted - - # ----------------------------------------------------------------------- - # Step 2 — Show recommendation - # ----------------------------------------------------------------------- - q1 = st.session_state.get("rec_q1", "") - q2 = st.session_state.get("rec_q2", "") - q3 = st.session_state.get("rec_q3", "") - q4 = st.session_state.get("rec_q4", "") - q5 = st.session_state.get("rec_q5", "") - q6 = st.session_state.get("rec_q6", "") - q7 = st.session_state.get("rec_q7", "") - - rec = _compute_recommendation(q1, q2, q3, q4, q5, q6, q7) - - # ----------------------------------------------------------------------- - # Primary recommendation card (HTML) - # ----------------------------------------------------------------------- - need_html = _bullet_list_html(rec["what_you_need"]) - get_html = _bullet_list_html(rec["what_you_get"]) - - card_html = ( - f'
    ' - f'

    {rec["primary_icon"]} {rec["primary_tool"]}

    ' - f'

    {rec["primary_rationale"]}

    ' - f'
    ' - # Left column — what you need - f'
    ' - f'

    ' - f'📋 What you need

    ' - f'{need_html}' - f'
    ' - # Right column — what you get - f'
    ' - f'

    ' - f'✅ What you get

    ' - f'{get_html}' - f'
    ' - f'
    ' - # Effort badge - f'
    ' - f'⏱ Estimated effort: {rec["estimated_effort"]}' - f'
    ' - f'
    ' + return + + recommendation = _recommendation( + problem_type=st.session_state["rec_q1"], + data_availability=st.session_state["rec_q2"], + scope=st.session_state["rec_q3"], + urgency=st.session_state["rec_q4"], + measurement_confidence=st.session_state["rec_q5"], + root_cause_status=st.session_state["rec_q6"], + experience=st.session_state["rec_q7"], ) - st.markdown(card_html, unsafe_allow_html=True) - - # ----------------------------------------------------------------------- - # "Open in App" button - # ----------------------------------------------------------------------- - if st.button( - f'Open in App → {rec["next_step_label"]}', - type="primary", - key="rec_open_in_app", - ): - st.session_state["app_mode"] = rec["next_step_in_app"] - st.rerun() - - st.markdown("
    ", unsafe_allow_html=True) - - # ----------------------------------------------------------------------- - # Supporting tools section - # ----------------------------------------------------------------------- - supporting = rec.get("supporting_tools", []) - if supporting: - st.markdown( - f'

    ' - f'🔧 Supporting tools to consider

    ', - unsafe_allow_html=True, - ) - - cols = st.columns(len(supporting)) - for col, tool in zip(cols, supporting): - with col: - col.markdown( - f'
    ' - f'

    {tool["icon"]}

    ' - f'

    {tool["name"]}

    ' - f'

    {tool["why"]}

    ' - f'
    ', - unsafe_allow_html=True, - ) - - st.markdown("
    ", unsafe_allow_html=True) - - # ----------------------------------------------------------------------- - # Cautions section — amber callout - # ----------------------------------------------------------------------- - cautions = rec.get("cautions", []) - if cautions: - caution_items = "".join( - f'
  • {c}
  • ' - for c in cautions - ) - st.markdown( - f'
    ' - f'

    ' - f'⚠️ Things to check first

    ' - f'' - f'
    ', - unsafe_allow_html=True, - ) - - st.markdown("
    ", unsafe_allow_html=True) - # ----------------------------------------------------------------------- - # Start over button - # ----------------------------------------------------------------------- - if st.button("↩ Start over", key="rec_start_over"): - st.session_state["rec_submitted"] = False - st.rerun() + st.success(f"Recommended starting point: {recommendation['icon']} {recommendation['tool']}") + st.write(recommendation["rationale"]) + st.caption(f"Typical effort: {recommendation['effort']}") + + left, right = st.columns(2) + with left: + st.markdown("#### What you need") + for item in recommendation["inputs"]: + st.markdown(f"- {item}") + st.markdown("#### What you get") + for item in recommendation["outputs"]: + st.markdown(f"- {item}") + with right: + st.markdown("#### Supporting tools") + for item in recommendation["supporting"]: + st.markdown(f"- {item}") + st.markdown("#### Cautions") + for item in recommendation["cautions"]: + st.warning(item) + + st.info(recommendation["next_step"])