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"])