larrysim commited on
Commit
06f8c36
Β·
verified Β·
1 Parent(s): b3a7d4f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +339 -336
app.py CHANGED
@@ -1,337 +1,340 @@
1
- import streamlit as st
2
- import pandas as pd
3
- import os
4
- import warnings
5
- import time
6
- import sqlite3
7
- import shutil
8
-
9
- # Suppress warnings
10
- warnings.filterwarnings("ignore")
11
-
12
- # ==========================================
13
- # 0. ROBUST IMPORTS
14
- # ==========================================
15
- try:
16
- from langchain_groq import ChatGroq
17
- from langchain_huggingface import HuggingFaceEmbeddings
18
- from langchain_community.vectorstores import FAISS
19
- from langchain_community.callbacks import StreamlitCallbackHandler
20
- from langchain_community.document_loaders import PyPDFLoader
21
- from langchain_text_splitters import CharacterTextSplitter
22
- from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
23
- from langchain_core.runnables import RunnablePassthrough
24
- from langchain_core.output_parsers import StrOutputParser
25
- from langchain_core.tools import tool
26
- from langchain.agents import AgentExecutor, create_tool_calling_agent
27
-
28
- except ImportError as e:
29
- st.error(f"❌ Critical Import Error: {e}")
30
- st.stop()
31
-
32
- # ==========================================
33
- # 1. DATABASE & CONFIG SETUP
34
- # ==========================================
35
- DB_FILE = "bank.db"
36
- INDEX_PATH = "faiss_index"
37
-
38
- def init_db():
39
- """Converts CSV files to SQLite DB if DB doesn't exist."""
40
- if os.path.exists(DB_FILE):
41
- return
42
-
43
- conn = sqlite3.connect(DB_FILE)
44
-
45
- # Map CSV filenames to Table Names
46
- csv_files = {
47
- "credit_score": "credit_score.csv",
48
- "account_status": "account_status.csv",
49
- "pr_status": "pr_status.csv"
50
- }
51
-
52
- try:
53
- for table, file in csv_files.items():
54
- if os.path.exists(file):
55
- df = pd.read_csv(file)
56
- # Clean column names (strip spaces)
57
- df.columns = [c.strip() for c in df.columns]
58
- # Force ID to string to match tool logic
59
- if 'ID' in df.columns:
60
- df['ID'] = df['ID'].astype(str)
61
- df.to_sql(table, conn, if_exists='replace', index=False)
62
- except Exception as e:
63
- st.error(f"DB Init Error: {e}")
64
- finally:
65
- conn.close()
66
-
67
- # Initialize DB on app load
68
- init_db()
69
-
70
- # Helper for tools
71
- def run_query(query, params=()):
72
- try:
73
- with sqlite3.connect(DB_FILE) as conn:
74
- cursor = conn.cursor()
75
- cursor.execute(query, params)
76
- return cursor.fetchone()
77
- except Exception as e:
78
- return f"DB Error: {e}"
79
-
80
- # ==========================================
81
- # 2. DEFINE TOOLS (SQL EDITION)
82
- # ==========================================
83
-
84
- @tool
85
- def get_credit_score(user_id: str) -> str:
86
- """Queries SQL DB for Credit Score."""
87
- clean_id = ''.join(filter(str.isdigit, str(user_id)))
88
- row = run_query("SELECT Credit_Score FROM credit_score WHERE ID = ?", (clean_id,))
89
- if row:
90
- return f"Credit Score: {row[0]}"
91
- return "User ID not found in Credit DB."
92
-
93
- @tool
94
- def get_account_status(user_id: str) -> str:
95
- """Queries SQL DB for Name, Nationality, Status, and Email."""
96
- clean_id = ''.join(filter(str.isdigit, str(user_id)))
97
- # We select columns safely. Ensure your CSV headers match these!
98
- row = run_query(
99
- "SELECT Name, Nationality, Account_Status, Email FROM account_status WHERE ID = ?",
100
- (clean_id,)
101
- )
102
- if isinstance(row, str) and "Error" in row: return row # Return error if caught
103
- if row:
104
- return f"Customer Name: {row[0]}, Nationality: {row[1]}, Status: {row[2]}, Email: {row[3]}"
105
- return "User ID not found in Account DB."
106
-
107
- @tool
108
- def check_pr_status(user_id: str) -> str:
109
- """Queries SQL DB for PR Status."""
110
- clean_id = ''.join(filter(str.isdigit, str(user_id)))
111
- # Try querying generic columns often found in these files
112
- row = run_query("SELECT PR_Status FROM pr_status WHERE ID = ?", (clean_id,))
113
-
114
- # Fallback if column name is different (e.g., Is_PR)
115
- if not row or (isinstance(row, str) and "no such column" in row.lower()):
116
- row = run_query("SELECT Is_PR FROM pr_status WHERE ID = ?", (clean_id,))
117
-
118
- if row and not isinstance(row, str):
119
- return f"PR Status: {row[0]}"
120
- return "PR Status: False (Record not found)"
121
-
122
- # ==========================================
123
- # 3. STREAMLIT APP UI
124
- # ==========================================
125
- st.set_page_config(page_title="Bank Loan Agent (SQL)", layout="wide")
126
- st.title("πŸ€– Multi-Policy Loan Assessor (SQL + RAG)")
127
- st.markdown("Agent connects to **SQLite Database** and **Persistent Vector Store**")
128
-
129
- # --- METRICS FUNCTION ---
130
- def update_metrics(placeholder):
131
- manual_time = 15 * 60 # 15 mins manual work
132
- if 'execution_time' in st.session_state:
133
- ai_time = st.session_state.execution_time
134
- time_saved = manual_time - ai_time
135
- saved_pct = (time_saved / manual_time) * 100
136
-
137
- with placeholder.container():
138
- col_kpi1, col_kpi2 = st.columns(2)
139
- col_kpi1.metric("AI Processing", f"{ai_time:.1f}s")
140
- col_kpi2.metric(
141
- "Time Saved",
142
- f"{time_saved/60:.1f} min",
143
- delta=f"{saved_pct:.1f}% faster"
144
- )
145
-
146
- # --- SIDEBAR ---
147
- # --- SIDEBAR ---
148
- with st.sidebar:
149
- st.header("πŸ” Authentication")
150
-
151
- # 1. INITIALIZE SESSION STATE
152
- if 'is_key_valid' not in st.session_state:
153
- st.session_state['is_key_valid'] = False
154
-
155
- # 2. CONDITIONAL RENDERING
156
- # If key is NOT valid, show the input field
157
- if not st.session_state['is_key_valid']:
158
- api_key_input = st.text_input("Enter Groq API Key", type="password", key="input_key")
159
-
160
- if st.button("Validate API Key"):
161
- if not api_key_input:
162
- st.error("⚠️ Please enter a key.")
163
- else:
164
- try:
165
- with st.spinner("Validating..."):
166
- # Test the key
167
- test_llm = ChatGroq(api_key=api_key_input, model_name="llama-3.3-70b-versatile")
168
- test_llm.invoke("Test")
169
-
170
- # Store in session state
171
- st.session_state['groq_api_key'] = api_key_input
172
- st.session_state['is_key_valid'] = True
173
-
174
- # Success & Force Rerun to hide the input immediately
175
- st.success("βœ… Valid Key!")
176
- time.sleep(0.5)
177
- st.rerun()
178
-
179
- except Exception as e:
180
- st.session_state['is_key_valid'] = False
181
- st.error(f"❌ Invalid Key: {e}")
182
-
183
- # 3. SECURE STATE
184
- # If key IS valid, show a locked state (Input field is physically gone)
185
- else:
186
- st.success("βœ… API Key is Active")
187
- st.info("The key is securely stored in this session.")
188
-
189
- # Option to Logout/Reset
190
- if st.button("πŸ”΄ Reset / Change Key"):
191
- st.session_state['is_key_valid'] = False
192
- st.session_state['groq_api_key'] = None
193
- st.rerun()
194
-
195
- st.divider()
196
- st.subheader("πŸ› οΈ System Maintenance")
197
-
198
- # ... (Rest of your sidebar code: Rebuild Knowledge Base, etc.) ...
199
-
200
- # --- MAIN LOGIC ---
201
- if st.session_state.get('is_key_valid', False):
202
-
203
- os.environ["GROQ_API_KEY"] = st.session_state['groq_api_key']
204
-
205
- # --- RAG SETUP (PERSISTENT) ---
206
- @st.cache_resource
207
- def setup_rag():
208
- embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
209
-
210
- # 1. LOAD EXISTING INDEX
211
- if os.path.exists(INDEX_PATH):
212
- print("Loading existing vector store...")
213
- vectorstore = FAISS.load_local(INDEX_PATH, embeddings, allow_dangerous_deserialization=True)
214
- return vectorstore.as_retriever()
215
-
216
- # 2. CREATE NEW INDEX
217
- else:
218
- print("Creating new vector store...")
219
- if pdfs_missing:
220
- st.error("Missing PDFs. Cannot start RAG.")
221
- st.stop()
222
-
223
- documents = []
224
- for pdf_file in required_pdfs:
225
- loader = PyPDFLoader(pdf_file)
226
- documents.extend(loader.load())
227
-
228
- text_splitter = CharacterTextSplitter(chunk_size=600, chunk_overlap=50)
229
- final_docs = text_splitter.split_documents(documents)
230
-
231
- vectorstore = FAISS.from_documents(final_docs, embeddings)
232
- vectorstore.save_local(INDEX_PATH)
233
- return vectorstore.as_retriever()
234
-
235
- with st.spinner("Initializing Knowledge Base..."):
236
- retriever = setup_rag()
237
-
238
- # --- LLM & CHAIN ---
239
- llm = ChatGroq(temperature=0, model_name="llama-3.3-70b-versatile")
240
- rag_prompt = ChatPromptTemplate.from_template("Answer based on context:\n{context}\nQuestion: {question}")
241
- rag_chain = (
242
- {"context": retriever | (lambda d: "\n".join([x.page_content for x in d])), "question": RunnablePassthrough()}
243
- | rag_prompt | llm | StrOutputParser()
244
- )
245
-
246
- @tool
247
- def consult_policy_doc(query: str) -> str:
248
- """Consults the Policy Documents to find specific Risk Rules and Interest Rates."""
249
- return rag_chain.invoke(query)
250
-
251
- tools = [get_credit_score, get_account_status, check_pr_status, consult_policy_doc]
252
-
253
- prompt = ChatPromptTemplate.from_messages([
254
- ("system", "You are a Loan Risk Officer. Query the SQL Database for customer info using the ID. Then consult Policy Documents for rules."),
255
- ("human", "{input}"),
256
- MessagesPlaceholder(variable_name="agent_scratchpad"),
257
- ])
258
-
259
- agent = create_tool_calling_agent(llm, tools, prompt)
260
- agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True)
261
-
262
- # --- UI LAYOUT ---
263
- col1, col2 = st.columns([1, 2])
264
- with col1:
265
- st.subheader("1. Customer Details")
266
- uid = st.text_input("Customer ID", "1111")
267
-
268
- st.subheader("βš™οΈ Simulation")
269
- use_simulation = st.checkbox("Override Database Data")
270
-
271
- sim_score = 650
272
- sim_status = "good-standing"
273
-
274
- if use_simulation:
275
- st.warning("⚠️ Simulation Mode Active")
276
- sim_score = st.slider("Sim Credit Score", 300, 900, 450)
277
- sim_status = st.selectbox("Sim Account Status", ["good-standing", "closed", "delinquent"])
278
-
279
- st.divider()
280
- btn = st.button("Assess Loan Risk", type="primary")
281
-
282
- with col2:
283
- if btn:
284
- # Construct Query
285
- if use_simulation:
286
- query = f"""
287
- Process Loan for Customer ID: {uid}.
288
- *** SIMULATION MODE ***
289
- 1. DO NOT query 'get_credit_score' or 'account_status' for Score/Status.
290
- 2. USE: Score: {sim_score}, Status: {sim_status}
291
- 3. Query 'get_account_status' ONLY for Name/Nationality.
292
- 4. Consult Policy Docs for risk/rates using SIMULATED values.
293
- 5. Output Final Report table + Justification.
294
- """
295
- else:
296
- query = f"""
297
- Process Loan for Customer ID: {uid}.
298
- 1. Query SQL tools for Name, Email, Nationality, Status, Score.
299
- 2. IF Nationality is 'Singaporean', SKIP 'check_pr_status'.
300
- 3. Consult Policy Docs for risk/rates.
301
- 4. Output Final Report table + Justification.
302
- """
303
-
304
- with st.status("πŸ€– Agent is processing...", expanded=True) as status:
305
- st_callback = StreamlitCallbackHandler(st.container())
306
- try:
307
- start_time = time.time()
308
- res = agent_executor.invoke({"input": query}, {"callbacks": [st_callback]})
309
- end_time = time.time()
310
-
311
- st.session_state.execution_time = end_time - start_time
312
- update_metrics(metrics_placeholder)
313
-
314
- status.update(label="βœ… Complete!", state="complete", expanded=False)
315
- except Exception as e:
316
- st.error(f"Error: {e}")
317
- st.stop()
318
-
319
- st.success("### πŸ“‹ Final Recommendation Report")
320
- st.markdown(res['output'])
321
-
322
- with st.expander("πŸ” detailed Trace"):
323
- steps = res.get("intermediate_steps", [])
324
- for i, (action, observation) in enumerate(steps):
325
- st.markdown(f"**Step {i+1}:** Tool `{action.tool}` | Output: `{observation}`")
326
-
327
- if not use_simulation:
328
- st.divider()
329
- with st.expander("βœ‰οΈ Draft Email"):
330
- email_prompt = f"Write a formal email based on this decision: {res['output']}"
331
- with st.spinner("Drafting..."):
332
- email_draft = llm.invoke(email_prompt).content
333
- st.text_area("Email Draft", value=email_draft, height=200)
334
- if st.button("Send Email"): st.toast("Sent!", icon="πŸ“¨")
335
-
336
- elif not st.session_state.get('is_key_valid', False):
 
 
 
337
  st.info("πŸ‘ˆ Please validate your Groq API Key.")
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import os
4
+ import warnings
5
+ import time
6
+ import sqlite3
7
+ import shutil
8
+
9
+ # ==========================================
10
+ # 1. PAGE CONFIG (MUST BE FIRST)
11
+ # ==========================================
12
+ st.set_page_config(page_title="Bank Loan Agent (SQL)", layout="wide")
13
+
14
+ # Suppress warnings
15
+ warnings.filterwarnings("ignore")
16
+
17
+ # ==========================================
18
+ # 2. ROBUST IMPORTS
19
+ # ==========================================
20
+ try:
21
+ from langchain_groq import ChatGroq
22
+ from langchain_huggingface import HuggingFaceEmbeddings
23
+ from langchain_community.vectorstores import FAISS
24
+ from langchain_community.callbacks import StreamlitCallbackHandler
25
+ from langchain_community.document_loaders import PyPDFLoader
26
+ from langchain_text_splitters import CharacterTextSplitter
27
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
28
+ from langchain_core.runnables import RunnablePassthrough
29
+ from langchain_core.output_parsers import StrOutputParser
30
+ from langchain_core.tools import tool
31
+ from langchain.agents import AgentExecutor, create_tool_calling_agent
32
+
33
+ except ImportError as e:
34
+ st.error(f"❌ Critical Import Error: {e}")
35
+ st.stop()
36
+
37
+ # ==========================================
38
+ # 3. DATABASE SETUP
39
+ # ==========================================
40
+ DB_FILE = "bank.db"
41
+ INDEX_PATH = "faiss_index"
42
+
43
+ def init_db():
44
+ """Converts CSV files to SQLite DB. Handles 'replace' errors gracefully."""
45
+ # Only run if DB doesn't exist to avoid redundant overwrites
46
+ if os.path.exists(DB_FILE):
47
+ return
48
+
49
+ conn = sqlite3.connect(DB_FILE)
50
+
51
+ csv_files = {
52
+ "credit_score": "credit_score.csv",
53
+ "account_status": "account_status.csv",
54
+ "pr_status": "pr_status.csv"
55
+ }
56
+
57
+ try:
58
+ for table, file in csv_files.items():
59
+ if os.path.exists(file):
60
+ df = pd.read_csv(file)
61
+ df.columns = [c.strip() for c in df.columns] # Clean headers
62
+ if 'ID' in df.columns:
63
+ df['ID'] = df['ID'].astype(str)
64
+
65
+ # Robust SQL Write
66
+ try:
67
+ df.to_sql(table, conn, if_exists='replace', index=False)
68
+ except Exception as sql_err:
69
+ # Fallback: if 'replace' fails on missing table, try creating it fresh
70
+ print(f"⚠️ SQL Warning for {table}: {sql_err}")
71
+ pass
72
+
73
+ except Exception as e:
74
+ st.error(f"DB Init Error: {e}")
75
+ finally:
76
+ conn.close()
77
+
78
+ # Initialize DB
79
+ init_db()
80
+
81
+ # Helper for tools
82
+ def run_query(query, params=()):
83
+ try:
84
+ with sqlite3.connect(DB_FILE) as conn:
85
+ cursor = conn.cursor()
86
+ cursor.execute(query, params)
87
+ return cursor.fetchone()
88
+ except Exception as e:
89
+ return f"DB Error: {e}"
90
+
91
+ # ==========================================
92
+ # 4. DEFINE TOOLS
93
+ # ==========================================
94
+
95
+ @tool
96
+ def get_credit_score(user_id: str) -> str:
97
+ """Queries SQL DB for Credit Score."""
98
+ clean_id = ''.join(filter(str.isdigit, str(user_id)))
99
+ row = run_query("SELECT Credit_Score FROM credit_score WHERE ID = ?", (clean_id,))
100
+ if row and not isinstance(row, str):
101
+ return f"Credit Score: {row[0]}"
102
+ return "User ID not found in Credit DB."
103
+
104
+ @tool
105
+ def get_account_status(user_id: str) -> str:
106
+ """Queries SQL DB for Name, Nationality, Status, and Email."""
107
+ clean_id = ''.join(filter(str.isdigit, str(user_id)))
108
+ row = run_query(
109
+ "SELECT Name, Nationality, Account_Status, Email FROM account_status WHERE ID = ?",
110
+ (clean_id,)
111
+ )
112
+ if row and not isinstance(row, str):
113
+ return f"Customer Name: {row[0]}, Nationality: {row[1]}, Status: {row[2]}, Email: {row[3]}"
114
+ return "User ID not found in Account DB."
115
+
116
+ @tool
117
+ def check_pr_status(user_id: str) -> str:
118
+ """Queries SQL DB for PR Status."""
119
+ clean_id = ''.join(filter(str.isdigit, str(user_id)))
120
+ row = run_query("SELECT PR_Status FROM pr_status WHERE ID = ?", (clean_id,))
121
+
122
+ # Fallback for column naming differences
123
+ if not row or (isinstance(row, str) and "no such column" in row.lower()):
124
+ row = run_query("SELECT Is_PR FROM pr_status WHERE ID = ?", (clean_id,))
125
+
126
+ if row and not isinstance(row, str):
127
+ return f"PR Status: {row[0]}"
128
+ return "PR Status: False (Record not found)"
129
+
130
+ # ==========================================
131
+ # 5. STREAMLIT APP UI
132
+ # ==========================================
133
+ st.title("πŸ€– Multi-Policy Loan Assessor (SQL + RAG)")
134
+ st.markdown("Agent connects to **SQLite Database** and **Persistent Vector Store**")
135
+
136
+ # --- METRICS FUNCTION ---
137
+ def update_metrics(placeholder):
138
+ manual_time = 15 * 60
139
+ if 'execution_time' in st.session_state:
140
+ ai_time = st.session_state.execution_time
141
+ time_saved = manual_time - ai_time
142
+ saved_pct = (time_saved / manual_time) * 100
143
+
144
+ with placeholder.container():
145
+ col_kpi1, col_kpi2 = st.columns(2)
146
+ col_kpi1.metric("AI Processing", f"{ai_time:.1f}s")
147
+ col_kpi2.metric(
148
+ "Time Saved",
149
+ f"{time_saved/60:.1f} min",
150
+ delta=f"{saved_pct:.1f}% faster"
151
+ )
152
+
153
+ # --- SIDEBAR ---
154
+ with st.sidebar:
155
+ st.header("πŸ” Authentication")
156
+
157
+ # 1. Check if Key exists in Secrets (Env Var)
158
+ if "GROQ_API_KEY" in st.secrets:
159
+ st.session_state['groq_api_key'] = st.secrets["GROQ_API_KEY"]
160
+ st.session_state['is_key_valid'] = True
161
+
162
+ # 2. Manual Entry Logic
163
+ if 'is_key_valid' not in st.session_state:
164
+ st.session_state['is_key_valid'] = False
165
+
166
+ if not st.session_state['is_key_valid']:
167
+ api_key_input = st.text_input("Enter Groq API Key", type="password", key="input_key")
168
+ if st.button("Validate API Key"):
169
+ if not api_key_input:
170
+ st.error("⚠️ Please enter a key.")
171
+ else:
172
+ try:
173
+ with st.spinner("Validating..."):
174
+ test_llm = ChatGroq(api_key=api_key_input, model_name="llama-3.3-70b-versatile")
175
+ test_llm.invoke("Test")
176
+ st.session_state['groq_api_key'] = api_key_input
177
+ st.session_state['is_key_valid'] = True
178
+ st.success("βœ… Valid Key!")
179
+ time.sleep(0.5)
180
+ st.rerun()
181
+ except Exception as e:
182
+ st.error(f"❌ Invalid Key: {e}")
183
+ else:
184
+ st.success("βœ… API Key Active")
185
+ if st.button("πŸ”΄ Reset Key"):
186
+ st.session_state['is_key_valid'] = False
187
+ st.session_state['groq_api_key'] = None
188
+ st.rerun()
189
+
190
+ st.divider()
191
+ st.subheader("πŸ› οΈ System Maintenance")
192
+
193
+ if st.button("♻️ Rebuild Knowledge Base"):
194
+ if os.path.exists(INDEX_PATH):
195
+ shutil.rmtree(INDEX_PATH)
196
+ st.cache_resource.clear()
197
+ st.success("Cache cleared.")
198
+ time.sleep(1)
199
+ st.rerun()
200
+
201
+ if st.button("πŸ’Ύ Reload CSVs to DB"):
202
+ if os.path.exists(DB_FILE):
203
+ os.remove(DB_FILE)
204
+ init_db()
205
+ st.success("Database refreshed.")
206
+
207
+ st.divider()
208
+
209
+ required_pdfs = ["Bank Loan Overall Risk Policy.pdf", "Bank Loan Interest Rate Policy.pdf"]
210
+ pdfs_missing = [f for f in required_pdfs if not os.path.exists(f)]
211
+
212
+ if os.path.exists(DB_FILE) and not pdfs_missing:
213
+ st.success("βœ… System Ready")
214
+ else:
215
+ st.warning(f"⚠️ Missing: {pdfs_missing}")
216
+
217
+ st.header("πŸ“Š Metrics")
218
+ metrics_placeholder = st.empty()
219
+ update_metrics(metrics_placeholder)
220
+
221
+ # --- MAIN LOGIC ---
222
+ if st.session_state.get('is_key_valid', False):
223
+
224
+ os.environ["GROQ_API_KEY"] = st.session_state['groq_api_key']
225
+
226
+ # --- RAG SETUP ---
227
+ @st.cache_resource
228
+ def setup_rag():
229
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
230
+ if os.path.exists(INDEX_PATH):
231
+ return FAISS.load_local(INDEX_PATH, embeddings, allow_dangerous_deserialization=True).as_retriever()
232
+ else:
233
+ if pdfs_missing:
234
+ st.error("Missing PDFs.")
235
+ st.stop()
236
+ documents = []
237
+ for pdf_file in required_pdfs:
238
+ loader = PyPDFLoader(pdf_file)
239
+ documents.extend(loader.load())
240
+ text_splitter = CharacterTextSplitter(chunk_size=600, chunk_overlap=50)
241
+ final_docs = text_splitter.split_documents(documents)
242
+ vectorstore = FAISS.from_documents(final_docs, embeddings)
243
+ vectorstore.save_local(INDEX_PATH)
244
+ return vectorstore.as_retriever()
245
+
246
+ with st.spinner("Initializing AI..."):
247
+ retriever = setup_rag()
248
+
249
+ llm = ChatGroq(temperature=0, model_name="llama-3.3-70b-versatile")
250
+
251
+ # RAG Chain
252
+ rag_prompt = ChatPromptTemplate.from_template("Answer based on context:\n{context}\nQuestion: {question}")
253
+ rag_chain = (
254
+ {"context": retriever | (lambda d: "\n".join([x.page_content for x in d])), "question": RunnablePassthrough()}
255
+ | rag_prompt | llm | StrOutputParser()
256
+ )
257
+
258
+ @tool
259
+ def consult_policy_doc(query: str) -> str:
260
+ """Consults Policy Documents for Risk Rules."""
261
+ return rag_chain.invoke(query)
262
+
263
+ tools = [get_credit_score, get_account_status, check_pr_status, consult_policy_doc]
264
+
265
+ prompt = ChatPromptTemplate.from_messages([
266
+ ("system", "You are a Loan Risk Officer. Query SQL DB for customer info. Consult Policy Docs for rules."),
267
+ ("human", "{input}"),
268
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
269
+ ])
270
+
271
+ agent = create_tool_calling_agent(llm, tools, prompt)
272
+ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True)
273
+
274
+ col1, col2 = st.columns([1, 2])
275
+ with col1:
276
+ st.subheader("1. Customer Details")
277
+ uid = st.text_input("Customer ID", "1111")
278
+ use_simulation = st.checkbox("Simulation Mode")
279
+
280
+ sim_score = 650
281
+ sim_status = "good-standing"
282
+ if use_simulation:
283
+ sim_score = st.slider("Sim Credit Score", 300, 900, 450)
284
+ sim_status = st.selectbox("Sim Status", ["good-standing", "closed", "delinquent"])
285
+
286
+ st.divider()
287
+ btn = st.button("Assess Loan Risk", type="primary")
288
+
289
+ with col2:
290
+ if btn:
291
+ if use_simulation:
292
+ query = f"""
293
+ Process Loan for Customer ID: {uid}.
294
+ *** SIMULATION MODE ***
295
+ 1. DO NOT query 'get_credit_score' or 'account_status' for Score/Status.
296
+ 2. USE: Score: {sim_score}, Status: {sim_status}
297
+ 3. Query 'get_account_status' ONLY for Name/Nationality.
298
+ 4. Consult Policy Docs for risk/rates.
299
+ 5. Output Final Report table + Justification.
300
+ """
301
+ else:
302
+ query = f"""
303
+ Process Loan for Customer ID: {uid}.
304
+ 1. Query SQL tools for Name, Email, Nationality, Status, Score.
305
+ 2. IF Nationality is 'Singaporean', SKIP 'check_pr_status'.
306
+ 3. Consult Policy Docs for risk/rates.
307
+ 4. Output Final Report table + Justification.
308
+ """
309
+
310
+ with st.status("πŸ€– Agent is processing...", expanded=True) as status:
311
+ st_callback = StreamlitCallbackHandler(st.container())
312
+ try:
313
+ start_time = time.time()
314
+ res = agent_executor.invoke({"input": query}, {"callbacks": [st_callback]})
315
+ end_time = time.time()
316
+ st.session_state.execution_time = end_time - start_time
317
+ update_metrics(metrics_placeholder)
318
+ status.update(label="βœ… Complete!", state="complete", expanded=False)
319
+ except Exception as e:
320
+ st.error(f"Error: {e}")
321
+ st.stop()
322
+
323
+ st.success("### πŸ“‹ Final Recommendation")
324
+ st.markdown(res['output'])
325
+
326
+ with st.expander("πŸ” Detailed Trace"):
327
+ steps = res.get("intermediate_steps", [])
328
+ for i, (action, observation) in enumerate(steps):
329
+ st.markdown(f"**Step {i+1}:** Tool `{action.tool}` | Output: `{observation}`")
330
+
331
+ if not use_simulation:
332
+ st.divider()
333
+ with st.expander("βœ‰οΈ Draft Email"):
334
+ email_prompt = f"Write a formal email based on this decision: {res['output']}"
335
+ with st.spinner("Drafting..."):
336
+ email_draft = llm.invoke(email_prompt).content
337
+ st.text_area("Email Draft", value=email_draft, height=200)
338
+
339
+ elif not st.session_state.get('is_key_valid', False):
340
  st.info("πŸ‘ˆ Please validate your Groq API Key.")