amana / app.py
Misbahuddin's picture
Pivot YouTube RAG scaffold into Amana — T&S triage copilot (Phases 1–3.6)
250666a
Raw
History Blame Contribute Delete
9.4 kB
"""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.")