mirror of
https://github.com/katanemo/plano.git
synced 2026-05-10 08:12:48 +02:00
simplifying ui
This commit is contained in:
parent
eb7d7be76c
commit
43799ded85
1 changed files with 91 additions and 154 deletions
|
|
@ -1,13 +1,11 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
PLANO_ENDPOINT = os.getenv("PLANO_ENDPOINT", "http://localhost:8001/v1")
|
PLANO_ENDPOINT = os.getenv("PLANO_ENDPOINT", "http://localhost:8001/v1")
|
||||||
CASE_SERVICE_URL = "http://localhost:10540"
|
|
||||||
|
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
page_title="Credit Risk Case Copilot",
|
page_title="Credit Risk Case Copilot",
|
||||||
|
|
@ -18,7 +16,7 @@ st.set_page_config(
|
||||||
|
|
||||||
|
|
||||||
# Load scenarios
|
# Load scenarios
|
||||||
def load_scenario(scenario_file):
|
def load_scenario(scenario_file: str):
|
||||||
"""Load scenario JSON from file."""
|
"""Load scenario JSON from file."""
|
||||||
try:
|
try:
|
||||||
with open(scenario_file, "r") as f:
|
with open(scenario_file, "r") as f:
|
||||||
|
|
@ -30,13 +28,15 @@ def load_scenario(scenario_file):
|
||||||
# Initialize session state
|
# Initialize session state
|
||||||
if "assessment_result" not in st.session_state:
|
if "assessment_result" not in st.session_state:
|
||||||
st.session_state.assessment_result = None
|
st.session_state.assessment_result = None
|
||||||
if "case_id" not in st.session_state:
|
if "raw_result" not in st.session_state:
|
||||||
st.session_state.case_id = None
|
st.session_state.raw_result = None
|
||||||
|
if "application_json" not in st.session_state:
|
||||||
|
st.session_state.application_json = "{}"
|
||||||
|
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
st.title("🏦 Credit Risk Case Copilot")
|
st.title("🏦 Credit Risk Case Copilot")
|
||||||
st.markdown("**AI-Powered Credit Risk Assessment & Case Management**")
|
st.markdown("A minimal UI for the Plano + CrewAI credit risk demo.")
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
# Sidebar
|
# Sidebar
|
||||||
|
|
@ -67,27 +67,32 @@ with st.sidebar:
|
||||||
# JSON input area
|
# JSON input area
|
||||||
application_json = st.text_area(
|
application_json = st.text_area(
|
||||||
"Loan Application JSON",
|
"Loan Application JSON",
|
||||||
value=st.session_state.get("application_json", "{}"),
|
value=st.session_state.application_json,
|
||||||
height=400,
|
height=380,
|
||||||
help="Paste or edit loan application JSON",
|
help="Paste or edit loan application JSON",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assess button
|
col_a, col_b = st.columns(2)
|
||||||
|
|
||||||
|
with col_a:
|
||||||
if st.button("🔍 Assess Risk", type="primary", use_container_width=True):
|
if st.button("🔍 Assess Risk", type="primary", use_container_width=True):
|
||||||
try:
|
try:
|
||||||
# Parse JSON
|
|
||||||
application_data = json.loads(application_json)
|
application_data = json.loads(application_json)
|
||||||
|
|
||||||
# Call Plano orchestrator
|
|
||||||
with st.spinner("Running risk assessment..."):
|
with st.spinner("Running risk assessment..."):
|
||||||
response = httpx.post(
|
response = httpx.post(
|
||||||
f"{PLANO_ENDPOINT}/chat/completions",
|
f"{PLANO_ENDPOINT}/chat/completions",
|
||||||
json={
|
json={
|
||||||
"model": "gpt-4o",
|
# Use risk_reasoning if you’re standardizing on aliases.
|
||||||
|
# If you want plain OpenAI model routing, set "gpt-4o".
|
||||||
|
"model": "risk_reasoning",
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": f"Assess credit risk for this loan application:\n\n{json.dumps(application_data, indent=2)}",
|
"content": (
|
||||||
|
"Assess credit risk for this loan application:\n\n"
|
||||||
|
f"{json.dumps(application_data, indent=2)}"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -95,10 +100,12 @@ with st.sidebar:
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
result = response.json()
|
raw = response.json()
|
||||||
content = result["choices"][0]["message"]["content"]
|
st.session_state.raw_result = raw
|
||||||
|
|
||||||
# Extract JSON from response
|
content = raw["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
# Extract JSON block from response
|
||||||
if "```json" in content:
|
if "```json" in content:
|
||||||
json_start = content.index("```json") + 7
|
json_start = content.index("```json") + 7
|
||||||
json_end = content.index("```", json_start)
|
json_end = content.index("```", json_start)
|
||||||
|
|
@ -107,8 +114,16 @@ with st.sidebar:
|
||||||
st.session_state.assessment_result = assessment
|
st.session_state.assessment_result = assessment
|
||||||
st.success("✅ Risk assessment complete!")
|
st.success("✅ Risk assessment complete!")
|
||||||
else:
|
else:
|
||||||
st.error("Could not parse assessment result")
|
st.session_state.assessment_result = None
|
||||||
|
st.error(
|
||||||
|
"Could not parse JSON assessment from the agent response."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
st.session_state.assessment_result = None
|
||||||
|
st.session_state.raw_result = {
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"text": response.text,
|
||||||
|
}
|
||||||
st.error(f"Error: {response.status_code} - {response.text}")
|
st.error(f"Error: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
|
|
@ -116,151 +131,73 @@ with st.sidebar:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"Error: {str(e)}")
|
st.error(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
with col_b:
|
||||||
|
if st.button("🧹 Clear", use_container_width=True):
|
||||||
|
st.session_state.assessment_result = None
|
||||||
|
st.session_state.raw_result = None
|
||||||
|
st.session_state.application_json = "{}"
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
# Main content area
|
# Main content area
|
||||||
if st.session_state.assessment_result:
|
if st.session_state.assessment_result:
|
||||||
result = st.session_state.assessment_result
|
result = st.session_state.assessment_result
|
||||||
|
|
||||||
# Risk summary
|
st.header("Decision")
|
||||||
st.header("Risk Assessment Summary")
|
|
||||||
|
|
||||||
col1, col2, col3, col4 = st.columns(4)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
risk_color = {"LOW": "🟢", "MEDIUM": "🟡", "HIGH": "🔴"}
|
risk_color = {"LOW": "🟢", "MEDIUM": "🟡", "HIGH": "🔴"}
|
||||||
st.metric(
|
risk_band = result.get("risk_band", "UNKNOWN")
|
||||||
"Risk Band",
|
st.metric("Risk Band", f"{risk_color.get(risk_band, '⚪')} {risk_band}")
|
||||||
f"{risk_color.get(result['risk_band'], '⚪')} {result['risk_band']}",
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.metric("Confidence", f"{result['confidence']:.1%}")
|
confidence = result.get("confidence", 0.0)
|
||||||
|
try:
|
||||||
|
st.metric("Confidence", f"{float(confidence):.0%}")
|
||||||
|
except Exception:
|
||||||
|
st.metric("Confidence", str(confidence))
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
st.metric("Recommended Action", result["recommended_action"])
|
st.metric("Recommended Action", result.get("recommended_action", "REVIEW"))
|
||||||
|
|
||||||
with col4:
|
|
||||||
st.metric("Documents Required", len(result.get("required_documents", [])))
|
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
# Tabbed interface
|
tab1, tab2, tab3 = st.tabs(["🧾 Summary", "📝 Decision Memo", "🧪 Raw Output"])
|
||||||
tab1, tab2, tab3, tab4, tab5 = st.tabs(
|
|
||||||
[
|
|
||||||
"📊 Risk Summary",
|
|
||||||
"🎯 Risk Drivers",
|
|
||||||
"📋 Policy & Compliance",
|
|
||||||
"📝 Decision Memo",
|
|
||||||
"🔍 Audit Trail",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
with tab1:
|
with tab1:
|
||||||
|
st.subheader("Summary")
|
||||||
|
|
||||||
|
human = result.get("human_response", "")
|
||||||
|
if human:
|
||||||
|
st.write(human.split("```")[0].strip())
|
||||||
|
else:
|
||||||
|
st.info("No human-readable summary available.")
|
||||||
|
|
||||||
|
st.divider()
|
||||||
st.subheader("Normalized Application")
|
st.subheader("Normalized Application")
|
||||||
st.json(result.get("normalized_application", {}))
|
st.json(result.get("normalized_application", {}))
|
||||||
|
|
||||||
st.subheader("Assessment Overview")
|
|
||||||
st.write(result.get("human_response", "").split("```")[0])
|
|
||||||
|
|
||||||
with tab2:
|
with tab2:
|
||||||
st.subheader("Risk Drivers")
|
st.subheader("Decision Memo")
|
||||||
drivers = result.get("drivers", [])
|
memo = result.get("decision_memo", "")
|
||||||
|
if memo:
|
||||||
for driver in drivers:
|
st.markdown(memo)
|
||||||
impact_color = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡", "LOW": "🟢"}
|
else:
|
||||||
st.markdown(
|
st.info("No decision memo available.")
|
||||||
f"**{impact_color.get(driver['impact'], '⚪')} {driver['factor']}** ({driver['impact']})"
|
|
||||||
)
|
|
||||||
st.write(driver["evidence"])
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
with tab3:
|
with tab3:
|
||||||
st.subheader("Policy Checks")
|
st.subheader("Raw Output")
|
||||||
checks = result.get("policy_checks", [])
|
with st.expander("Show raw agent response JSON"):
|
||||||
|
st.json(st.session_state.raw_result or {})
|
||||||
|
|
||||||
for check in checks:
|
with st.expander("Show parsed assessment JSON"):
|
||||||
status_icon = {"PASS": "✅", "FAIL": "❌", "WARNING": "⚠️"}
|
st.json(result)
|
||||||
st.markdown(
|
|
||||||
f"{status_icon.get(check['status'], '❓')} **{check['check']}**: {check['details']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
exceptions = result.get("exceptions", [])
|
|
||||||
if exceptions:
|
|
||||||
st.subheader("⚠️ Policy Exceptions")
|
|
||||||
for exc in exceptions:
|
|
||||||
st.warning(exc)
|
|
||||||
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
st.subheader("📎 Required Documents")
|
|
||||||
docs = result.get("required_documents", [])
|
|
||||||
for doc in docs:
|
|
||||||
st.write(f"- {doc}")
|
|
||||||
|
|
||||||
with tab4:
|
|
||||||
st.subheader("Decision Memo")
|
|
||||||
st.markdown(result.get("decision_memo", "No memo available"))
|
|
||||||
|
|
||||||
with tab5:
|
|
||||||
st.subheader("Audit Trail")
|
|
||||||
audit = result.get("audit_trail", {})
|
|
||||||
st.json(audit)
|
|
||||||
|
|
||||||
# Case creation
|
|
||||||
st.divider()
|
|
||||||
st.header("📁 Case Management")
|
|
||||||
|
|
||||||
col1, col2 = st.columns([3, 1])
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
if st.session_state.case_id:
|
|
||||||
st.success(f"✅ Case created: **{st.session_state.case_id}**")
|
|
||||||
else:
|
|
||||||
st.info(
|
|
||||||
"Create a case to store this assessment in the case management system"
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
if not st.session_state.case_id:
|
|
||||||
if st.button("📁 Create Case", type="primary", use_container_width=True):
|
|
||||||
try:
|
|
||||||
# Create case via direct API
|
|
||||||
case_data = {
|
|
||||||
"applicant_name": result["normalized_application"][
|
|
||||||
"applicant_name"
|
|
||||||
],
|
|
||||||
"loan_amount": result["normalized_application"]["loan_amount"],
|
|
||||||
"risk_band": result["risk_band"],
|
|
||||||
"confidence": result["confidence"],
|
|
||||||
"recommended_action": result["recommended_action"],
|
|
||||||
"required_documents": result.get("required_documents", []),
|
|
||||||
"policy_exceptions": result.get("exceptions", []),
|
|
||||||
"notes": result.get("decision_memo", "")[:500],
|
|
||||||
}
|
|
||||||
|
|
||||||
response = httpx.post(
|
|
||||||
f"{CASE_SERVICE_URL}/cases", json=case_data, timeout=10.0
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
case_result = response.json()
|
|
||||||
st.session_state.case_id = case_result["case_id"]
|
|
||||||
st.rerun()
|
|
||||||
else:
|
|
||||||
st.error(f"Failed to create case: {response.text}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
st.error(f"Error creating case: {str(e)}")
|
|
||||||
else:
|
|
||||||
if st.button("🔄 Reset", use_container_width=True):
|
|
||||||
st.session_state.case_id = None
|
|
||||||
st.session_state.assessment_result = None
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
st.info(
|
st.info(
|
||||||
"👈 Select a scenario or paste a loan application JSON in the sidebar, then click 'Assess Risk'"
|
"👈 Select a scenario or paste a loan application JSON in the sidebar, then click **Assess Risk**."
|
||||||
)
|
)
|
||||||
|
|
||||||
st.subheader("Sample Application Format")
|
st.subheader("Sample Application Format")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue