Interactive UI + adaptive learning + best-iter composite scoring#1
Conversation
…earning
Adds an interactive web UI (Flask + vanilla JS/CSS) launched after the
multi-agent pipeline completes. The UI turns the final named clusters
into a feedback loop with persistent memory:
- Direct edits to name/tagline/description/traits/confidence persist
back to outputs/personas.json
- "Regenerate with hint" calls the Decision Maker (PersonaNamingAgent)
live for a single cluster, honouring a freeform user suggestion
- Merge 2+ clusters: stats are recomputed by weighted aggregation,
the merged cluster gets a fresh LLM-generated name
- Global rules ("never use the word 'shopper'") apply to every future run
Every UI action is appended to outputs/user_feedback_log.jsonl with
priority/date/type metadata. PersonaNamingAgent now prepends a
"USER PREFERENCES" block built from that log to its prompt on every
future pipeline run, so the agent system genuinely learns from prior
user interactions.
Launch with: python -m ui.launch
Pipeline ↔ UI integration upgrades - Live architecture graph + per-iteration history pills (FeatureEngineer, FeatureSelector, Clusterer + PCA scatter snapshots per Clusterer iter) - 3-ledger Tokens & cost panel: Pipeline / Evidence / Naming discussions - Bypass-mode auto-decision: when a warning fires, LLM explains the pipeline's chosen action in active voice (Evidence ledger, separate spend) - Interactive-mode pause-on-warning + decision modal that feeds the user's guidance back as a high-priority memory rule - Per-cluster multi-turn chat with the LLM (POST /api/cluster-chat) → Conclude → propose rename/merge/keep/recluster actions - Final pipeline summary card (top of Evidence tab on completion): per-iter silhouette / F1 / tokens / cost / time, winning iter highlighted - Mode toggle (Bypass / Interactive), demo focus URLs ?demo=<area> - 4 tabs: Live pipeline | Data & evidence | Named clusters; tokens panel pinned directly below the architecture graph - Memory drawer redesigned: inline 'Add memory rule' form, filter chips, DELETE endpoint, user_change : priority : date catalog format Pipeline behavior changes - silhouette_target (default 0.5) — Clusterer outputs below target loop back to feature selection - max_reselect_failures (default 3) — N misses → re-engineer features from raw data + Decision Maker picks fresh algorithm - max_relax_failures (default 3) — N misses → bypass auto-lowers silhouette_target by 0.1, interactive opens relax modal - All escalations + target changes log as Orchestrator agent outputs - bus.report() pauses on warnings when mode=interactive; bus.ask() supports category='pipeline'|'evidence'|'naming' for per-ledger cost tracking - OrchestratorBus init truncates events JSONL + deletes stale outputs so every restart starts clean Plus - 12 new public datasets via UCI + Kaggle (humans / products / signals / images-as-tabular) under data/raw/, total ~80 MB - record_demo.py: Playwright-based 5-window recorder (one .webm per UI region: graph / convos / outputs / evidence / tokens) - File upload + drop-zone + per-column skewness histograms in Evidence tab - Bus emits llm_call_started / llm_call_finished events with full prompt + response for live chat-bubble rendering Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 3 session screenshots were debugging artifacts, not project assets. Drop them from the repo and prevent future .png files from being tracked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ch docs record_demo.py now records intent + log windows alongside the original five, matching the views available via ?demo=<area>. style.css adds the matching body.demo-intent and body.demo-log focus modes so each window captures only its target region. Docs cover the macOS gotcha where `setsid` is not a binary and the Python `start_new_session=True` recipe is required to fully reparent the pipeline + recorder to launchd so closing Cursor cannot SIGHUP them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipeline behavior
- update_best now scores by F1×100 + avg_confidence so the actual winning
iteration drives outputs/personas.json (and the Named Clusters tab),
not whichever iteration happened to be approved first.
- auto-approve waits for 3 passing iterations before approving so the
orchestrator can compare multiple {naming, F1} candidates.
- approve path saves state.best_* instead of the current iter, with a
console note when an earlier iteration scored higher.
- human-checkpoint recluster now calls _ask_parameter_tuning so each
exploration round tries a different algorithm/k for diversity.
- silhouette_target is plumbed from orchestrator into ClusteringAgent.run
so the clusterer's success/warning chip uses the dynamic target
(was hardcoded 0.25). max_relax_failures code default aligned to 3.
UI fixes
- applyChatConclusion('rename') now refreshes state.draft after server
update, so a subsequent Save Edits no longer overwrites the renamed
title with the stale draft. The chat proposal summary + reason are
also POSTed to /api/feedback/global as a high-priority memory rule so
the next pipeline run sees WHY the rename happened.
- Removed the misleading "accuracy 0.964" chip from the Classifier card;
F1 is now the single headline metric for the gate.
- Per-iteration summary table gets a Why column explaining each row's
outcome ("silhouette 0.30 < target 0.50 → reselect features",
"Clarity Gate failed", "F1 low"), plus algorithm-column hover that
surfaces the auto-select reasoning and silhouette / target side-by-side.
- _relax_silhouette_target emits a high-visibility Orchestrator
agent_report so 0.5 → 0.4 → 0.3 degrades land as a warning chip in
the outputs panel, not a buried event-log line.
Recording
- record_demo.py: --skip-pipeline-check (record against previously-saved
personas), --stop-on-key (manual interactions), and a 'full' pseudo-
region that loads the bare UI at 1280×800 for free-form walkthroughs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Playwright captures only the viewport and there's no real user to scroll, so long-running pipeline demos (outputs panel, evidence cards, named clusters grid) appeared frozen as new content appended below the fold. - New autoScrollForDemo() helper; no-op outside body.demo-mode so live sessions keep their natural scroll position. - Hooked into renderOutputsPanel, renderEvidence, renderGrid. - Scrolls both the relevant inner container and the window, so every demo mode is covered regardless of which CSS pattern it uses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…resilience Pipeline - agents/state.py: new composite_score() = F1×100 + Silhouette×30 − log10(VIF)×5. update_best() now uses it, runs on EVERY iteration (not just Clarity-Gate passers), so the best-iteration decision genuinely reflects all three user-named metrics (F1 ↑, Silhouette ↑, VIF ↓). Adds current_max_vif() helper that reads the latest fs_history entry. - agents/orchestrator.py: refactored the main loop so PersonaNamer + Classifier always run when the clusterer produced labels — even on silhouette-target miss or Clarity-Gate fail. Naming uses force_proceed=True when silhouette is below target so personas exist for Classifier to score. Escalation rules (reselect/re-engineer/relax) now apply AFTER scoring, so a "bad" iteration's metrics still feed the best-iter tracker. UI resilience during recordings - ui/app.py: 120-second in-memory dedup cache on /api/explain so the multi- window recorder (3 Chromium contexts open simultaneously) doesn't fire 3× identical EvidenceExplainer LLM calls for every warning. - ui/static/app.js: 20-second global refresh watchdog re-renders graph, cost panel, Evidence tab, and Named Clusters grid even when SSE goes silent (proxy timeouts, backgrounded tabs, bursty event starvation of the per-event debounce). Evidence tab's event-driven debounce gets a 20-s hard deadline so it can't be starved during long bursts. - record_demo.py: --port / --base-url flags so the recorder can target an arbitrary UI port (e.g. --port 5090). 'full' pseudo-region opens the bare UI at 1280×800 for manual walkthrough recordings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the current state of the outputs/ directory (force-added past .gitignore) so the run logs, events stream, persona artifacts, and upload preview are tracked at this point in time. The in-progress run (2026-05-17 12:46 → ongoing) is included but will continue to evolve; re-snapshot after it completes if you want the final state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Describes the new live web UI (architecture graph, agent ↔ Decision Maker chat bubbles, per-iteration outputs, 3 cost ledgers, Data & Evidence tab, Named Clusters tab) and the user-feedback system: direct edit, regenerate with hint, merge clusters, per-cluster multi-turn chat. Documents how feedback in outputs/user_feedback_log.jsonl flows back into the next pipeline run's Decision Maker prompts (adaptive learning across runs). Also adds Bypass vs Interactive mode and the memory drawer. Cross-links the new section from "How to Run". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two short, embed-friendly clips captured from the live UI on port 5090: - data_evidence.mp4 (13 MB, 60 s) — Data & Evidence tab walkthrough - named_cluster.mp4 (18 MB, 59 s) — Named Clusters tab walkthrough Force-added past the recordings/ gitignore so the slide deck has a stable public URL for these specific clips. Larger source webms and full-length recordings are intentionally NOT tracked (would exceed GitHub's 100 MB per-file limit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Selected from the demo MP4s (recordings/data_evidence.mp4 and recordings/named_cluster.mp4). Three frames chosen to convey the whole story in the README's Interactive UI section: 1. Per-cluster multi-turn chat + Conclude→propose-action panel (named_cluster.mp4 @ ~36s) 2. Per-iteration 2-D PCA projection with the orchestrator's silhouette- miss escalation warning rendered in line (data_evidence.mp4 @ ~27s) 3. Adaptive Memory drawer showing global rules with priority badges that the next pipeline run will read (named_cluster.mp4 @ ~56s) Force-added past the *.png gitignore (one-off allowlist via -f). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…diagram - Reorders the 3 Interactive UI screenshots so the description text comes BEFORE its figure (the previous order made the figure float without context until you scrolled past). - Replaces the broken GitHub user-attachments architecture image (only visible to logged-in users) with a self-contained Mermaid flowchart that GitHub renders natively. Covers all 7 agents, the OrchestratorBus, the Decision Maker, all backward-feedback loops with their thresholds, the Human Checkpoint, and the save step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub's Mermaid renderer treats '<' as HTML and aborts on "F1 < 0.70". Rewrote edge labels to use "below" instead of "<", quoted all dotted-edge labels, dropped the subgraph wrapper, and split the BUS<->agents fan-out into individual edges (the '&' chained form had inconsistent rendering). Also kept the new node layout but removed the unused 'gate' classDef. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the lexer-fragile mermaid block with the existing agentic_labelling.png (157 KB) — 7 STEP boxes in a row with dotted feedback arrows down to a central Orchestrator box. Renders identically in every markdown viewer and Google Docs / Notion paste without needing mermaid support. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the earlier minimal PNG with one that shows EVERY gate inside
its step box (purple chips) plus all the feedback routes labeled:
- FeatureSelector: PCA + AE · VIF ≤ threshold · LLM picks 25-55
- Clusterer: silhouette ≥ target · size ≤ 40 % · deepening
- PersonaNamer: Clarity Gate (conf ≥ 6, unique names)
- Classifier: macro-F1 ≥ 0.70 · per-class F1
Escalation arrows down to the orchestrator:
Clusterer → oversized cluster → reselect features
Clusterer → silhouette below target → reselect features
Clusterer → 3 consecutive misses → re-engineer + new algorithm
PersonaNamer → Clarity Gate fails → re-cluster
Classifier → F1 < 0.70 → reselect features
Classifier → F1 < 0.70 → re-cluster
Orchestrator panel now states the actual best-iteration rule
(composite: F1 ↑, Silhouette ↑, max-VIF ↓) and that user feedback
feeds back into the next run's prompts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Down from 277 → 76 lines. New structure: TL;DR · Architecture · Quick Start · Interactive UI · Configuration · Outputs (with best-effort note) Dropped: verbose "Problem" intro, per-agent role table (the figure now carries it), "How each agent calls the Decision Maker" + concrete-prompt example, separate Demo Dataset section (folded into Quick Start), the "What you can see in real time" / "Feedback system" / "Adaptive learning" / "Bypass vs Interactive" / "Memory drawer" subsections (the 3 captioned screenshots already convey them), Skills table, Setup duplicate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous version: gap_x=1.0 left only ~0.8 units for the arrow shaft and the line was 2pt — visually almost absent, contradicting the README's "solid arrows = forward path" caption. Now: gap_x=3.4 + linewidth=3 + larger arrowhead. The forward path now reads unambiguously against the dotted feedback arrows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02a4f118d0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| save_outputs(cr, nr, clf, self.bus) | ||
| # Make sure the CURRENT iteration competes for best (it just passed | ||
| # all gates) so the all-time best comparison is fair. | ||
| state.update_best(nr, cr, clf) |
There was a problem hiding this comment.
Pass current max VIF when re-scoring approved iteration
The approve path re-scores the current iteration with state.update_best(nr, cr, clf) but omits max_vif; in PipelineState.update_best, a missing max_vif falls back to self.best_max_vif, so the current candidate can be compared using the previous winner’s VIF penalty instead of its own. When the current iteration has worse multicollinearity, this can incorrectly overwrite best_* immediately before save_outputs, violating the intended composite winner logic (F1 + silhouette − current VIF penalty).
Useful? React with 👍 / 👎.
| deadline = time.time() + UI_INTENT_TIMEOUT_S | ||
| try: | ||
| while time.time() < deadline: | ||
| if PENDING_INTENT_PATH.exists(): | ||
| return self._consume_intent_file() | ||
| time.sleep(1.0) |
There was a problem hiding this comment.
Avoid blocking 10 minutes before terminal intent prompts
_wait_for_ui_intent() waits up to UI_INTENT_TIMEOUT_S (600s) in a polling loop, and run() calls it unconditionally before any CLI prompt. In headless or --no-ui runs where no browser writes pending_intent.json, users are forced to wait ~10 minutes (or manually Ctrl-C) before terminal input starts, which makes non-UI execution appear hung.
Useful? React with 👍 / 👎.
…deos Replaces the two figures (per-cluster chat + memory drawer) with the existing recordings/named_cluster.mp4 — that single clip covers BOTH the chat workflow and the memory drawer that captures it, so a combined caption + one video reads more naturally than two stills. Also swaps the Data & Evidence figure for recordings/data_evidence.mp4 so all three Interactive UI subsections are now live demos. GitHub auto-detects bare *.mp4 URLs on their own line and renders them as inline video players (the documented embedding approach). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bare *.mp4 URLs in README markdown trigger GitHub to serve them as file downloads, not inline players. Explicit <video src=… controls muted playsinline> renders the HTML5 player on the README page so the demo plays in-browser without anyone having to download. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub serves both raw-repo mp4s and release-asset mp4s with Content-Disposition: attachment, so <video> tags rendered as download links instead of inline players. Replaced with markdown ![]() GIF references — these auto-play and loop in the GitHub README without any user action. Sized down to 1.9 MB / 10 s loops / 560 px wide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- run_pipeline.py: reconfigure stdout/stderr to UTF-8 so the Unicode separators
('─') in agent output do not crash _Tee on Windows cp1252 consoles.
- requirements.txt: add `requests` (used by download_datasets.py and
record_demo.py) and `scikit-fuzzy` (used by the fuzzy_cmeans branch of
agents/clusterer.py). Flask was already pinned but missing from local envs.
- ui/templates/index.html: tab title now matches the in-app brand
('⬢ Persona Studio · Live Cluster Discovery').
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fuses the existing multi-agent clustering pipeline with a live Flask + SSE web UI, replaces the "approve on first pass" auto-checkpoint with a composite-score (F1 × 100 + Silhouette × 30 − log10(VIF) × 5) best-of-N selection across iterations, and adds adaptive-learning plumbing where UI feedback is persisted to outputs/user_feedback_log.jsonl and replayed into Decision Maker prompts on subsequent runs. It also slims the README, adds a Playwright-based multi-region demo recorder, and snapshots example outputs/logs.
Changes:
- Composite best-iteration scoring (
agents/state.py) + auto-approve waiting for ≥3 passing iterations (run_pipeline.py) +silhouette_targetplumbed dynamically intoClusteringAgent.run. - New Flask UI (
ui/) +OrchestratorBusevent streaming, interactive-mode pause-on-warning, three cost ledgers, adaptive memory store (ui/feedback_store.py) injected intoPersonaNamingAgent. - Playwright demo recorder, expanded
download_datasets.py, slimmed README, new docs, and committed exampleoutputs/snapshots + per-run terminal logs.
Reviewed changes
Copilot reviewed 53 out of 101 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
run_pipeline.py |
Auto-approve now needs ≥3 passing iters; embeds UI launcher + --ui-port flag. |
agents/state.py |
Adds composite-score update_best(), VIF tracking, escalation counters. |
agents/clusterer.py |
Honors dynamic silhouette_target for success/warning chip + report. |
agents/persona_namer.py |
Prepends build_preferences_block() from UI feedback store to prompt. |
agents/user_input.py |
UI-first intent collection via outputs/pending_intent.json. |
skills/orchestrator_bus.py |
JSONL event stream, interactive-mode decision wait, stale-file cleanup. |
ui/* |
New Flask app, templates, feedback store, transient LLM bridge for regenerate/merge/chat. |
record_demo.py |
Playwright multi-window recorder. |
config.yaml, requirements.txt, download_datasets.py |
Escalation knobs, Flask + scikit-fuzzy, 8 new datasets. |
README.md, docs/* |
Slim README, session/HOWTO/changelog docs. |
.gitignore |
Adds *.png, data/uploads/, recordings/, *.webm. |
outputs/* |
Force-committed run snapshots, mode file, feedback log, per-run terminal logs; removes some prior snapshot JSONs. |
data/raw/*/README.md |
Per-dataset descriptions for new bundles. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {"type": "global_rule", "rule": "Guidance for DatasetExaminer warning (\"Mean feature skewness=3.6 is high. Log-transform recommended before clustering.\"): yes apply log-transforms", "priority": "high", "id": "fb_6c970266", "date": "2026-05-16T14:47:20+00:00", "active": true} | ||
| {"type": "global_rule", "rule": "Guidance for DatasetExaminer warning (\"Mean feature skewness=3.6 is high. Log-transform recommended before clustering.\"): yes apply what you suggested", "priority": "high", "id": "fb_eaf08b0a", "date": "2026-05-16T14:57:03+00:00", "active": true} | ||
| {"type": "global_rule", "rule": "Guidance for Clusterer warning (\"Cluster 1 has 92.4% of data (>40% threshold)\"): recluster this too big cluster to a smaller size", "priority": "high", "id": "fb_acfb263c", "date": "2026-05-16T15:12:45+00:00", "active": true} | ||
| {"type": "global_rule", "rule": "Guidance for Clusterer warning (\"Cluster 1 has 92.4% of data (>40% threshold)\"): recluster this big cluster to proper size, you can decide after analyse it", "priority": "high", "id": "fb_e7df3d82", "date": "2026-05-16T15:13:58+00:00", "active": true} | ||
| {"type": "global_rule", "rule": "Guidance for Clusterer warning (\"Silhouette=0.127 < 0.15 — clusters overlap; interpretability may be low. Consider reviewing feature selection or k.\"): go back to feature selection", "priority": "high", "id": "fb_1c1c8bea", "date": "2026-05-16T15:18:28+00:00", "active": true} | ||
| {"type": "global_rule", "rule": "Guidance for PersonaNamer warning (\"Must-have cluster type(s) not found in any persona name/description: ['travellers']\"): try more k", "priority": "high", "id": "fb_8a3630fb", "date": "2026-05-16T15:19:32+00:00", "active": true} | ||
| {"type": "global_rule", "rule": "Guidance for Clusterer warning (\"Cluster 4 has 40.9% of data (>40% threshold)\"): recluster that cluster 4, to more clusters", "priority": "high", "id": "fb_540e77d0", "date": "2026-05-16T15:19:55+00:00", "active": true} | ||
| {"type": "global_rule", "rule": "Guidance for Clusterer warning (\"Silhouette=0.145 < 0.15 — clusters overlap; interpretability may be low. Consider reviewing feature selection or k.\"): let it pass", "priority": "high", "id": "fb_510836ec", "date": "2026-05-16T15:23:38+00:00", "active": true} | ||
| {"type": "manual_override", "target_cluster_id": "7", "target_cluster_name": "Entertainment & Miscellaneous Spenders", "before": {"confidence": 8, "description": "These 24 people spend 3–3.7x the average on miscellaneous purchases (both online and in-store) and 2.65x the average on entertainment. They live in very small towns despite their high spending, and they barely travel at all. Their money goes into local entertainment and diverse, large purchases rather than travel.", "dominant_features": ["sum_misc_pos_6m", "sum_misc_net_6m", "sum_entertainment_6m"], "name": "Entertainment & Miscellaneous Big Spenders", "tagline": "They drop large sums on fun and hard-to-categorise purchases", "traits": ["Miscellaneous spending is over 3x the average", "Entertainment spending is nearly 3x above average", "Very low travel spending — homebodies who spend locally", "Live in tiny towns despite very high expenditure", "Diverse, high-value purchases across categories"]}, "after": {"confidence": 8, "description": "These 24 people spend 3–3.7x the average on miscellaneous purchases (both online and in-store) and 2.65x the average on entertainment. They live in very small towns despite their high spending, and they barely travel at all. Their money goes into local entertainment and diverse, large purchases rather than travel.", "dominant_features": ["sum_misc_pos_6m", "sum_misc_net_6m", "sum_entertainment_6m"], "name": "Entertainment & Miscellaneous Spenders", "tagline": "They drop large sums on fun and hard-to-categorise purchases", "traits": ["Miscellaneous spending is over 3x the average", "Entertainment spending is nearly 3x above average", "Very low travel spending — homebodies who spend locally", "Live in tiny towns despite very high expenditure", "Diverse, high-value purchases across categories"]}, "priority": "high", "id": "fb_c17f009b", "date": "2026-05-16T22:44:19+00:00", "active": true} | ||
| {"type": "manual_override", "target_cluster_id": "8", "target_cluster_name": "Grocery-Obsessed Female Bulk Buyers", "before": {"confidence": 9, "description": "All 112 people in this cluster are female (0% male), and they are the single biggest grocery buyers in the entire dataset — spending 2.7–2.8x the average on both online and in-store groceries. Their grocery transaction counts are also 2.5x above average, meaning they shop for food very frequently and in large amounts.", "dominant_features": ["sum_grocery_net_12m", "sum_grocery_pos_all", "count_grocery_net_12m"], "name": "Grocery-Obsessed Bulk Buyers", "tagline": "They buy groceries constantly and in very large quantities", "traits": ["100% female — the only all-female cluster", "Grocery spending is nearly 3x above average", "Shops for groceries very frequently — over 2.5x the average count", "Large in-store and online grocery volumes", "Low gas spending — may rely on delivery or live close to stores"]}, "after": {"confidence": 9, "description": "All 112 people in this cluster are female (0% male), and they are the single biggest grocery buyers in the entire dataset — spending 2.7–2.8x the average on both online and in-store groceries. Their grocery transaction counts are also 2.5x above average, meaning they shop for food very frequently and in large amounts.", "dominant_features": ["sum_grocery_net_12m", "sum_grocery_pos_all", "count_grocery_net_12m"], "name": "Grocery-Obsessed Female Bulk Buyers", "tagline": "They buy groceries constantly and in very large quantities", "traits": ["100% female — the only all-female cluster", "Grocery spending is nearly 3x above average", "Shops for groceries very frequently — over 2.5x the average count", "Large in-store and online grocery volumes", "Low gas spending — may rely on delivery or live close to stores"]}, "priority": "high", "id": "fb_84b6045b", "date": "2026-05-16T22:45:17+00:00", "active": true} |
|  | ||
|
|
||
| The script: | ||
| **Data & Evidence tab** — per-iteration 2-D PCA projection of the clustered data, with the orchestrator's adaptive-escalation warning surfaced in line: *"Silhouette=0.142 < target 0.40 — orchestrator will reselect features (or escalate after 3 consecutive misses)"*. | ||
|
|
||
| - Loads `.env` and `config.yaml` | ||
| - Auto-detects whether to run FeatureEngineerAgent (raw CSV) or skip it (parquet) | ||
| - Runs the Decision Maker loop with `max_total_iterations=10` | ||
| - After each failure, the Decision Maker proposes new VIF/k/algorithm/silhouette parameters | ||
| - At max iterations, delivers a best-effort result if no iteration fully passed | ||
| - Writes all outputs under `outputs/` and prints a full console report | ||
|  |
| if "awaiting_intent" not in {e for e in []} and not args.auto_submit: | ||
| print(" [demo] note: pipeline state =", json.dumps(status, indent=2)) |
| [run_pipeline] Logging full output to /Users/tzu-chunchen/Documents/AI/AI_Personalization/outputs/pipeline_run_20260516_180657.txt | ||
| [run_pipeline] Launching live UI at http://127.0.0.1:5057/ (use --no-ui to disable) | ||
| [run_pipeline] Using raw CSV for fresh feature engineering: data/raw/fraudTrain.csv | ||
| * Serving Flask app 'ui.app' | ||
| * Debug mode: off | ||
| Address already in use | ||
| Port 5057 is in use by another program. Either identify and stop that program, or start the server with a different port. | ||
| [Orchestrator] Skill catalog loaded: 11778 chars (skill.md) | ||
| [Orchestrator] Agent catalog loaded: 20866 chars (agent.md) | ||
|
|
||
| ================================================================= | ||
| Multi-Agent Clustering & Persona Discovery Pipeline | ||
| ================================================================= | ||
| Config : {'dataset_path': None, 'n_clusters': None, 'clustering_algorithm': 'auto', 'max_cluster_size_pct': 0.4, 'sub_n_clusters': 3, 'max_depth': 2, 'silhouette_target': 0.5, 'max_reselect_failures': 3, 'persona_tone': 'easy', 'ae_bottleneck_cap': 32, 'ae_max_iter': 200, 'classifier_model': 'auto'} | ||
| Features : data/raw/fraudTrain.csv | ||
| Max iters : 10 | ||
| Classifier F1 threshold: 0.7 | ||
|
|
||
| ================================================================= | ||
| AGENTIC CLUSTERING PIPELINE — Intent Collection | ||
| ================================================================= | ||
|
|
||
| [UserInput] Waiting up to 600s for intent from the UI form | ||
| [UserInput] (open http://127.0.0.1:5057/ and submit the intent form, | ||
| [UserInput] or press Ctrl-C to fall back to terminal prompts) | ||
|
|
||
| [UserInput] Interrupted — falling back to terminal prompts. | ||
| Before we begin, please answer a few questions. | ||
| (Press Enter to skip optional questions and use defaults.) | ||
|
|
||
|
|
||
| 1. What entity are you clustering? | ||
| Examples: customers, products, employees, merchants | ||
| [Non-interactive] Using default: 'customers' | ||
|
|
||
| 2. What is the business purpose of this clustering? | ||
| Be specific — this shapes which features are built and how clusters are interpreted. | ||
| Example: 'understand customer shopping behaviour to personalise product recommendations' | ||
| [Non-interactive] Using default: '' | ||
|
|
||
| [UserInput] Your answer is quite short — a more specific purpose | ||
| leads to better features and cluster labels. | ||
|
|
||
| Can you elaborate? (or press Enter to continue with what you gave) | ||
| Tip: mention what decision or action the clusters will support | ||
| [Non-interactive] Using default: '' | ||
|
|
||
| Default dataset path: data/raw/fraudTrain.csv | ||
|
|
||
| 3. Dataset path? (press Enter to use default) | ||
| [Non-interactive] Using default: '' | ||
|
|
||
| 4. Any constraints or filters? (optional — press Enter to skip) | ||
| Example: 'only use last 12 months of transactions', 'exclude VIP customers' | ||
| [Non-interactive] Using default: '' | ||
|
|
||
| 5. How many clusters would you like? (press Enter to let the pipeline decide) | ||
| Example: '5' for exactly 5 clusters. Leave blank for data-driven selection. | ||
| [Non-interactive] Using default: '' | ||
|
|
||
| 6. Must any specific types appear as clusters? (optional — press Enter to skip) | ||
| List types separated by commas — these are semantic labels the pipeline | ||
| MUST produce as distinct personas. | ||
| Example: 'traveller, high-value-customer, weekend-shopper' | ||
| [Non-interactive] Using default: '' | ||
|
|
||
| ───────────────────────────────────────────────────────────────── | ||
| Captured intent: | ||
| Target entity : customers | ||
| Business purpose : | ||
| Dataset path : data/raw/fraudTrain.csv | ||
| Clusters wanted : data-driven (auto-select) | ||
| ───────────────────────────────────────────────────────────────── | ||
| [Bus] ✓ UserInput (iter 0): SUCCESS — Collected intent: target='customers', purpose='' | ||
| Doubt: Business purpose may still be too vague. | ||
|
|
||
| Loading raw transaction data: data/raw/fraudTrain.csv | ||
| Traceback (most recent call last): | ||
| File [35m"/Users/tzu-chunchen/Documents/AI/AI_Personalization/run_pipeline.py"[0m, line [35m181[0m, in [35m<module>[0m | ||
| result = orchestrator.run( | ||
| features_path=_default_features_path, | ||
| max_total_iterations=10, | ||
| skip_user_input=False, # UserInputAgent will prompt for your clustering intent | ||
| ) | ||
| File [35m"/Users/tzu-chunchen/Documents/AI/AI_Personalization/agents/orchestrator.py"[0m, line [35m472[0m, in [35mrun[0m | ||
| full_raw_df = _load_df(raw_data_path) | ||
| File [35m"/Users/tzu-chunchen/Documents/AI/AI_Personalization/agents/orchestrator.py"[0m, line [35m52[0m, in [35m_load_df[0m | ||
| return [31mpd.read_csv[0m[1;31m(path, sep=sep, low_memory=False)[0m | ||
| [31m~~~~~~~~~~~[0m[1;31m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[0m | ||
| File [35m"/Users/tzu-chunchen/Documents/AI/AI_Personalization/.venv/lib/python3.13/site-packages/pandas/io/parsers/readers.py"[0m, line [35m873[0m, in [35mread_csv[0m | ||
| return _read(filepath_or_buffer, kwds) | ||
| File [35m"/Users/tzu-chunchen/Documents/AI/AI_Personalization/.venv/lib/python3.13/site-packages/pandas/io/parsers/readers.py"[0m, line [35m306[0m, in [35m_read[0m | ||
| return [31mparser.read[0m[1;31m(nrows)[0m | ||
| [31m~~~~~~~~~~~[0m[1;31m^^^^^^^[0m | ||
| File [35m"/Users/tzu-chunchen/Documents/AI/AI_Personalization/.venv/lib/python3.13/site-packages/pandas/io/parsers/readers.py"[0m, line [35m1947[0m, in [35mread[0m | ||
| ) = [31mself._engine.read[0m[1;31m( # type: ignore[attr-defined][0m | ||
| [31m~~~~~~~~~~~~~~~~~[0m[1;31m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[0m | ||
| [1;31mnrows[0m | ||
| [1;31m^^^^^[0m | ||
| [1;31m)[0m | ||
| [1;31m^[0m | ||
| File [35m"/Users/tzu-chunchen/Documents/AI/AI_Personalization/.venv/lib/python3.13/site-packages/pandas/io/parsers/c_parser_wrapper.py"[0m, line [35m219[0m, in [35mread[0m | ||
| data = self._reader.read(nrows) | ||
| File [35m"pandas/_libs/parsers.pyx"[0m, line [35m814[0m, in [35mpandas._libs.parsers.TextReader.read[0m | ||
| File [35m"pandas/_libs/parsers.pyx"[0m, line [35m906[0m, in [35mpandas._libs.parsers.TextReader._read_rows[0m | ||
| File [35m"pandas/_libs/parsers.pyx"[0m, line [35m885[0m, in [35mpandas._libs.parsers.TextReader._check_tokenize_status[0m | ||
| File [35m"pandas/_libs/parsers.pyx"[0m, line [35m2076[0m, in [35mpandas._libs.parsers.raise_parser_error[0m | ||
| File [35m"<frozen codecs>"[0m, line [35m322[0m, in [35mdecode[0m | ||
| [1;35mKeyboardInterrupt[0m |
| { | ||
| "mode": "bypass" | ||
| } No newline at end of file |
| deadline = time.time() + DECISION_TIMEOUT_S | ||
| decision = None | ||
| try: | ||
| while time.time() < deadline: | ||
| if PENDING_DECISION.exists(): | ||
| try: | ||
| decision = json.loads(PENDING_DECISION.read_text(encoding='utf-8')) | ||
| except (OSError, json.JSONDecodeError) as exc: | ||
| print(f" [INTERACTIVE MODE] Bad decision file: {exc}") | ||
| try: | ||
| PENDING_DECISION.unlink(missing_ok=True) | ||
| except OSError: | ||
| pass | ||
| break | ||
| time.sleep(0.6) | ||
| except KeyboardInterrupt: | ||
| print("\n [INTERACTIVE MODE] Interrupted — proceeding without decision.") | ||
| return |
| print(f"\n [UserInput] Waiting up to {UI_INTENT_TIMEOUT_S}s for intent from the UI form") | ||
| print(f" [UserInput] (open http://127.0.0.1:5057/ and submit the intent form,") | ||
| print(f" [UserInput] or press Ctrl-C to fall back to terminal prompts)") |
| def _load_env_file() -> None: | ||
| env_path = _ROOT / '.env' | ||
| if not env_path.exists(): | ||
| return | ||
| for line in env_path.read_text().splitlines(): | ||
| line = line.strip() | ||
| if line and not line.startswith('#') and '=' in line: | ||
| k, _, v = line.partition('=') | ||
| os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) |
| if str(_ROOT) not in sys.path: | ||
| sys.path.insert(0, str(_ROOT)) | ||
|
|
||
| _MODEL = 'claude-sonnet-4-6' |
| _MIN_PASSING_BEFORE_APPROVE = 3 | ||
| _passing_count = {'n': 0} | ||
|
|
||
| _orig_chk = _orch_mod.human_checkpoint | ||
| def _auto_approve(personas, cr, clf, bus): | ||
| _orig_chk(personas, cr, clf, bus) | ||
| print('\n[Auto-approve] Selecting option 1 (Approve).') | ||
| _passing_count['n'] += 1 | ||
| n = _passing_count['n'] | ||
| if n < _MIN_PASSING_BEFORE_APPROVE: | ||
| print(f'\n[Auto-approve] Passing iteration #{n} — continuing to collect ' | ||
| f'more candidates before picking the winner ({_MIN_PASSING_BEFORE_APPROVE} target).') | ||
| return HumanDecision( | ||
| action='recluster', | ||
| feedback=( | ||
| f'Exploration round {n}/{_MIN_PASSING_BEFORE_APPROVE}: keep iterating ' | ||
| 'to find a higher-F1 cluster set. Try a different algorithm or k.' | ||
| ), | ||
| ) | ||
| print(f'\n[Auto-approve] Collected {n} passing iterations — selecting best by F1.') | ||
| return HumanDecision(action='approve') | ||
| _orch_mod.human_checkpoint = _auto_approve |
…ssifier reporting Two bugs surfaced by the latest text run on text_articles.csv: 1. LogisticRegression.__init__() got an unexpected keyword argument 'multi_class' sklearn 1.5+ removed the `multi_class` kwarg (lbfgs handles multinomial automatically when n_classes > 2). `_build_model` now introspects the constructor signature and only passes `multi_class='auto'` on sklearn versions that still accept it. Works on both old and new sklearn. 2. AttributeError: 'NoneType' object has no attribute 'cv_accuracy' When the classifier crashed (per bug #1) the orchestrator set `clf = None`, but `human_checkpoint` dereferenced `classifier_result.cv_accuracy` / `.per_class_f1` unconditionally. Both call sites are now None-guarded: prints "(classifier failed this iteration — no F1 available)" when clf is missing, skips the per-class table + worst-performers section, and the persona table just shows " n/a" in the CV-F1 column. Together these prevent a single classifier failure from aborting the entire human-checkpoint stage and losing the whole iteration. Tests: - experiments/test_tabular_regression.py ✓ - experiments/test_text_clustering.py ✓ - experiments/test_text_e2e_orchestrator.py ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Ships the live web UI, the per-cluster feedback loop, and a tighter pipeline runtime that scores every iteration on a composite of F1, silhouette, and VIF — then auto-picks the best one across all 10 attempts.
What's in this PR
Pipeline runtime (
agents/,run_pipeline.py)state.update_best()now ranks byF1×100 + Silhouette×30 − log10(VIF)×5, runs on every iteration (not only Clarity-Gate passers), andstate.current_max_vif()reads the latest fs_history entry for the VIF term.PersonaNameris invoked withforce_proceed=Truewhen silhouette is below target so personas always exist for the classifier to score. Escalation rules apply after scoring, so a "bad" iteration still contributes to the best-iter tracker.state.best_*— even if the human checkpoint approves mid-loop, the all-time best iteration is what hitsoutputs/personas.json(and the Named Clusters tab). Console logs note when an earlier iteration was kept over the current one.run_pipeline.pyreturnsreclusteruntil at least three iterations have produced a passing classifier, so the composite-best comparison has real data to work with. Recluster path now calls_ask_parameter_tuningfor algorithm/k diversity.silhouette_targetplumbed from orchestrator intoClusteringAgent.run(was hardcoded 0.25);_relax_silhouette_targetemits a high-visibility Orchestrator agent_report so the 0.5 → 0.4 → 0.3 degrade lands as a warning chip.Interactive UI (
ui/,ui/static/,ui/templates/)OrchestratorBus: every agent step, LLM call, gate decision, and escalation streams over Server-Sent Events.outputs/user_feedback_log.jsonl, where the next run's Decision Maker prompts read it. Renames, merges, hints, and chat conclusions all influence subsequent iterations.applyChatConclusion('rename')now refreshesstate.draftafter the server update, so a subsequent Save Edits no longer overwrites the renamed title./api/explaindedup cache — 120 s in-memory cache so the multi-window recorder doesn't fire identical EvidenceExplainer LLM calls 3× per warning.Recording (
record_demo.py)intent,graph,log,convos,outputs,evidence,tokens,named) plus a bare-UIfullpseudo-region at 1280×800.--port,--base-url,--skip-pipeline-check,--stop-on-keyflags.Docs (
README.md,docs/screenshots/)docs/screenshots/:00_architecture.png— all gates surfaced (silhouette target, 40% size guard, Clarity Gate, F1 ≥ 0.70, VIF, composite best-iter)01_per_cluster_chat_and_save.png— Named Clusters tab with chat panel02_pca_with_adaptive_escalation.png— Data & Evidence with silhouette-miss warning03_adaptive_memory_drawer.png— Adaptive Memory drawerSnapshots
outputs/snapshot from the most recent runs (force-added past.gitignore).recordings/data_evidence.mp4+recordings/named_cluster.mp4(1-minute embeddable clips).Test plan
python run_pipeline.py --data data/raw/wholesale_customers/wholesale_customers.csv --ui-port 5090boots the UI on 5090 and runs end-to-end.outputs/personas.jsonreflects the iteration with the highest composite (F1 ↑ + Silhouette ↑ − VIF penalty), not necessarily the last iteration.user_feedback_log.jsonl.python record_demo.py --port 5090 --regions full graph tokensproduces the 3 webms.🤖 Generated with Claude Code