"""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.")