Spaces:
Sleeping
Sleeping
| """AutoSource dashboard — AG-UI-aligned event stream viewer + human approval gate. | |
| Modes: | |
| Live follow — follows a run being written right now (start one from the sidebar). | |
| Replay — plays any recorded run from traces/ turn by turn (zero API calls). | |
| Tabs: | |
| 📊 Mission control — the animated run view + AP2 approval gate. | |
| 🔬 Under the hood — agent system prompts, parsed transcripts, raw events, | |
| and what every module contributes. | |
| Run: streamlit run ui/app.py | |
| """ | |
| from __future__ import annotations | |
| import html | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| import time | |
| import uuid | |
| from datetime import datetime | |
| from pathlib import Path | |
| import streamlit as st | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| from core.config import PROJECT_ROOT, api_key, db_path, settings, traces_dir # noqa: E402 | |
| from core.hosted_runtime import ( # noqa: E402 | |
| acquire_live_slot, | |
| cleanup_expired_sessions, | |
| get_workspace, | |
| is_hosted, | |
| live_slot_status, | |
| new_session_id, | |
| release_live_slot, | |
| ) | |
| st.set_page_config(page_title="AutoSource", page_icon="🤝", layout="wide") | |
| # ---------------------------------------------------------------- css / anims | |
| # All custom components use EXPLICIT text colors on light backgrounds so they | |
| # stay readable in both Streamlit themes (dark theme made inherited text white). | |
| st.markdown(""" | |
| <style> | |
| @keyframes pulse { 0% {box-shadow:0 0 0 0 rgba(46,125,50,.55);} | |
| 70% {box-shadow:0 0 0 12px rgba(46,125,50,0);} | |
| 100% {box-shadow:0 0 0 0 rgba(46,125,50,0);} } | |
| @keyframes fadeup { from {opacity:0; transform:translateY(8px);} | |
| to {opacity:1; transform:translateY(0);} } | |
| .agent-card {border-radius:12px; padding:10px 12px; text-align:center; | |
| background:#f4f6f4; border:1px solid #d5d9d5; color:#1b2b1b;} | |
| .agent-card.active {border:2px solid #2e7d32; background:#eef7ee;} | |
| .agent-dot {display:inline-block; width:12px; height:12px; border-radius:50%; | |
| background:#9e9e9e; margin-right:6px;} | |
| .agent-card.active .agent-dot {background:#2e7d32; animation:pulse 1.4s infinite;} | |
| .agent-name {font-weight:700; font-size:0.95rem; color:#14261a;} | |
| .agent-status {font-size:0.75rem; color:#4a5a4a; min-height:2.2em;} | |
| .bubble {border-radius:14px; padding:10px 14px; margin:6px 0; max-width:78%; | |
| animation:fadeup .45s ease; font-size:0.92rem; line-height:1.35; color:#1c2620;} | |
| .bubble .who {font-size:0.72rem; font-weight:700; letter-spacing:.4px; | |
| text-transform:uppercase; color:#3c4a40; margin-bottom:2px;} | |
| .bubble .price {font-weight:800; font-size:1.0rem; margin-left:8px; color:#0d3311;} | |
| .bubble.buyer {background:#dff0e0; border:1px solid #b7dcb9; margin-right:auto;} | |
| .bubble.vendor {background:#fdeed8; border:1px solid #f3d3a4; margin-left:auto;} | |
| .bubble.system {background:#e8e2f5; border:1px solid #cbbfe8; margin:6px auto; | |
| max-width:92%; font-size:0.85rem;} | |
| .bubble.human {background:#dbeafe; border:1px solid #b3d4f5; margin:6px auto;} | |
| .toolchip {display:inline-block; background:#e7ebee; border:1px solid #c3ccd2; | |
| color:#26343d; border-radius:20px; padding:2px 12px; font-size:0.75rem; | |
| margin:3px 0; animation:fadeup .4s ease;} | |
| .flagchip {display:inline-block; background:#fdecea; border:1px solid #f5b8b1; | |
| color:#8c1d18; border-radius:20px; padding:1px 10px; font-size:0.7rem; | |
| margin-left:6px;} | |
| .winner-banner {background:linear-gradient(90deg,#2e7d32,#66bb6a); color:#ffffff; | |
| border-radius:12px; padding:14px 18px; font-size:1.05rem; font-weight:700; | |
| animation:fadeup .6s ease;} | |
| .console {background:#0d1117; color:#7ee787; border:1px solid #30363d; | |
| border-radius:8px; padding:12px 14px; font-family:ui-monospace,Consolas,monospace; | |
| font-size:0.78rem; line-height:1.45; white-space:pre-wrap; word-break:break-all; | |
| max-height:420px; overflow-y:auto;} | |
| .console .err {color:#ff7b72;} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ---------------------------------------------------------------- data access | |
| HOSTED = is_hosted() | |
| BUNDLED_TRACES = traces_dir() | |
| if HOSTED: | |
| if "runtime_session_id" not in st.session_state: | |
| st.session_state.runtime_session_id = new_session_id() | |
| cleanup_expired_sessions() | |
| WORKSPACE = get_workspace(st.session_state.runtime_session_id) | |
| LIVE_TRACES = WORKSPACE.traces | |
| LIVE_DB = WORKSPACE.db | |
| CHILD_ENV = WORKSPACE.child_env() | |
| else: | |
| WORKSPACE = None | |
| LIVE_TRACES = BUNDLED_TRACES | |
| LIVE_DB = db_path() | |
| CHILD_ENV = os.environ.copy() | |
| def reset_warehouse() -> subprocess.CompletedProcess: | |
| """Seed this visitor's warehouse without affecting any other session.""" | |
| return subprocess.run( | |
| [sys.executable, "-m", "scripts.init_db"], cwd=str(PROJECT_ROOT), | |
| env=CHILD_ENV, capture_output=True, text=True, timeout=30, | |
| ) | |
| if HOSTED and not LIVE_DB.exists(): | |
| init_result = reset_warehouse() | |
| if init_result.returncode != 0: | |
| st.error("Could not initialize your private demo warehouse. Please reload.\n\n" | |
| + (init_result.stderr or init_result.stdout)[-1200:]) | |
| st.stop() | |
| def list_runs() -> list[str]: | |
| paths = list(LIVE_TRACES.glob("*.jsonl")) | |
| if LIVE_TRACES != BUNDLED_TRACES: | |
| paths.extend(BUNDLED_TRACES.glob("*.jsonl")) | |
| by_id = {p.stem: p for p in paths | |
| if not p.stem.startswith(".") and p.stem != "llm_calls"} | |
| return sorted(by_id, key=lambda r: by_id[r].stat().st_mtime, reverse=True) | |
| def trace_root_for_run(run_id: str) -> Path: | |
| if (LIVE_TRACES / f"{run_id}.jsonl").exists(): | |
| return LIVE_TRACES | |
| return BUNDLED_TRACES | |
| def load_events(run_id: str) -> list[dict]: | |
| path = trace_root_for_run(run_id) / f"{run_id}.jsonl" | |
| if not path.exists(): | |
| return [] | |
| events = [] | |
| for line in path.read_text(encoding="utf-8").splitlines(): | |
| line = line.strip() | |
| if line: | |
| try: | |
| events.append(json.loads(line)) | |
| except json.JSONDecodeError: | |
| pass # tail of a line still being written | |
| return events | |
| def load_mandates(run_id: str) -> dict | None: | |
| path = trace_root_for_run(run_id) / f"{run_id}.mandates.json" | |
| if path.exists(): | |
| try: | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError: | |
| return None | |
| return None | |
| def write_decision(run_id: str, payload: dict) -> None: | |
| if not (LIVE_TRACES / f"{run_id}.jsonl").exists(): | |
| raise PermissionError("recorded replay runs are read-only") | |
| tmp = LIVE_TRACES / f".{run_id}.decision.tmp" | |
| tmp.write_text(json.dumps(payload), encoding="utf-8") | |
| tmp.replace(LIVE_TRACES / f"{run_id}.decision.json") | |
| def start_live_run(sku: str | None) -> str: | |
| """Launch scripts.run_demo as a detached subprocess; returns its run_id.""" | |
| session_tag = (st.session_state.runtime_session_id[:8] if HOSTED else "local") | |
| run_id = ("live-" + session_tag + "-" + datetime.now().strftime("%Y%m%d-%H%M%S") | |
| + "-" + uuid.uuid4().hex[:4]) | |
| args = [sys.executable, "-u", "-m", "scripts.run_demo", "--run-id", run_id] | |
| if sku: | |
| args += ["--sku", sku] | |
| token = None | |
| if HOSTED: | |
| token = acquire_live_slot(st.session_state.runtime_session_id, run_id) | |
| if token is None: | |
| raise RuntimeError("another visitor is currently negotiating") | |
| args += ["--live-lock-token", token] | |
| console = open(LIVE_TRACES / f"{run_id}.console.log", "w", encoding="utf-8") | |
| try: | |
| subprocess.Popen(args, cwd=str(PROJECT_ROOT), env=CHILD_ENV, | |
| stdout=console, stderr=subprocess.STDOUT) | |
| console.close() | |
| except Exception: | |
| console.close() | |
| if token: | |
| release_live_slot(token) | |
| raise | |
| return run_id | |
| def pending_gate_exists() -> bool: | |
| for path in LIVE_TRACES.glob("*.mandates.json"): | |
| try: | |
| if json.loads(path.read_text(encoding="utf-8"))["context"]["status"] == "pending": | |
| return True | |
| except (KeyError, json.JSONDecodeError, OSError): | |
| continue | |
| return False | |
| # ---------------------------------------------------------------- sidebar | |
| st.sidebar.title("🤝 AutoSource") | |
| st.sidebar.caption("Autonomous procurement & negotiation network") | |
| qp = st.query_params | |
| runs = list_runs() | |
| default_mode = 0 if qp.get("mode", "live") == "live" else 1 | |
| mode = st.sidebar.radio("Mode", ["Live follow", "Replay"], index=default_mode) | |
| requested_run = qp.get("run") | |
| if requested_run and runs and requested_run not in runs: | |
| if HOSTED: | |
| own_prefix = f"live-{st.session_state.runtime_session_id[:8]}-" | |
| pending_console = LIVE_TRACES / f"{requested_run}.console.log" | |
| if requested_run.startswith(own_prefix) and pending_console.exists(): | |
| st.sidebar.info(f"⏳ starting {requested_run} …") | |
| time.sleep(2) | |
| st.rerun() | |
| else: | |
| st.sidebar.warning("That run belongs to an expired or different " | |
| "browser session. Visitor runs are private.") | |
| del st.query_params["run"] | |
| st.rerun() | |
| else: | |
| st.sidebar.info(f"⏳ waiting for {requested_run} …") | |
| if st.sidebar.button("Cancel waiting"): | |
| del st.query_params["run"] | |
| st.rerun() | |
| time.sleep(2) | |
| st.rerun() | |
| if not runs: | |
| st.info("No runs recorded yet. Start one below or run: " | |
| "`python -m scripts.run_demo`") | |
| run_id = None | |
| else: | |
| default_run = requested_run if requested_run in runs else runs[0] | |
| run_id = st.sidebar.selectbox("Run", runs, index=runs.index(default_run)) | |
| # ---- live demo controls --------------------------------------------------- | |
| st.sidebar.divider() | |
| st.sidebar.subheader("🎮 Live demo") | |
| if HOSTED: | |
| st.sidebar.caption("Private visitor session · " | |
| f"`{st.session_state.runtime_session_id[:8]}`") | |
| if not api_key("GROQ_API_KEY"): | |
| st.sidebar.warning("Live mode is temporarily unavailable because the Space " | |
| "owner has not configured `GROQ_API_KEY`. Replay still works.") | |
| else: | |
| sku_options = (["SEED-MAIZE-01", "FERT-NPK-01", "(auto: first low-stock item)"] | |
| if HOSTED else | |
| ["(auto: first low-stock item)", "SEED-MAIZE-01", "FERT-NPK-01"]) | |
| sku_choice = st.sidebar.selectbox("Item to restock", sku_options) | |
| if HOSTED and sku_choice == "FERT-NPK-01": | |
| st.sidebar.caption("Stress-test item: vendor floors exceed its budget, so " | |
| "a safe walk-away is the expected result.") | |
| hosted_cfg = settings().get("hosted", {}) | |
| launches = int(st.session_state.get("live_launches", 0)) | |
| max_launches = int(hosted_cfg.get("max_runs_per_session", 3)) | |
| last_launch = float(st.session_state.get("last_live_launch", 0)) | |
| cooldown_left = max(0, int(hosted_cfg.get("cooldown_s", 120) | |
| - (time.time() - last_launch))) | |
| slot = live_slot_status() if HOSTED else None | |
| at_limit = HOSTED and launches >= max_launches | |
| has_pending_gate = pending_gate_exists() | |
| disabled_reason = None | |
| if has_pending_gate: | |
| disabled_reason = "Approve or reject your pending mandate before another run." | |
| elif slot: | |
| disabled_reason = "Another live negotiation is using the shared free-tier slot." | |
| elif cooldown_left > 0: | |
| disabled_reason = f"This session can launch again in {cooldown_left}s." | |
| elif at_limit: | |
| disabled_reason = (f"This browser session used its {max_launches} live runs. " | |
| "Replay remains available.") | |
| if disabled_reason: | |
| st.sidebar.info(disabled_reason) | |
| if st.sidebar.button("🚀 Trigger stock check & negotiate", type="primary", | |
| use_container_width=True, disabled=bool(disabled_reason)): | |
| sku = None if sku_choice.startswith("(") else sku_choice | |
| try: | |
| new_run = start_live_run(sku) | |
| except Exception as exc: # noqa: BLE001 | |
| st.sidebar.error(f"Could not start: {str(exc)[:240]}") | |
| else: | |
| st.session_state.live_launches = launches + 1 | |
| st.session_state.last_live_launch = time.time() | |
| st.query_params["run"] = new_run | |
| st.query_params["mode"] = "live" | |
| st.sidebar.success(f"started {new_run}") | |
| time.sleep(2) | |
| st.rerun() | |
| reset_blocked = bool(slot) or has_pending_gate | |
| if st.sidebar.button("🔄 Reset warehouse (seed data)", | |
| use_container_width=True, disabled=reset_blocked): | |
| out = reset_warehouse() | |
| st.sidebar.success("warehouse reset" if out.returncode == 0 | |
| else f"failed: {out.stdout}{out.stderr}") | |
| if reset_blocked: | |
| st.sidebar.caption("Finish the active negotiation/approval before resetting.") | |
| elif HOSTED: | |
| st.sidebar.caption(f"{launches}/{max_launches} live runs used in this session. " | |
| "Data expires automatically after 6 hours.") | |
| else: | |
| st.sidebar.caption("Live runs negotiate on real free-tier APIs — see " | |
| "docs/LIVE_DEMO.md") | |
| events_all = load_events(run_id) if run_id else [] | |
| TRACES = trace_root_for_run(run_id) if run_id else LIVE_TRACES | |
| run_is_owned = bool(run_id and (LIVE_TRACES / f"{run_id}.jsonl").exists()) | |
| if mode == "Replay" and run_id: | |
| st.sidebar.divider() | |
| speed = st.sidebar.slider("Replay speed (events/tick)", 1, 10, 2) | |
| if "replay_idx" not in st.session_state or st.session_state.get("replay_run") != run_id: | |
| st.session_state.replay_idx = 0 | |
| st.session_state.replay_run = run_id | |
| st.session_state.playing = False | |
| c1, c2, c3 = st.sidebar.columns(3) | |
| if c1.button("▶ Play"): | |
| st.session_state.playing = True | |
| if c2.button("⏸ Pause"): | |
| st.session_state.playing = False | |
| if c3.button("⏮ Reset"): | |
| st.session_state.replay_idx = 0 | |
| st.session_state.playing = False | |
| st.session_state.replay_idx = st.sidebar.slider( | |
| "Position", 0, max(len(events_all), 1), st.session_state.replay_idx) | |
| events = events_all[:st.session_state.replay_idx] | |
| else: | |
| events = events_all | |
| if run_id: | |
| st.sidebar.caption(f"{len(events)} events · auto-refreshing") | |
| if run_id is None: | |
| st.stop() | |
| # ---------------------------------------------------------------- derive state | |
| AGENTS = [("auditor", "🔍 Auditor"), ("sourcing", "📇 Sourcing"), | |
| ("buyer", "🤝 Buyer"), ("vendor", "🏪 Vendors"), | |
| ("settlement", "⚖️ Settlement")] | |
| def agent_key(actor: str) -> str: | |
| return "vendor" if actor.startswith("vendor") else actor | |
| last_status: dict[str, str] = {} | |
| for e in events: | |
| k = agent_key(e["actor"]) | |
| if e["type"] == "STATE_UPDATE" and "status" in e["data"]: | |
| last_status[k] = str(e["data"]["status"]).replace("_", " ") | |
| elif e["type"] == "TEXT_MESSAGE": | |
| last_status[k] = "speaking…" | |
| active = agent_key(events[-1]["actor"]) if events else None | |
| pipeline_running = bool(events) and not any( | |
| e["type"] == "STATE_UPDATE" and e["data"].get("status") in | |
| ("complete", "rejected", "pipeline_done", "approval_timeout") | |
| for e in events[-6:]) | |
| meta = {"budget": None, "best_ask": None, "round": None, "vendor": None} | |
| deals, winner, requests_seen, active_sku, transcript_refs = [], None, [], None, [] | |
| for e in events: | |
| d = e["data"] | |
| if e["type"] == "STATE_UPDATE": | |
| if "buyer_offer" in d: | |
| meta["round"], meta["best_ask"] = d.get("round"), d.get("vendor_ask") | |
| meta["vendor"] = d.get("vendor") | |
| if "winner" in d and d.get("status") == "pipeline_done": | |
| winner = d["winner"] | |
| if "shortlist" in d: | |
| meta["budget"] = d["shortlist"].get("max_unit_budget") | |
| active_sku = d["shortlist"].get("sku") | |
| if d.get("status") == "negotiation_done": | |
| deals.append(d) | |
| if d.get("transcript_ref"): | |
| transcript_refs.append(d["transcript_ref"]) | |
| if e["type"] == "TEXT_MESSAGE" and "request" in d: | |
| requests_seen.append(d["request"]) | |
| request = next((r for r in requests_seen if r["sku"] == active_sku), | |
| requests_seen[0] if requests_seen else None) | |
| # ---------------------------------------------------------------- tabs | |
| tab_main, tab_tech = st.tabs(["📊 Mission control", "🔬 Under the hood"]) | |
| with tab_main: | |
| cols = st.columns(len(AGENTS)) | |
| for col, (key, label) in zip(cols, AGENTS): | |
| is_active = key == active and (mode == "Replay" or pipeline_running) | |
| col.markdown( | |
| f"""<div class="agent-card {'active' if is_active else ''}"> | |
| <span class="agent-dot"></span><span class="agent-name">{label}</span> | |
| <div class="agent-status">{last_status.get(key, 'idle')}</div></div>""", | |
| unsafe_allow_html=True) | |
| st.divider() | |
| m1, m2, m3, m4, m5 = st.columns(5) | |
| list_price = request["list_price"] if request else None | |
| m1.metric("Item", request["sku"] if request else "—") | |
| m2.metric("List price", f"{list_price}" if list_price else "—") | |
| m3.metric("Hidden budget 🤫", f"{request['max_unit_budget']}" if request else "—") | |
| if winner and winner.get("agreed_unit_price") and list_price: | |
| saved = round((list_price - winner["agreed_unit_price"]) / list_price * 100, 1) | |
| m4.metric("Winning price", winner["agreed_unit_price"], | |
| delta=f"-{saved}% vs list", delta_color="inverse") | |
| else: | |
| m4.metric("Current ask", meta["best_ask"] or "—") | |
| m5.metric("Deals settled", | |
| f"{sum(1 for d in deals if d.get('settled'))}/{len(deals)}" | |
| if deals else "—") | |
| left, right = st.columns([3, 2]) | |
| with left: | |
| st.subheader("Negotiation timeline") | |
| box = st.container(height=520) | |
| with box: | |
| for e in events: | |
| d, actor = e["data"], e["actor"] | |
| if e["type"] == "TEXT_MESSAGE": | |
| cls = ("buyer" if actor == "buyer" else | |
| "vendor" if actor.startswith("vendor") else | |
| "human" if actor == "human" else "system") | |
| price = f'<span class="price">@{d["price"]}</span>' \ | |
| if d.get("price") is not None else "" | |
| flags = "".join( | |
| f'<span class="flagchip">🛡 {html.escape(str(f))}</span>' | |
| for f in d.get("flags", []) | |
| if f.startswith(("BLOCKED", "clamped", "non_monotonic", | |
| "premature"))) | |
| who = html.escape(actor.upper()) | |
| if d.get("round"): | |
| who += f" · R{d['round']}" | |
| st.markdown( | |
| f'<div class="bubble {cls}"><div class="who">{who}</div>' | |
| f'{html.escape(str(d["text"]))}{price}{flags}</div>', | |
| unsafe_allow_html=True) | |
| elif e["type"] == "TOOL_CALL": | |
| st.markdown(f'<span class="toolchip">🔧 {html.escape(actor)} → MCP ' | |
| f'{html.escape(str(d["tool"]))}()</span>', | |
| unsafe_allow_html=True) | |
| with right: | |
| st.subheader("Deals") | |
| if deals: | |
| rows = [{"vendor": d.get("vendor_id"), "outcome": d.get("outcome"), | |
| "price": d.get("agreed_unit_price"), "rounds": d.get("rounds")} | |
| for d in deals] | |
| st.dataframe(rows, use_container_width=True, hide_index=True) | |
| else: | |
| st.caption("No negotiations finished yet.") | |
| if winner: | |
| st.markdown( | |
| f'<div class="winner-banner">🏆 Winner: {winner["vendor_id"]} @ ' | |
| f'{winner["agreed_unit_price"]}/unit · total ' | |
| f'{winner["landed_cost"]}</div>', unsafe_allow_html=True) | |
| mandates = load_mandates(run_id) | |
| if mandates: | |
| ctx = mandates["context"] | |
| st.subheader("AP2 mandate") | |
| if ctx["status"] == "pending": | |
| st.warning( | |
| f"**Approval required** — {ctx['vendor_name']} · " | |
| f"{mandates['cart']['qty']} × {mandates['cart']['sku']} @ " | |
| f"{mandates['cart']['unit_price']} = " | |
| f"**{mandates['cart']['total']}** " | |
| f"(saves {ctx['savings_pct']}% vs list)") | |
| with st.expander("Intent · Cart · Payment (AP2 trio)"): | |
| st.json(mandates["intent"]) | |
| st.json(mandates["cart"]) | |
| st.json(mandates["payment"]) | |
| if run_is_owned: | |
| b1, b2 = st.columns(2) | |
| if b1.button("✅ Approve", type="primary", use_container_width=True): | |
| write_decision(run_id, {"decision": "approve"}) | |
| st.toast("Approved — signing cart mandate") | |
| time.sleep(1) | |
| st.rerun() | |
| if b2.button("❌ Reject", use_container_width=True): | |
| write_decision(run_id, {"decision": "reject"}) | |
| st.toast("Rejected") | |
| time.sleep(1) | |
| st.rerun() | |
| new_price = st.number_input( | |
| "…or change max unit price", | |
| value=float(mandates["intent"]["max_unit_price"]), | |
| step=0.05, format="%.2f") | |
| if st.button("💰 Change budget & re-evaluate", | |
| use_container_width=True): | |
| write_decision(run_id, {"decision": "change_budget", | |
| "new_max_unit_price": new_price}) | |
| st.rerun() | |
| else: | |
| st.info("This is a read-only recorded run. Start a new live run " | |
| "to make an approval decision.") | |
| elif ctx["status"] == "approved": | |
| po = ctx.get("po", {}) | |
| st.success(f"✅ PO **{po.get('po_id', '—')}** written via MCP · " | |
| f"total {po.get('total', '—')} · MOCK payment " | |
| f"authorized. Saved {ctx['savings_pct']}% vs list price.") | |
| st.balloons() | |
| elif ctx["status"] == "rejected": | |
| st.error("❌ Purchase rejected by the human. Nothing was written.") | |
| # ---------------------------------------------------------------- tech tab | |
| with tab_tech: | |
| sub_arch, sub_llm, sub_console, sub_events, sub_db, sub_health = st.tabs( | |
| ["🧭 Architecture & prompts", "🧠 LLM calls", "🖥 Console & files", | |
| "📡 Events & transcripts", "🗄 Warehouse DB", "🩺 Health & config"]) | |
| # ---------------- architecture + prompts ---------------- | |
| with sub_arch: | |
| st.markdown("### How AutoSource fits together") | |
| st.markdown(""" | |
| | Stage | Module | What it contributes | | |
| |---|---|---| | |
| | 🗄 Warehouse | `mcp_server/server.py` | FastMCP server — the **only** code that touches SQLite. 5 tools; `cost_floor` never leaves it | | |
| | 🔍 Detect | `agents/auditor/auditor.py` | Deterministic shortage scan over MCP → `ProcurementRequest` (budget = list × 0.9) | | |
| | 📡 Hand-off | `core/a2a_layer.py` | A2A protocol: Agent Cards + JSON-RPC `message/send` over HTTP (official a2a-sdk types) | | |
| | 📇 Rank | `agents/sourcing/` | Deterministic vendor score + Gemini re-rank + `vendor_memory` learning | | |
| | 🤝 Negotiate | `core/negotiation.py` | The referee: state machine, **hard validators** (budget/floor/monotonicity), transcripts | | |
| | 🗣 Speak | `agents/buyer/` · `agents/vendor/` | LLM negotiators with **hidden thresholds** in system prompts (below) | | |
| | ⚖️ Settle | `agents/settlement/` | Deterministic winner (landed cost → reliability → lead time) + memory write-back | | |
| | 🔏 Gate | `protocols/ap2/` + `orchestrator/gate.py` | Intent→Cart→Payment mandates, hash-bound; human approve → verify → PO | | |
| | 🚦 Router | `core/router.py` | Token-bucket RPM limit, backoff on 429, Groq key rotation, provider failover | | |
| | 📈 Prove | `eval/` | 20+ scenario harness, savings chart, LLM-judge; asserts 0 violations | | |
| """) | |
| st.markdown("### Agent system prompts (the real ones)") | |
| st.caption("Live imports from the agent source — what the models actually " | |
| "see. The CONFIDENTIAL lines are the hidden thresholds; " | |
| "deterministic validators enforce them even if a model ignores " | |
| "its prompt.") | |
| try: | |
| from agents.buyer.buyer import SYSTEM_TMPL as BUYER_TMPL | |
| from agents.vendor.vendor import PERSONA_TACTICS | |
| from agents.vendor.vendor import SYSTEM_TMPL as VENDOR_TMPL | |
| with st.expander("🤝 Buyer — 'shrewd but fair procurement officer'"): | |
| st.code(BUYER_TMPL, language="text") | |
| with st.expander("🏪 Vendor — persona-driven seller"): | |
| st.code(VENDOR_TMPL, language="text") | |
| st.markdown("**Persona tactics injected per vendor:**") | |
| for persona, tactics in PERSONA_TACTICS.items(): | |
| st.markdown(f"- **{persona}** — {tactics}") | |
| except ImportError: | |
| st.info("Agent sources not bundled in this deployment.") | |
| try: | |
| from agents.sourcing.ranking import RERANK_SYSTEM, WEIGHTS | |
| with st.expander("📇 Sourcing — deterministic score + LLM re-rank"): | |
| st.markdown(f"**Score weights (code):** `{WEIGHTS}`") | |
| st.code(RERANK_SYSTEM, language="text") | |
| except ImportError: | |
| pass | |
| try: | |
| from eval.judge import SYSTEM as JUDGE_SYSTEM | |
| with st.expander("⚖️ LLM-judge — transcript auditor"): | |
| st.code(JUDGE_SYSTEM, language="text") | |
| except ImportError: | |
| pass | |
| # ---------------- LLM call log ---------------- | |
| with sub_llm: | |
| st.markdown("### Every LLM call the router made") | |
| st.caption("Full system prompt, user message, and raw response per call — " | |
| "logged by `core/router.py` to `traces/llm_calls.jsonl`. " | |
| "Failures (429s, failovers) appear in red.") | |
| call_log = TRACES / "llm_calls.jsonl" | |
| calls: list[dict] = [] | |
| if call_log.exists(): | |
| for line in call_log.read_text(encoding="utf-8").splitlines(): | |
| try: | |
| calls.append(json.loads(line)) | |
| except json.JSONDecodeError: | |
| pass | |
| # scope to this run's time window when possible | |
| if events and calls: | |
| t_start, t_end = events[0]["ts"] - 5, events[-1]["ts"] + 90 | |
| run_calls = [c for c in calls if t_start <= c.get("ts", 0) <= t_end] | |
| else: | |
| run_calls = calls | |
| c1, c2, c3 = st.columns(3) | |
| scope = c1.radio("Scope", ["This run's window", "All calls"], horizontal=True) | |
| shown_calls = run_calls if scope == "This run's window" else calls | |
| roles = sorted({c.get("role", "?") for c in shown_calls}) | |
| role_pick = c2.multiselect("Agent role", roles, default=roles) | |
| only_fail = c3.checkbox("Failures only") | |
| shown_calls = [c for c in shown_calls if c.get("role") in role_pick | |
| and (not only_fail or not c.get("ok", True))] | |
| st.caption(f"{len(shown_calls)} calls shown (log has {len(calls)} total)") | |
| for i, c in enumerate(reversed(shown_calls[-80:])): | |
| ok = c.get("ok", True) | |
| ts = datetime.fromtimestamp(c["ts"]).strftime("%H:%M:%S") if c.get("ts") else "—" | |
| head = (f'{"✅" if ok else "❌"} {ts} · {c.get("role")} → ' | |
| f'{c.get("provider")} · {c.get("latency_ms", "?")} ms' | |
| + ("" if ok else " · FAILED")) | |
| with st.expander(head): | |
| st.markdown("**System prompt**") | |
| st.code(c.get("system", ""), language="text") | |
| st.markdown("**User message**") | |
| st.code(json.dumps(c.get("messages", []), indent=2, | |
| ensure_ascii=False), language="json") | |
| st.markdown("**Response**" if ok else "**Error**") | |
| st.code(c.get("response", c.get("error", "")), language="text") | |
| # ---------------- console + file browser ---------------- | |
| with sub_console: | |
| st.markdown("### Run console (subprocess stdout/stderr)") | |
| console_path = TRACES / f"{run_id}.console.log" | |
| if console_path.exists(): | |
| lines = console_path.read_text(encoding="utf-8", | |
| errors="replace").splitlines() | |
| body = "\n".join(lines[-120:]) or "(empty)" | |
| st.markdown(f'<div class="console">{html.escape(body)}</div>', | |
| unsafe_allow_html=True) | |
| st.caption(f"last 120 of {len(lines)} lines · traces/{run_id}.console.log") | |
| else: | |
| st.info("No console capture for this run — console logs exist only for " | |
| "runs started from the 🎮 Live demo button. Terminal-started " | |
| "runs print to your terminal.") | |
| st.markdown("### Trace file browser") | |
| files = sorted(TRACES.rglob("*"), key=lambda p: p.stat().st_mtime, | |
| reverse=True) | |
| files = [f for f in files if f.is_file() and not f.name.startswith(".")] | |
| names = [str(f.relative_to(TRACES)) for f in files[:60]] | |
| pick = st.selectbox("File", names) if names else None | |
| if pick: | |
| fpath = TRACES / pick | |
| size_kb = fpath.stat().st_size / 1024 | |
| st.caption(f"{size_kb:.1f} KB · modified " | |
| f"{datetime.fromtimestamp(fpath.stat().st_mtime):%H:%M:%S}") | |
| raw = fpath.read_text(encoding="utf-8", errors="replace") | |
| if fpath.suffix == ".json": | |
| try: | |
| st.json(json.loads(raw)) | |
| except json.JSONDecodeError: | |
| st.code(raw[-8000:], language="text") | |
| else: | |
| tail = "\n".join(raw.splitlines()[-150:]) | |
| st.markdown(f'<div class="console">{html.escape(tail)}</div>', | |
| unsafe_allow_html=True) | |
| # ---------------- events + transcripts ---------------- | |
| with sub_events: | |
| st.markdown("### Negotiation transcripts (parsed)") | |
| st.caption("Each turn stores the raw message AND the parsed price, so " | |
| "fabricated numbers can be caught. 🛡 flags = validator actions.") | |
| if transcript_refs: | |
| for ref in transcript_refs: | |
| ref_path = Path(ref) | |
| tpath = (ref_path if ref_path.is_absolute() | |
| else TRACES.parent / ref_path) | |
| if not tpath.exists(): | |
| tpath = TRACES / "negotiations" / ref_path.name | |
| if not tpath.exists(): | |
| continue | |
| t = json.loads(tpath.read_text(encoding="utf-8")) | |
| m, res = t["meta"], t["result"] | |
| label = (f"{m['vendor_id']} — {res['outcome']} " | |
| f"{'@ ' + str(res['agreed_unit_price']) if res['agreed_unit_price'] else ''} " | |
| f"(budget {m['budget']} · floor {m['floor']} 🤫)") | |
| with st.expander(label): | |
| st.dataframe( | |
| [{"actor": turn["actor"], "action": turn["action"], | |
| "price": turn["price"], "message": turn["raw"], | |
| "🛡 flags": ", ".join(turn["flags"])} | |
| for turn in t["turns"]], | |
| use_container_width=True, hide_index=True) | |
| else: | |
| st.caption("No transcripts in this run yet.") | |
| st.markdown("### Raw AG-UI event stream") | |
| type_filter = st.multiselect( | |
| "Event types", | |
| ["TEXT_MESSAGE", "STATE_UPDATE", "TOOL_CALL", "UI_COMPONENT"], | |
| default=["TOOL_CALL", "UI_COMPONENT"]) | |
| shown = [e for e in events if e["type"] in type_filter] | |
| st.caption(f"{len(shown)} of {len(events)} events (traces/{run_id}.jsonl)") | |
| for e in shown[-60:]: | |
| with st.expander( | |
| f'{e["type"]} · {e["actor"]} · {e.get("iso_ts", "")[11:19]}'): | |
| st.json(e["data"]) | |
| st.markdown("### Router telemetry (from this run)") | |
| stats_events = [e["data"]["router_stats"] for e in events | |
| if e["type"] == "STATE_UPDATE" | |
| and "router_stats" in e["data"]] | |
| if stats_events: | |
| s = stats_events[-1] | |
| r1, r2, r3, r4 = st.columns(4) | |
| r1.metric("LLM calls", s.get("calls", 0)) | |
| r2.metric("Tokens", f'{s.get("prompt_tokens", 0) + s.get("completion_tokens", 0):,}') | |
| r3.metric("Failovers", s.get("failovers", 0)) | |
| r4.metric("Providers", ", ".join(s.get("by_provider", {}).keys()) or "—") | |
| if s.get("last_errors"): | |
| st.error("Last provider errors: " | |
| + json.dumps(s["last_errors"], indent=2)) | |
| else: | |
| st.caption("No router telemetry events in this run " | |
| "(recorded before this feature, or no LLM calls yet).") | |
| # ---------------- warehouse DB inspector ---------------- | |
| with sub_db: | |
| st.markdown("### Warehouse DB (read-only debug view)") | |
| st.caption("⚠ Debug panel only. Agents NEVER touch this DB directly — " | |
| "they go through the MCP server's five tools. This inspector " | |
| "reads the same SQLite file so you can verify what the tools did.") | |
| try: | |
| import sqlite3 | |
| if LIVE_DB.exists(): | |
| conn = sqlite3.connect(f"file:{LIVE_DB}?mode=ro", uri=True) | |
| for table in ("inventory", "purchase_orders", "vendor_memory", | |
| "suppliers"): | |
| with st.expander(f"table: {table}", | |
| expanded=(table == "purchase_orders")): | |
| import pandas as pd | |
| df = pd.read_sql_query(f"SELECT * FROM {table}", conn) # noqa: S608 — fixed table names | |
| if table == "suppliers": | |
| df = df.drop(columns=["cost_floor"]) | |
| st.caption("cost_floor hidden — same rule as the MCP " | |
| "server: buyers must never see it.") | |
| st.dataframe(df, use_container_width=True, hide_index=True) | |
| conn.close() | |
| else: | |
| st.info("Your private warehouse has not been initialized yet.") | |
| except Exception as e: # noqa: BLE001 | |
| st.warning(f"DB inspector unavailable: {e}") | |
| # ---------------- health & config ---------------- | |
| with sub_health: | |
| st.markdown("### API keys (presence only — values never shown)") | |
| for key_name in ("GROQ_API_KEY", "GROQ_API_KEY_2", "GEMINI_API_KEY", | |
| "OPENROUTER_API_KEY", "HF_TOKEN"): | |
| st.markdown(f"- `{key_name}`: " | |
| + ("✅ set" if api_key(key_name) else "❌ missing")) | |
| if api_key("GROQ_API_KEY") and not HOSTED: | |
| st.markdown("### Provider health check") | |
| st.caption("Sends one tiny live request per provider.") | |
| if st.button("🩺 Test providers now"): | |
| from core.router import _PROVIDER_CLASSES | |
| for pname, cls in _PROVIDER_CLASSES.items(): | |
| try: | |
| provider = cls() | |
| provider.chat("Reply with JSON only.", | |
| [{"role": "user", | |
| "content": 'reply with {"ok": true}'}], | |
| 0.1, True) | |
| st.success(f"{pname}: OK") | |
| except Exception as e: # noqa: BLE001 | |
| st.error(f"{pname}: {str(e)[:220]}") | |
| elif HOSTED: | |
| st.caption("Live provider pings are disabled on the public Space to " | |
| "protect the shared free-tier quota. Pipeline calls still " | |
| "report provider health in Router telemetry.") | |
| st.markdown("### Runtime configuration (config/settings.yaml)") | |
| cfg_path = PROJECT_ROOT / "config" / "settings.yaml" | |
| if cfg_path.exists(): | |
| st.code(cfg_path.read_text(encoding="utf-8"), language="yaml") | |
| # ---------------------------------------------------------------- refresh loop | |
| if mode == "Live follow" and pipeline_running: | |
| time.sleep(2) | |
| st.rerun() | |
| elif mode == "Replay" and st.session_state.get("playing"): | |
| if st.session_state.replay_idx < len(events_all): | |
| st.session_state.replay_idx = min(len(events_all), | |
| st.session_state.replay_idx + speed) | |
| time.sleep(0.5) | |
| st.rerun() | |
| else: | |
| st.session_state.playing = False | |