Spaces:
Running on Zero
Running on Zero
| """ | |
| Smart Warehouse AI Assistant | |
| ============================= | |
| A Daifuku-style intralogistics AI copilot demo, built for a Hugging Face | |
| Space. Combines: | |
| 1. An LLM-powered assistant (RAG: TF-IDF retrieval + hosted LLM via the | |
| HF Inference API) for natural-language warehouse operations Q&A. | |
| 2. A TF-IDF + Logistic Regression intent classifier that routes queries | |
| into 8 operational categories. | |
| 3. A lightweight NL -> structured query layer over synthetic inventory / | |
| order tables. | |
| 4. An Isolation Forest anomaly detector for conveyor/crane sensor | |
| streams (predictive maintenance). | |
| 5. A Model Evaluation tab reporting real accuracy/F1/ROC-AUC metrics | |
| computed by build_artifacts.py. | |
| Author: (your name here) -- built as an application project for Daifuku Co., Ltd. | |
| """ | |
| import json | |
| import os | |
| import gradio as gr | |
| import pandas as pd | |
| from src.anomaly_model import FEATURES as SENSOR_FEATURES | |
| from src.anomaly_model import load_artifacts as load_anomaly_artifacts | |
| from src.anomaly_model import score_reading | |
| from src.data_generation import generate_inventory_db, generate_orders_db | |
| from src.intent_model import INTENT_DESCRIPTIONS, load_pipeline, predict as intent_predict | |
| from src.inventory_db import query_inventory, query_orders | |
| from src.llm_client import answer_query, test_connection, _get_hf_token, TOKEN_ENV_VAR_CANDIDATES | |
| from src.retriever import KBRetriever | |
| ROOT = os.path.dirname(os.path.abspath(__file__)) | |
| MODELS_DIR = os.path.join(ROOT, "models") | |
| DATA_DIR = os.path.join(ROOT, "data") | |
| ASSETS_DIR = os.path.join(ROOT, "assets") | |
| # -------------------------------------------------------------------------- | |
| # Load pre-trained artifacts (fast: no training happens at Space startup | |
| # under normal conditions). If unpickling fails -- e.g. the models were | |
| # built with a different scikit-learn version than the one installed in | |
| # this container -- we transparently rebuild everything from scratch. This | |
| # is fast (a few seconds, see build_artifacts.py) and makes the app immune | |
| # to sklearn version-pinning mismatches between build time and deploy time. | |
| # -------------------------------------------------------------------------- | |
| def _load_or_rebuild_artifacts(): | |
| intent_path = os.path.join(MODELS_DIR, "intent_pipeline.joblib") | |
| anomaly_model_path = os.path.join(MODELS_DIR, "anomaly_iforest.joblib") | |
| anomaly_scaler_path = os.path.join(MODELS_DIR, "anomaly_scaler.joblib") | |
| def _try_load(): | |
| pipeline = load_pipeline(intent_path) | |
| model, scaler = load_anomaly_artifacts(anomaly_model_path, anomaly_scaler_path) | |
| # Smoke-test the loaded pipeline against the exact code path the app | |
| # uses at request time. On a scikit-learn version mismatch, sklearn | |
| # sometimes unpickles "successfully" but throws later on first real | |
| # use (e.g. LogisticRegression missing an internal attribute) -- | |
| # catching that here, at import time, is what makes this self-healing. | |
| intent_predict(pipeline, "healthcheck") | |
| score_reading(model, scaler, {"motor_temp_c": 55, "vibration_mm_s": 2.2, "current_amps": 12, "belt_speed_mps": 1.5}) | |
| return pipeline, model, scaler | |
| try: | |
| return _try_load() | |
| except Exception as e: # noqa: BLE001 -- any load/version issue triggers a rebuild | |
| print(f"[startup] Could not load pre-built model artifacts ({type(e).__name__}: {e}). " | |
| f"Rebuilding from scratch with the installed scikit-learn version...") | |
| import build_artifacts | |
| build_artifacts.build_intent_classifier() | |
| build_artifacts.build_anomaly_detector() | |
| build_artifacts.build_retrieval_eval() | |
| build_artifacts.build_inventory_and_orders() | |
| print("[startup] Rebuild complete.") | |
| return _try_load() | |
| intent_pipeline, anomaly_model, anomaly_scaler = _load_or_rebuild_artifacts() | |
| retriever = KBRetriever() | |
| # Prefer the pre-generated CSVs (so demo state matches the eval run); fall back to | |
| # regenerating in-memory if they're missing for some reason. | |
| try: | |
| inventory_df = pd.read_csv(os.path.join(DATA_DIR, "inventory.csv")) | |
| orders_df = pd.read_csv(os.path.join(DATA_DIR, "orders.csv")) | |
| except FileNotFoundError: | |
| inventory_df = generate_inventory_db() | |
| orders_df = generate_orders_db() | |
| def _load_json(name): | |
| path = os.path.join(DATA_DIR, name) | |
| if os.path.exists(path): | |
| with open(path) as f: | |
| return json.load(f) | |
| return {} | |
| intent_eval = _load_json("intent_eval.json") | |
| anomaly_eval = _load_json("anomaly_eval.json") | |
| retrieval_eval = _load_json("retrieval_eval.json") | |
| latency_eval = _load_json("latency_eval.json") | |
| HF_TOKEN_SET = bool(_get_hf_token()) | |
| # -------------------------------------------------------------------------- | |
| # ZeroGPU compatibility shim | |
| # -------------------------------------------------------------------------- | |
| # This app is CPU-only by design (scikit-learn locally, LLM calls go to the | |
| # remote HF Inference API). Some Spaces accounts, however, only offer the | |
| # free "ZeroGPU" hardware tier, which requires at least one function | |
| # decorated with `@spaces.GPU` to be present or the platform's startup | |
| # check fails with "No @spaces.GPU function detected". This is a harmless, | |
| # unused health-check function that satisfies that requirement without | |
| # changing any real behaviour -- it is never called on the request path. | |
| try: | |
| import spaces | |
| def _zerogpu_healthcheck(): | |
| return True | |
| except ImportError: | |
| # Running locally / on CPU-basic hardware where `spaces` isn't installed. | |
| def _zerogpu_healthcheck(): | |
| return True | |
| # ========================================================================== | |
| # TAB 1 -- AI Assistant (LLM + RAG + intent routing) | |
| # ========================================================================== | |
| def chat_fn(message, history): | |
| intent = intent_predict(intent_pipeline, message) | |
| response = answer_query(message, retriever) | |
| intent_label = INTENT_DESCRIPTIONS.get(intent.intent, intent.intent) | |
| meta_lines = [f"**Detected intent:** {intent_label} ({intent.confidence:.0%} confidence)"] | |
| if response.sources: | |
| src_str = ", ".join(f"{s.title} ({s.score:.2f})" for s in response.sources) | |
| meta_lines.append(f"**Retrieved context:** {src_str}") | |
| meta_lines.append( | |
| f"**Generation:** {'LLM (' + response.model_id + ')' if response.used_llm else 'retrieval-only fallback'}" | |
| f" · {response.latency_s * 1000:.0f} ms" | |
| ) | |
| if not response.used_llm and response.debug_errors: | |
| err_lines = "\n".join(f" - `{e}`" for e in response.debug_errors) | |
| meta_lines.append(f"**Why the LLM wasn't used:**\n{err_lines}") | |
| full_reply = response.answer + "\n\n---\n" + "\n".join(meta_lines) | |
| return full_reply | |
| def test_llm_fn(): | |
| response = test_connection(retriever) | |
| if response.used_llm: | |
| return ( | |
| f"✅ **LLM connection working.** Model: `{response.model_id}` · " | |
| f"{response.latency_s * 1000:.0f} ms\n\nSample answer: {response.answer}" | |
| ) | |
| err_lines = "\n".join(f"- `{e}`" for e in response.debug_errors) or "(no error detail captured)" | |
| return ( | |
| "❌ **LLM connection failed** — running in retrieval-only fallback mode.\n\n" | |
| f"**Errors from each candidate model tried:**\n{err_lines}\n\n" | |
| "**Common causes:** missing/invalid `HF_TOKEN` secret, the token's account " | |
| "lacking Inference API access, or the candidate models being temporarily " | |
| "unavailable on HF's free serverless tier. See `src/llm_client.py` to add " | |
| "or reorder candidate models, or set the `LLM_MODEL_ID` secret to force a " | |
| "specific one." | |
| ) | |
| ASSISTANT_EXAMPLES = [ | |
| "The conveyor belt in Zone C is making noise", | |
| "How many units of SKU-1042 are in Zone B?", | |
| "What's the difference between an AGV and an AMR?", | |
| "A forklift near-miss was reported in Zone A", | |
| "What's the fastest picking route for a high volume order?", | |
| "Is Crane-03 operational?", | |
| ] | |
| # ========================================================================== | |
| # TAB 2 -- Inventory & Order Query | |
| # ========================================================================== | |
| def inventory_query_fn(text): | |
| intent = intent_predict(intent_pipeline, text) | |
| if intent.intent == "order_status": | |
| result = query_orders(orders_df, text) | |
| note = "Interpreted as an **order status** query." | |
| else: | |
| result = query_inventory(inventory_df, text) | |
| note = "Interpreted as an **inventory** query." | |
| if result.empty: | |
| result = pd.DataFrame({"message": ["No matching records found for this query."]}) | |
| return note, result | |
| # ========================================================================== | |
| # TAB 3 -- Predictive Maintenance / Anomaly Detection | |
| # ========================================================================== | |
| def anomaly_fn(motor_temp, vibration, current, belt_speed): | |
| reading = { | |
| "motor_temp_c": motor_temp, | |
| "vibration_mm_s": vibration, | |
| "current_amps": current, | |
| "belt_speed_mps": belt_speed, | |
| } | |
| result = score_reading(anomaly_model, anomaly_scaler, reading) | |
| verdict = "🔴 ANOMALY DETECTED" if result.is_anomaly else "🟢 Normal operating range" | |
| detail = ( | |
| f"### {verdict}\n\n" | |
| f"**Anomaly score:** {result.anomaly_score:.2f} / 1.00\n\n" | |
| f"| Sensor | Value | Typical normal range |\n" | |
| f"|---|---|---|\n" | |
| f"| Motor temperature | {motor_temp:.1f} °C | 47–63 °C |\n" | |
| f"| Vibration | {vibration:.2f} mm/s | 1.2–3.2 mm/s |\n" | |
| f"| Motor current | {current:.1f} A | 9–15 A |\n" | |
| f"| Belt speed | {belt_speed:.2f} m/s | 1.2–1.8 m/s |\n" | |
| ) | |
| if result.is_anomaly: | |
| detail += ( | |
| "\n**Recommended action:** Flag for inspection. Elevated temperature + " | |
| "vibration + current with reduced belt speed typically indicates bearing " | |
| "wear, belt misalignment, or motor overload -- schedule maintenance before " | |
| "the next shift to avoid an unplanned stoppage." | |
| ) | |
| return detail | |
| ANOMALY_PRESETS = { | |
| "Normal reading": (55.0, 2.2, 12.0, 1.5), | |
| "Early bearing wear": (68.0, 3.8, 15.5, 1.3), | |
| "Severe fault (imminent failure)": (85.0, 6.5, 21.0, 0.6), | |
| } | |
| def load_preset(name): | |
| return ANOMALY_PRESETS[name] | |
| # ========================================================================== | |
| # TAB 4 -- Model Evaluation | |
| # ========================================================================== | |
| def eval_intent_section_md(): | |
| cls_report = intent_eval.get("classification_report", {}) | |
| per_class_rows = [] | |
| for cls in intent_eval.get("classes", []): | |
| stats = cls_report.get(cls, {}) | |
| per_class_rows.append( | |
| f"| {cls} | {stats.get('precision', 0):.2f} | {stats.get('recall', 0):.2f} | " | |
| f"{stats.get('f1-score', 0):.2f} | {int(stats.get('support', 0))} |" | |
| ) | |
| per_class_table = "\n".join(per_class_rows) | |
| return f""" | |
| ## 1. Intent Classifier (TF-IDF + Logistic Regression) | |
| **What it does:** routes a free-text query (e.g. *"The conveyor belt in Zone C | |
| is making noise"*) into one of 8 operational categories, used by both the | |
| AI Assistant and Inventory & Order Query tabs to decide how to handle a | |
| request. | |
| **Training data:** {intent_eval.get('n_train', '?') } synthetically generated | |
| example queries (see chart below), built from ~8 hand-written templates per | |
| category with randomised SKU codes, zone names, order IDs, and equipment IDs | |
| slotted in -- e.g. *"How many units of {{sku}} are in {{zone}}?"*. This keeps | |
| the language varied while being fully reproducible (`src/data_generation.py`). | |
| Evaluated on a **held-out stratified test split** of {intent_eval.get('n_test', '?')} | |
| examples the model never saw during training. | |
| | Metric | Score | | |
| |---|---| | |
| | **Accuracy** | **{intent_eval.get('accuracy', 0):.2%}** | | |
| | **Macro F1** | **{intent_eval.get('macro_f1', 0):.2%}** | | |
| **Per-class performance:** | |
| | Intent | Precision | Recall | F1 | Support | | |
| |---|---|---|---|---| | |
| {per_class_table} | |
| """ | |
| def eval_anomaly_section_md(): | |
| return f""" | |
| ## 2. Predictive Maintenance Anomaly Detector (Isolation Forest) | |
| **What it does:** flags abnormal conveyor/crane motor sensor readings | |
| (temperature, vibration, current, belt speed) before they cause an | |
| unplanned stoppage -- powers the Predictive Maintenance tab. | |
| **Training data:** {900 + 100} synthetic sensor readings (900 "normal" | |
| + 100 "anomaly" patterns), each with 4 features. Normal readings are drawn | |
| from realistic operating ranges (e.g. ~55°C motor temp, ~2.2 mm/s vibration); | |
| anomalies simulate bearing wear / misalignment / overload (elevated temp, | |
| vibration, and current with reduced belt speed). See the distribution chart | |
| below for exactly how these two classes differ. | |
| The model itself is trained **unsupervised** (Isolation Forest never sees | |
| the anomaly label during fitting) -- labels are used only to *evaluate* it | |
| afterward, on a held-out test split of {anomaly_eval.get('n_test', '?')} | |
| readings ({anomaly_eval.get('test_anomaly_rate', 0):.1%} true anomaly rate). | |
| | Metric | Score | | |
| |---|---| | |
| | **Precision** | **{anomaly_eval.get('precision', 0):.2%}** | | |
| | **Recall** | **{anomaly_eval.get('recall', 0):.2%}** | | |
| | **F1 Score** | **{anomaly_eval.get('f1', 0):.2%}** | | |
| | **ROC-AUC** | **{anomaly_eval.get('roc_auc', 0):.3f}** | | |
| | Accuracy | {anomaly_eval.get('accuracy', 0):.2%} | | |
| """ | |
| def eval_retrieval_section_md(): | |
| retrieval_rows = "\n".join( | |
| f"| {r['query']} | {r['expected']} | {r['retrieved_top1']} | " | |
| f"{'✅' if r['hit@1'] else ('〰️' if r['hit@2'] else '❌')} | {r['top1_score']:.2f} |" | |
| for r in retrieval_eval.get("rows", []) | |
| ) | |
| return f""" | |
| ## 3. Retrieval (RAG) Evaluation | |
| **What it does:** before the LLM answers a question, this component finds | |
| the most relevant passages from a 10-article warehouse-operations knowledge | |
| base (AS/RS, AGV/AMR, WMS, sortation, picking strategy, safety, etc. -- see | |
| `src/knowledge_base.py`) using TF-IDF + cosine similarity, so the LLM answers | |
| from real context rather than guessing. | |
| **Evaluation data:** {retrieval_eval.get('n_queries', '?')} hand-labelled | |
| (query, expected-article) pairs -- a small ground-truth set built by hand to | |
| check the retriever finds the *right* article, not just *an* article. | |
| | Metric | Score | | |
| |---|---| | |
| | **Hit Rate @ 1** | **{retrieval_eval.get('hit_rate_at_1', 0):.0%}** | | |
| | **Hit Rate @ 2** | **{retrieval_eval.get('hit_rate_at_2', 0):.0%}** | | |
| | Query | Expected Doc | Retrieved (top-1) | Hit | Score | | |
| |---|---|---|---|---| | |
| {retrieval_rows} | |
| """ | |
| def eval_latency_section_md(): | |
| return f""" | |
| ## 4. Latency Benchmark (per-request, CPU) | |
| Average of 50 runs each, measured on the same CPU hardware the Space runs on. | |
| | Component | Avg. latency | | |
| |---|---| | |
| | Intent classification | {latency_eval.get('intent_classifier_ms', '?')} ms | | |
| | Anomaly scoring | {latency_eval.get('anomaly_detector_ms', '?')} ms | | |
| | KB retrieval (TF-IDF) | {latency_eval.get('kb_retrieval_ms', '?')} ms | | |
| | LLM generation | Depends on the hosted Inference API (measured live per-request in the Assistant tab, not benchmarked here) | | |
| """ | |
| EVAL_METHODOLOGY_MD = """ | |
| ### Evaluation methodology notes | |
| - All datasets are **synthetically generated** (see `src/data_generation.py`) using | |
| templated-but-varied natural language and randomised sensor distributions with a | |
| fixed seed, so results are fully reproducible via `python build_artifacts.py`. | |
| - The intent classifier and anomaly detector are evaluated on a **held-out test | |
| split** they never saw during training (stratified, 25% / 30% respectively). | |
| - The anomaly detector itself is trained **unsupervised** (Isolation Forest never | |
| sees the `label` column during `.fit()`); labels are used only to *evaluate* it, | |
| mirroring how you'd validate an anomaly model against a small set of confirmed | |
| historical incidents in production. | |
| - In a production deployment, all three components would be continuously | |
| re-evaluated against real WMS/WCS/sensor logs rather than synthetic data. | |
| """ | |
| # ========================================================================== | |
| # TAB 5 -- About | |
| # ========================================================================== | |
| ABOUT_MD = f""" | |
| # 🏭 Smart Warehouse AI Assistant | |
| **A portfolio project demonstrating an applied-AI approach to intralogistics | |
| operations, built as part of a job application to Daifuku Co., Ltd.** | |
| ## What this demonstrates | |
| Daifuku builds material handling and automation systems -- AS/RS, conveyors | |
| and sortation, AGVs/AMRs, and the software (WMS/WCS) that orchestrates them. | |
| This project is a compact but end-to-end example of how an AI layer can sit | |
| on top of that kind of system: | |
| | Capability | Where | | |
| |---|---| | |
| | **LLM-powered natural-language assistant**, grounded with retrieval (RAG) so it answers from real warehouse-ops knowledge rather than hallucinating | *AI Assistant* tab | | |
| | **Intent classification** to route free-text requests (maintenance, safety, navigation, inventory, etc.) the way a real ops system would triage tickets | *AI Assistant* / *Inventory* tabs | | |
| | **Predictive maintenance** via unsupervised anomaly detection on conveyor/crane sensor streams -- catching bearing wear or misalignment before an unplanned stoppage | *Predictive Maintenance* tab | | |
| | **NL-to-structured-query** over inventory/order data, a lightweight stand-in for a WMS query tool | *Inventory & Order Query* tab | | |
| | **Rigorous, reproducible evaluation** of every ML component (accuracy, F1, ROC-AUC, retrieval hit-rate, latency) rather than just a demo that "looks like it works" | *Model Evaluation* tab | | |
| ## Tech stack | |
| - **UI / deployment:** [Gradio](https://gradio.app) on Hugging Face Spaces | |
| - **LLM:** Hosted instruct model via the Hugging Face **Inference API** | |
| (`huggingface_hub.InferenceClient`), configurable via the `LLM_MODEL_ID` | |
| env var. Falls back gracefully to a retrieval-only answer if no `HF_TOKEN` | |
| is configured, so the public demo never breaks. | |
| - **Retrieval:** TF-IDF + cosine similarity over a small hand-written | |
| warehouse-operations knowledge base (simple, fast, fully local RAG). | |
| - **Intent classification:** TF-IDF + Logistic Regression (scikit-learn) -- | |
| chosen deliberately over a heavier transformer classifier because it | |
| trains in under a second and comfortably reaches **{intent_eval.get('accuracy', 0):.0%} | |
| accuracy** on this task; right-sizing the model to the problem. | |
| - **Anomaly detection:** Isolation Forest (scikit-learn), trained | |
| unsupervised on scaled sensor features. | |
| - **Evaluation:** scikit-learn metrics + matplotlib, all computed by | |
| `build_artifacts.py` and saved as static artifacts the app loads at | |
| startup (fast, reproducible Space boot). | |
| ## Architecture | |
| ``` | |
| ┌───────────────────────┐ | |
| User query ───► │ Intent Classifier │ (TF-IDF + LogisticRegression) | |
| └──────────┬────────────┘ | |
| │ intent label | |
| ▼ | |
| ┌───────────────────────┐ | |
| │ KB Retriever (RAG) │ (TF-IDF cosine similarity) | |
| └──────────┬────────────┘ | |
| │ top-k passages | |
| ▼ | |
| ┌───────────────────────┐ | |
| │ Hosted LLM │ (HF Inference API) | |
| │ (or extractive │ | |
| │ fallback if offline) │ | |
| └──────────┬────────────┘ | |
| ▼ | |
| Grounded answer | |
| Sensor stream ───► StandardScaler ───► IsolationForest ───► anomaly / normal | |
| ``` | |
| ## Why this matters for Daifuku | |
| Modern intralogistics platforms generate huge volumes of operational data -- | |
| equipment telemetry, WMS transactions, safety logs. The value of AI here isn't | |
| a flashy chatbot; it's **routing, grounding, and reliability**: correctly | |
| triaging a request, answering from real operational context instead of | |
| guessing, and flagging equipment problems before they cause downtime. This | |
| project tries to demonstrate that mindset in miniature, with honest, | |
| reproducible evaluation numbers rather than cherry-picked demo runs. | |
| ## Limitations & next steps | |
| - All data here is **synthetic**, for portfolio/demo purposes -- a production | |
| version would connect to real WMS/WCS APIs and historical sensor logs. | |
| - The intent set (8 classes) and knowledge base (10 articles) are intentionally | |
| small to keep the demo fast and auditable; both are easy to extend. | |
| - The anomaly detector uses 4 hand-picked features; a production system would | |
| likely use a richer multivariate sensor set and a supervised or | |
| semi-supervised model once labelled failure data is available. | |
| --- | |
| *Built as an application project. Source code available on request / in the | |
| linked repository. Feedback welcome.* | |
| """ | |
| # ========================================================================== | |
| # GRADIO APP | |
| # ========================================================================== | |
| CUSTOM_CSS = """ | |
| #title-banner { text-align: center; margin-bottom: 0.5em; } | |
| .gradio-container { max-width: 1150px !important; margin: auto; } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="Smart Warehouse AI Assistant") as demo: | |
| gr.Markdown( | |
| "<h1 id='title-banner'>🏭 Smart Warehouse AI Assistant</h1>" | |
| "<p style='text-align:center; color:gray;'>LLM-powered intralogistics copilot · " | |
| "intent routing · predictive maintenance · retrieval-grounded Q&A</p>" | |
| ) | |
| if not HF_TOKEN_SET: | |
| gr.Markdown( | |
| "> ⚠️ **No HF token secret detected.** The AI Assistant tab will run in " | |
| "**retrieval-only fallback mode** (still functional, just not LLM-generated " | |
| f"prose). Add a secret named `HF_TOKEN` (or one of: {', '.join(TOKEN_ENV_VAR_CANDIDATES[1:])}) " | |
| "in *Space settings → Variables and secrets* to enable full LLM responses." | |
| ) | |
| with gr.Tab("💬 AI Assistant"): | |
| gr.Markdown( | |
| "Ask about equipment status, maintenance, safety, inventory, order status, " | |
| "AGV routing, picking strategy, or general warehouse-automation concepts. " | |
| "Answers are grounded (RAG) in a small warehouse-operations knowledge base." | |
| ) | |
| with gr.Accordion("ℹ️ About the data behind this tab", open=False): | |
| gr.Markdown( | |
| "- **Knowledge base (RAG source):** 10 original, hand-written articles " | |
| "covering AS/RS, AGV/AMR, WMS, conveyor/sortation, picking strategy, " | |
| "predictive maintenance, safety protocol, inventory accuracy, KPIs, and " | |
| "energy efficiency (`src/knowledge_base.py`). Every answer's *Retrieved " | |
| "context* line shows exactly which article(s) it drew on.\n" | |
| "- **Intent classifier:** trained on ~480 synthetically generated example " | |
| "queries across 8 categories (see the Model Evaluation tab for accuracy).\n" | |
| "- **LLM:** a hosted instruct model called via the Hugging Face Inference " | |
| "API — not run locally. If no `HF_TOKEN` is configured, or the API call " | |
| "fails, this tab automatically falls back to showing the retrieved " | |
| "knowledge-base passages directly, so it never just breaks." | |
| ) | |
| gr.ChatInterface( | |
| fn=chat_fn, | |
| type="messages", | |
| examples=ASSISTANT_EXAMPLES, | |
| chatbot=gr.Chatbot(height=430, label="Warehouse Assistant", type="messages"), | |
| textbox=gr.Textbox(placeholder="e.g. The conveyor belt in Zone C is making noise"), | |
| ) | |
| with gr.Accordion("🔧 LLM connection diagnostics", open=False): | |
| gr.Markdown( | |
| "If the assistant keeps answering in retrieval-only fallback mode, " | |
| "click below to test the LLM connection directly and see the exact " | |
| "error from each candidate model." | |
| ) | |
| test_llm_btn = gr.Button("Test LLM connection") | |
| test_llm_output = gr.Markdown() | |
| test_llm_btn.click(test_llm_fn, inputs=None, outputs=test_llm_output) | |
| with gr.Tab("📦 Inventory & Order Query"): | |
| gr.Markdown( | |
| "Type a natural-language inventory or order question. The intent classifier " | |
| "decides whether to query the inventory table or the orders table, then " | |
| "extracts SKU / order-id / zone slots to filter the result." | |
| ) | |
| with gr.Accordion("ℹ️ About the data behind this tab", open=False): | |
| gr.Markdown( | |
| f"- **Inventory table:** {len(inventory_df)} synthetic SKUs across 5 " | |
| "categories (Electronics, Apparel, Automotive Parts, Food & Beverage, " | |
| "Household) and 4 warehouse zones, with randomised on-hand quantities, " | |
| "reorder points, and unit costs.\n" | |
| f"- **Orders table:** {len(orders_df)} synthetic orders with randomised " | |
| "status (Received / Picking / Packed / Shipped / Delayed), line count, " | |
| "priority, and zone.\n" | |
| "- Both tables are generated by `src/data_generation.py` with a fixed " | |
| "random seed, so they're reproducible but **not real operational data** " | |
| "-- this is a stand-in for a live WMS/WCS query interface.\n" | |
| "- Query parsing is regex-based slot extraction (SKU codes like `SKU-1042`, " | |
| "order IDs like `#10007`, zone names) combined with the same intent " | |
| "classifier used in the AI Assistant tab (`src/inventory_db.py`)." | |
| ) | |
| with gr.Row(): | |
| inv_input = gr.Textbox(label="Query", placeholder="How many units of SKU-1042 are in Zone B?", scale=4) | |
| inv_btn = gr.Button("Search", variant="primary", scale=1) | |
| inv_note = gr.Markdown() | |
| inv_result = gr.Dataframe(label="Results", wrap=True) | |
| inv_btn.click(inventory_query_fn, inputs=inv_input, outputs=[inv_note, inv_result]) | |
| inv_input.submit(inventory_query_fn, inputs=inv_input, outputs=[inv_note, inv_result]) | |
| gr.Examples( | |
| examples=[ | |
| "How many units of SKU-1042 are in Zone B?", | |
| "What's the status of order #10007?", | |
| "Show me low stock items", | |
| "Any delayed orders?", | |
| ], | |
| inputs=inv_input, | |
| ) | |
| with gr.Accordion("Browse full tables", open=False): | |
| gr.Markdown("**Inventory** (synthetic)") | |
| gr.Dataframe(inventory_df, wrap=True) | |
| gr.Markdown("**Orders** (synthetic)") | |
| gr.Dataframe(orders_df, wrap=True) | |
| with gr.Tab("⚠️ Predictive Maintenance"): | |
| gr.Markdown( | |
| "Enter live (or hypothetical) conveyor/crane motor sensor readings to check " | |
| "for anomalous behaviour using an Isolation Forest model trained on " | |
| "historical sensor patterns." | |
| ) | |
| with gr.Accordion("ℹ️ About the data behind this tab", open=False): | |
| gr.Markdown( | |
| "- **Training data:** 1,000 synthetic sensor readings (900 normal + 100 " | |
| "anomalous) across 4 features -- motor temperature, vibration, motor " | |
| "current, and belt speed -- generated by `src/data_generation.py`. " | |
| "Anomalies simulate realistic failure signatures: elevated temperature, " | |
| "vibration, and current combined with reduced/erratic belt speed (the " | |
| "pattern of bearing wear, belt misalignment, or motor overload).\n" | |
| "- **Model:** Isolation Forest (unsupervised) trained on scaled features " | |
| "-- it never sees a 'this is an anomaly' label during training, only " | |
| "learns what 'normal' looks like and flags deviations from it.\n" | |
| "- See the **Model Evaluation** tab for the feature-distribution chart " | |
| "showing exactly how normal vs. anomalous readings differ, plus " | |
| "precision/recall/F1/ROC-AUC on held-out data." | |
| ) | |
| preset_dropdown = gr.Dropdown( | |
| choices=list(ANOMALY_PRESETS.keys()), label="Load a preset reading", value="Normal reading" | |
| ) | |
| with gr.Row(): | |
| motor_temp_in = gr.Slider(30, 100, value=55, step=0.5, label="Motor temperature (°C)") | |
| vibration_in = gr.Slider(0, 10, value=2.2, step=0.1, label="Vibration (mm/s)") | |
| with gr.Row(): | |
| current_in = gr.Slider(5, 30, value=12, step=0.5, label="Motor current (A)") | |
| belt_speed_in = gr.Slider(0.1, 2.5, value=1.5, step=0.05, label="Belt speed (m/s)") | |
| check_btn = gr.Button("Check for anomaly", variant="primary") | |
| anomaly_output = gr.Markdown() | |
| preset_dropdown.change( | |
| load_preset, inputs=preset_dropdown, | |
| outputs=[motor_temp_in, vibration_in, current_in, belt_speed_in], | |
| ) | |
| check_btn.click( | |
| anomaly_fn, | |
| inputs=[motor_temp_in, vibration_in, current_in, belt_speed_in], | |
| outputs=anomaly_output, | |
| ) | |
| with gr.Tab("📊 Model Evaluation"): | |
| gr.Markdown( | |
| "Every metric on this tab is computed on **held-out test data** by " | |
| "`build_artifacts.py` (not cherry-picked from a live demo run) -- " | |
| "re-run that script any time to reproduce these numbers from scratch." | |
| ) | |
| gr.Markdown(eval_intent_section_md()) | |
| gr.Markdown("**Dataset composition** (how many training examples per intent):") | |
| gr.Image(os.path.join(ASSETS_DIR, "intent_dataset_composition.png"), show_label=False, container=False) | |
| gr.Markdown("**Per-class precision / recall / F1:**") | |
| gr.Image(os.path.join(ASSETS_DIR, "intent_per_class_bar.png"), show_label=False, container=False) | |
| gr.Markdown("**Confusion matrix:**") | |
| gr.Image(os.path.join(ASSETS_DIR, "intent_confusion_matrix.png"), show_label=False, container=False) | |
| gr.Markdown("---") | |
| gr.Markdown(eval_anomaly_section_md()) | |
| gr.Markdown("**Sensor feature distributions (normal vs. anomaly):**") | |
| gr.Image(os.path.join(ASSETS_DIR, "sensor_distributions.png"), show_label=False, container=False) | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("**Evaluation metrics:**") | |
| gr.Image(os.path.join(ASSETS_DIR, "anomaly_metrics_bar.png"), show_label=False, container=False) | |
| with gr.Column(): | |
| gr.Markdown("**Confusion matrix:**") | |
| gr.Image(os.path.join(ASSETS_DIR, "anomaly_confusion_matrix.png"), show_label=False, container=False) | |
| gr.Markdown("**ROC curve:**") | |
| gr.Image(os.path.join(ASSETS_DIR, "anomaly_roc_curve.png"), show_label=False, container=False) | |
| gr.Markdown("---") | |
| gr.Markdown(eval_retrieval_section_md()) | |
| gr.Image(os.path.join(ASSETS_DIR, "retrieval_hitrate_bar.png"), show_label=False, container=False) | |
| gr.Markdown("---") | |
| gr.Markdown(eval_latency_section_md()) | |
| gr.Image(os.path.join(ASSETS_DIR, "latency_bar.png"), show_label=False, container=False) | |
| gr.Markdown("---") | |
| gr.Markdown(EVAL_METHODOLOGY_MD) | |
| with gr.Tab("ℹ️ About"): | |
| gr.Markdown(ABOUT_MD) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860))) | |