Spaces:
Sleeping
Sleeping
| """Amana — moderator review queue (human-in-the-loop). | |
| The AI triages each campaign and *recommends* APPROVE / REJECT / ESCALATE with cited rules, risk | |
| signals, and a rationale. A human moderator makes the **final** decision and can override the AI — | |
| overrides require a written reason. Every decision is appended to an audit log. There is no | |
| auto-decide path: the recommendation is never applied without a human click. | |
| streamlit run app.py | |
| """ | |
| from __future__ import annotations | |
| import streamlit as st | |
| from src.agent import _resolve_model, triage | |
| from src.audit import append_decision, decided_campaign_ids, load_decisions | |
| from src.campaigns import list_campaign_paths, load_campaign | |
| from src.config import CONFIG | |
| from src.policy import get_rule | |
| from src.schemas import Campaign, GatedDecision | |
| from src.store import count | |
| st.set_page_config(page_title="Amana — Triage Copilot", page_icon="🛡️", layout="wide") | |
| # Human button label -> the AI recommendation it corresponds to (used to detect overrides). | |
| HUMAN_TO_REC = {"Approve": "APPROVE", "Reject": "REJECT", "Request info": "ESCALATE"} | |
| # Recommendation -> Streamlit banner function (color). | |
| BADGE = {"APPROVE": st.success, "REJECT": st.error, "ESCALATE": st.warning} | |
| if "triage" not in st.session_state: | |
| st.session_state.triage = {} # campaign_id -> TriageDecision (cached so we bill once per campaign) | |
| # --------------------------------------------------------------------------- rendering | |
| def render_gate_banner(gd: GatedDecision) -> None: | |
| """Show when the deterministic policy gate adjusted the model's recommendation — the | |
| human/AI boundary made visible (and the reason it converges weak and strong models).""" | |
| if not gd.overrides: | |
| return | |
| rules = "; ".join(f"**{o.rule_id}** — {o.reason}" for o in gd.overrides) | |
| st.warning( | |
| f"⚖ **Policy gate adjusted the model's recommendation: " | |
| f"{gd.llm_recommendation} → {gd.decision.recommendation}.** " | |
| f"The deterministic policy layer enforces these rules in code, independent of the model:\n\n{rules}" | |
| ) | |
| def render_decision(gd: GatedDecision) -> None: | |
| render_gate_banner(gd) | |
| d = gd.decision | |
| BADGE.get(d.recommendation, st.info)(f"**{d.recommendation}** · confidence: **{d.confidence}**") | |
| if d.manipulation_detected: | |
| st.error("⚠ **Manipulation detected** — the campaign text contains instructions aimed at the " | |
| "reviewer/AI. Treated as data and escalated, never obeyed (DEC-6).") | |
| if d.rule_violations: | |
| st.markdown("**Policy rules cited**") | |
| for v in d.rule_violations: | |
| with st.expander(f"`{v.rule_id}` · {v.severity} — {v.evidence[:70]}"): | |
| st.markdown(f"**Evidence:** {v.evidence}") | |
| rule = get_rule(v.rule_id) | |
| st.markdown(f"**Rule text:** {rule.text if rule else '⚠ rule id not found in policy'}") | |
| if d.risk_signals: | |
| st.markdown("**Risk signals**") | |
| for s in d.risk_signals: | |
| st.markdown(f"- `{s.severity}` **{s.name}** — {s.detail}") | |
| if d.rationale: | |
| st.markdown("**Rationale**") | |
| st.write(d.rationale) | |
| if d.questions_for_submitter: | |
| st.markdown("**Needs verification / questions for submitter**") | |
| for q in d.questions_for_submitter: | |
| st.markdown(f"- {q}") | |
| elif d.confidence == "low": | |
| st.markdown("**Needs verification:** confidence is low — a human should review before deciding.") | |
| def render_human_controls(cid: str, campaign: Campaign, gd: GatedDecision, provider: str) -> None: | |
| d = gd.decision | |
| st.divider() | |
| st.markdown("#### Your decision _(final — the AI does not decide)_") | |
| reason = st.text_area("Reason / notes — **required to override the AI's recommendation**", | |
| key=f"reason_{cid}") | |
| b1, b2, b3 = st.columns(3) | |
| clicked = None | |
| if b1.button("✅ Approve", key=f"ap_{cid}", use_container_width=True): | |
| clicked = "Approve" | |
| if b2.button("⛔ Reject", key=f"rj_{cid}", use_container_width=True): | |
| clicked = "Reject" | |
| if b3.button("❓ Request info", key=f"ri_{cid}", use_container_width=True): | |
| clicked = "Request info" | |
| if not clicked: | |
| return | |
| human_rec = HUMAN_TO_REC[clicked] | |
| is_override = human_rec != d.recommendation | |
| if is_override and not reason.strip(): | |
| st.warning(f"You're overriding the AI's **{d.recommendation}** with **{human_rec}**. " | |
| "A written reason is required before this can be logged.") | |
| return | |
| append_decision({ | |
| "campaign_id": cid, | |
| "title": campaign.title, | |
| "ai_recommendation": d.recommendation, # final, gate-corrected | |
| "ai_llm_recommendation": gd.llm_recommendation, # what the model said before the gate | |
| "gate_overrides": [o.rule_id for o in gd.overrides], | |
| "ai_confidence": d.confidence, | |
| "provider": provider, | |
| "human_decision": human_rec, | |
| "is_override": is_override, | |
| "reason": reason.strip(), | |
| }) | |
| if is_override: | |
| st.success(f"Logged. You overrode the AI's {d.recommendation} with **{human_rec}** — reason recorded.") | |
| else: | |
| st.success(f"Logged: **{human_rec}** (agreed with the AI).") | |
| st.rerun() | |
| # --------------------------------------------------------------------------- sidebar | |
| with st.sidebar: | |
| st.title("🛡️ Amana") | |
| st.caption("Campaign Trust & Safety triage. **The AI recommends — you decide.**") | |
| provider = st.radio( | |
| "Triage model", | |
| ["anthropic", "ollama"], | |
| index=0 if CONFIG.llm_provider != "ollama" else 1, | |
| help="Anthropic = Claude (for the demo). Ollama = free local model (for dev).", | |
| ) | |
| with st.expander("⚖ Why a policy gate?"): | |
| st.caption( | |
| "The model only *recommends*. A deterministic **policy gate** then enforces the " | |
| "non-negotiable rules in code — sanctions (COMP-1), prompt injection (DEC-6), " | |
| "high-value review (COMP-2), reject-needs-evidence (DEC-2), low-confidence humility " | |
| "(DEC-5). It can only route a case **to a human** (→ ESCALATE), never approve or reject " | |
| "on its own. Because these rules live in code, the safety-critical behavior holds even " | |
| "on a weak local model — try the **ollama** option and watch the gate fire where the " | |
| "small model would otherwise slip." | |
| ) | |
| st.divider() | |
| n_rules = count(CONFIG.policy_collection) | |
| st.metric("Policy rules indexed", n_rules) | |
| st.metric("Precedent cases", count(CONFIG.cases_collection)) | |
| if n_rules == 0: | |
| st.warning("No index. Run `python -m scripts.build_index`.") | |
| st.divider() | |
| with st.expander("Decision history (audit log)"): | |
| decisions = load_decisions() | |
| if not decisions: | |
| st.caption("No decisions logged yet.") | |
| for rec in reversed(decisions[-25:]): | |
| tag = "⚠ override" if rec.get("is_override") else "✓ agreed" | |
| gate = " · ⚖ gate" if rec.get("gate_overrides") else "" | |
| st.markdown(f"`{str(rec.get('timestamp', ''))[:19]}` **{rec.get('campaign_id')}** — " | |
| f"AI {rec.get('ai_recommendation')} → human **{rec.get('human_decision')}** · {tag}{gate}") | |
| # --------------------------------------------------------------------------- queue + detail | |
| st.markdown("### Campaign review queue") | |
| paths = {p.stem: p for p in list_campaign_paths()} | |
| decided = decided_campaign_ids() | |
| cid = st.selectbox( | |
| "Select a campaign", | |
| list(paths), | |
| format_func=lambda c: ("✓ " if c in decided else "• ") + c, | |
| ) | |
| campaign = load_campaign(paths[cid]) | |
| left, right = st.columns(2) | |
| with left: | |
| st.subheader(campaign.title) | |
| st.caption(f"{campaign.category} · goal {campaign.goal_amount:.0f} {campaign.currency} · " | |
| f"beneficiary {campaign.beneficiary.name} ({campaign.beneficiary.country}) · " | |
| f"organizer verified: {campaign.organizer.verified}") | |
| st.write(campaign.story) | |
| if cid in decided: | |
| rec = decided[cid] | |
| st.info(f"Already decided: **{rec.get('human_decision')}** " | |
| f"({'override of AI' if rec.get('is_override') else 'agreed with AI'}).") | |
| with right: | |
| st.markdown("#### AI recommendation _(advisory — not a decision)_") | |
| decision = st.session_state.triage.get(cid) | |
| run_col, rerun_col = st.columns(2) | |
| if run_col.button("▶ Run AI triage", key=f"run_{cid}", type="primary", use_container_width=True): | |
| with st.spinner(f"Triaging with {provider}…"): | |
| try: | |
| decision = triage(campaign, model=_resolve_model(provider)) | |
| st.session_state.triage[cid] = decision | |
| except Exception as e: # surface provider/setup errors instead of a blank page | |
| st.error(f"Triage failed: {e}") | |
| decision = None | |
| if decision is not None and rerun_col.button("↻ Re-run", key=f"rerun_{cid}", use_container_width=True): | |
| st.session_state.triage.pop(cid, None) | |
| st.rerun() | |
| if decision is not None: | |
| render_decision(decision) | |
| render_human_controls(cid, campaign, decision, provider) | |
| else: | |
| st.caption("Click **Run AI triage** to get a recommendation, then make your decision.") | |