import io import json import pandas as pd import streamlit as st from core import audit _ACTION_LABELS = { "criteria_extracted": "๐Ÿ“‹ Criteria Extracted", "bidder_processed": "๐Ÿ“ฅ Bidder Document Indexed", "criterion_evaluated": "โš–๏ธ Criterion Evaluated", "human_review_action": "๐Ÿ‘ค Human Review Action", "precomputed_fallback_used":"โš ๏ธ Fallback Used", "vision_ocr_invoked": "๐Ÿ‘๏ธ Vision OCR Invoked", "smoke_test": "๐Ÿงช Smoke Test", } _ACTION_CATEGORIES = { "criteria_extracted": "system", "bidder_processed": "system", "criterion_evaluated": "system", "human_review_action": "human", "precomputed_fallback_used": "warning", "vision_ocr_invoked": "system", } _VERDICT_ICONS = { "eligible": "โœ… Eligible", "not_eligible": "โŒ Not Eligible", "needs_review": "โš ๏ธ Needs Review", } def _make_summary(row: dict) -> str: action = row.get("action", "") bidder = row.get("bidder_id") or "" crit = row.get("criterion_id") or "" try: p = json.loads(row.get("payload_json") or "{}") except Exception: p = {} if action == "criteria_extracted": return f"Extracted {p.get('count', '?')} criteria from {p.get('source', 'tender PDF')}" if action == "bidder_processed": return f"{bidder} โ€” {p.get('doc_name', '?')} indexed ({p.get('chunk_count', '?')} chunks)" if action == "criterion_evaluated": verdict = _VERDICT_ICONS.get(p.get("verdict", ""), p.get("verdict", "?")) conf = p.get("combined_confidence", "?") conf_str = f"{float(conf):.0%}" if conf != "?" else "?" extracted = p.get("extracted_value", "") esc = p.get("escalation_reason", "") base = f"{bidder} / {crit} โ†’ {verdict} (confidence: {conf_str})" if extracted: base += f" | Extracted: {extracted}" if esc: base += f" | โš ๏ธ {esc}" return base if action == "human_review_action": taken = p.get("action_taken", "?").capitalize() orig = p.get("original_extracted_value", "") edited = p.get("edited_value", "") base = f"Officer {taken}: {bidder} / {crit}" if orig: base += f" | Original value: {orig}" if edited: base += f" โ†’ Edited to: {edited}" return base if action == "precomputed_fallback_used": return f"API unavailable โ€” pre-computed data used | {p.get('reason', '')}" if action == "vision_ocr_invoked": tc = p.get("tesseract_conf", "?") tc_str = f"{float(tc):.0%}" if tc != "?" else "?" return f"{bidder} page {p.get('page', '?')} โ€” Tesseract confidence {tc_str}, escalated to Vision LLM" return action def _category_color(category: str) -> str: return {"system": "๐Ÿ”ต", "human": "๐ŸŸข", "warning": "๐ŸŸก"}.get(category, "โšช") def render() -> None: st.header("Audit Log") st.caption( "Every system action and human decision is recorded here. " "This log is the compliance trail โ€” it can be exported and submitted as part of the evaluation record." ) # โ”€โ”€ Filters โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ col1, col2, col3 = st.columns(3) with col1: bidder_filter = st.selectbox( "Filter by bidder", options=["All", "bidder_a", "bidder_b", "bidder_c"], ) with col2: action_filter = st.selectbox( "Filter by action", options=["All"] + list(_ACTION_LABELS.keys()), format_func=lambda x: "All" if x == "All" else _ACTION_LABELS.get(x, x), ) with col3: if st.button("๐Ÿ—‘ Clear Log", type="secondary", use_container_width=True): st.session_state["confirm_clear_audit"] = True if st.session_state.get("confirm_clear_audit"): st.warning("This will permanently delete all audit entries. Are you sure?") c1, c2 = st.columns(2) if c1.button("Yes, clear everything", type="primary", use_container_width=True): audit.clear() st.session_state.pop("confirm_clear_audit", None) st.success("Audit log cleared.") st.rerun() if c2.button("Cancel", use_container_width=True): st.session_state.pop("confirm_clear_audit", None) st.rerun() filters: dict = {} if bidder_filter != "All": filters["bidder_id"] = bidder_filter if action_filter != "All": filters["action"] = action_filter rows = audit.query(filters if filters else None) if not rows: st.info("No audit entries yet. Run an evaluation to generate entries.") return # โ”€โ”€ Summary counts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ total = len(rows) human_actions = sum(1 for r in rows if r["action"] == "human_review_action") fallbacks = sum(1 for r in rows if r["action"] == "precomputed_fallback_used") vision_ocr = sum(1 for r in rows if r["action"] == "vision_ocr_invoked") m1, m2, m3, m4 = st.columns(4) m1.metric("Total entries", total) m2.metric("Human actions", human_actions) m3.metric("Fallback events", fallbacks) m4.metric("Vision OCR calls", vision_ocr) st.divider() # โ”€โ”€ Human-readable table โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ df = pd.DataFrame(rows) df["Action"] = df["action"].map(lambda x: _ACTION_LABELS.get(x, x)) df["Category"] = df["action"].map( lambda x: _category_color(_ACTION_CATEGORIES.get(x, "system")) ) df["Summary"] = df.apply(_make_summary, axis=1) df["Timestamp"] = df["ts"].str[:19].str.replace("T", " ") df["Actor"] = df["actor"] df["Bidder"] = df["bidder_id"].fillna("โ€”") df["Criterion"] = df["criterion_id"].fillna("โ€”") display = df[["Category", "Timestamp", "Action", "Bidder", "Criterion", "Summary", "Actor"]].copy() st.dataframe( display, use_container_width=True, hide_index=True, column_config={ "Category": st.column_config.TextColumn("", width="small"), "Timestamp": st.column_config.TextColumn("Timestamp", width="medium"), "Action": st.column_config.TextColumn("Action", width="medium"), "Bidder": st.column_config.TextColumn("Bidder", width="small"), "Criterion": st.column_config.TextColumn("Criterion", width="small"), "Summary": st.column_config.TextColumn("Summary", width="large"), "Actor": st.column_config.TextColumn("Actor", width="small"), }, ) # โ”€โ”€ Raw detail expander โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with st.expander("Raw payload data (for compliance / full detail)", expanded=False): raw_df = df[["Timestamp", "action", "actor", "bidder_id", "criterion_id", "payload_json"]].copy() raw_df.columns = ["Timestamp", "action", "actor", "bidder_id", "criterion_id", "payload_json"] st.dataframe(raw_df, use_container_width=True, hide_index=True) # โ”€โ”€ Export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ export_df = df[["Timestamp", "Action", "Actor", "Bidder", "Criterion", "Summary"]].copy() export_df["raw_payload"] = df["payload_json"] csv_buf = io.StringIO() export_df.to_csv(csv_buf, index=False) st.download_button( label="Export CSV", data=csv_buf.getvalue().encode("utf-8"), file_name="tenderiq_audit_log.csv", mime="text/csv", )