From 3aebc464e246ce4d9b2474acf44f4bbd831a744d Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:31:43 +0800 Subject: [PATCH 01/16] feat: record assessment generation provenance --- src/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models.py b/src/models.py index f5dd570..5fda5b1 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 = "deterministic_fallback" + model_name: str | None = None + fallback_reason: str | None = None From bf6747474ef6e565d9e782c64d68feba787a03e0 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:32:07 +0800 Subject: [PATCH 02/16] revert: defer provenance fields until generation path is updated --- src/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/models.py b/src/models.py index 5fda5b1..f5dd570 100644 --- a/src/models.py +++ b/src/models.py @@ -42,6 +42,3 @@ class AssessmentResult: action_tracker: List[Dict[str, str]] project_memory: Dict[str, List[str]] role_summary: str - generation_mode: str = "deterministic_fallback" - model_name: str | None = None - fallback_reason: str | None = None From 65f7f12f8920878322b1ac19f59a4fd6d6e977cb Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:48:51 +0800 Subject: [PATCH 03/16] feat: capture assessment generation provenance --- src/models.py | 3 +++ 1 file changed, 3 insertions(+) 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 From d5d0f1e24c9c9065e267a1b3cb93631eafd4e6f8 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:49:11 +0800 Subject: [PATCH 04/16] feat: make model and fallback provenance explicit --- src/assessment_service.py | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/assessment_service.py diff --git a/src/assessment_service.py b/src/assessment_service.py new file mode 100644 index 0000000..0c95aeb --- /dev/null +++ b/src/assessment_service.py @@ -0,0 +1,114 @@ +"""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 _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, + 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=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) From 9c296af084abffd1ab7f842399b87d259594a8b5 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:49:34 +0800 Subject: [PATCH 05/16] feat: route assessments through provenance-aware orchestration --- src/engine.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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, + ) From 27da10e2d301534d771ccc0b8a89053b4f4c71a4 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:49:49 +0800 Subject: [PATCH 06/16] feat: expose strict live-model mode in CLI demo --- run_demo.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/run_demo.py b/run_demo.py index 9d2cd70..155644b 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 @@ -29,10 +30,24 @@ 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(render_markdown_summary(result)) From 8b2ffbfe4310c5f21f71f2279150a98759caaa47 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:50:40 +0800 Subject: [PATCH 07/16] docs: show generation provenance in CLI output --- run_demo.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/run_demo.py b/run_demo.py index 155644b..2e1f0ae 100644 --- a/run_demo.py +++ b/run_demo.py @@ -15,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") @@ -48,6 +55,8 @@ def main() -> None: except AssessmentGenerationError as exc: parser.error(str(exc)) + print(_provenance_line(result)) + print() print(render_markdown_summary(result)) From 0d02bc8a8e71a0fdf3bc78144917cf2904e48f64 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:51:15 +0800 Subject: [PATCH 08/16] test: make deterministic fallback explicit --- tests/test_assessment_provenance.py | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/test_assessment_provenance.py diff --git a/tests/test_assessment_provenance.py b/tests/test_assessment_provenance.py new file mode 100644 index 0000000..6c452b7 --- /dev/null +++ b/tests/test_assessment_provenance.py @@ -0,0 +1,36 @@ +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 + + +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) From 16c06e9a54b06588b83fbc40c5813a46873b9b6f Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:51:45 +0800 Subject: [PATCH 09/16] docs: explain live-model and fallback generation modes --- docs/assessment-generation.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/assessment-generation.md 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. From 41bab58ecde8099777e8395c4c7892dea6d650f1 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:52:33 +0800 Subject: [PATCH 10/16] feat: surface generation provenance in role summaries --- src/assessment_service.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/assessment_service.py b/src/assessment_service.py index 0c95aeb..288267a 100644 --- a/src/assessment_service.py +++ b/src/assessment_service.py @@ -24,6 +24,14 @@ 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, @@ -34,6 +42,7 @@ def _fallback( 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, @@ -102,7 +111,7 @@ def run_assessment_with_provenance( control_plan=_parse_items(data["control_plan"]), action_tracker=data["action_tracker"], project_memory=data["project_memory"], - role_summary=data["role_summary"], + role_summary=f"{_generation_note('llm')}\n\n{data['role_summary']}", generation_mode="llm", model_name=MODEL_NAME, fallback_reason=None, From bf5a1fe5f82a79cd1c674be02aa0816105080bbf Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:53:26 +0800 Subject: [PATCH 11/16] test: keep fallback provenance visible in user summaries --- tests/test_assessment_provenance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_assessment_provenance.py b/tests/test_assessment_provenance.py index 6c452b7..fc9edaf 100644 --- a/tests/test_assessment_provenance.py +++ b/tests/test_assessment_provenance.py @@ -27,6 +27,7 @@ def test_missing_api_key_is_disclosed_as_deterministic_fallback(monkeypatch) -> 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: From cb010351eb6ffa584a9c93b0f0169bab83be6e32 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:54:46 +0800 Subject: [PATCH 12/16] chore: add dependency update automation --- .github/dependabot.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/dependabot.yml 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 From f034d982c5edd886d3a4aa548b945fb8a4f6cc13 Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:01:54 +0800 Subject: [PATCH 13/16] ci: capture lint diagnostics for review --- .github/workflows/diagnose-lint.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/diagnose-lint.yml diff --git a/.github/workflows/diagnose-lint.yml b/.github/workflows/diagnose-lint.yml new file mode 100644 index 0000000..6a645bc --- /dev/null +++ b/.github/workflows/diagnose-lint.yml @@ -0,0 +1,24 @@ +name: Diagnose lint + +on: + pull_request: + +permissions: + contents: read + +jobs: + diagnose: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: | + python -m pip install --upgrade pip flake8 + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics > flake8-diagnostics.txt 2>&1 || true + - uses: actions/upload-artifact@v4 + with: + name: flake8-diagnostics + path: flake8-diagnostics.txt + retention-days: 1 From f62a06f4809d51dc5e4928502d3ff085ab91812b Mon Sep 17 00:00:00 2001 From: Sima Bagheri <37793675+simaba@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:04:06 +0800 Subject: [PATCH 14/16] fix: replace broken recommender with testable wizard --- ui/tool_recommender.py | 1109 +++++++++++----------------------------- 1 file changed, 289 insertions(+), 820 deletions(-) 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'
' - 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'{rec["primary_rationale"]}
' - f'' - f'📋 What you need
' - f'{need_html}' - f'' - f'✅ What you get
' - f'{get_html}' - 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'{tool["icon"]}
' - f'{tool["name"]}
' - f'{tool["why"]}
' - f'' - f'⚠️ Things to check first
' - f'