walepavan commited on
Commit
71a733c
Β·
verified Β·
1 Parent(s): 613a439

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +43 -20
  2. app.py +567 -0
  3. requirements.txt +11 -3
Dockerfile CHANGED
@@ -1,20 +1,43 @@
1
- FROM python:3.13.5-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
13
-
14
- RUN pip3 install -r requirements.txt
15
-
16
- EXPOSE 8501
17
-
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
-
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Use an official Python runtime as a parent image
3
+ FROM python:3.10-slim
4
+
5
+
6
+ # Set environment variables
7
+ ENV PYTHONUNBUFFERED 1
8
+
9
+
10
+ # Install system dependencies and git
11
+ RUN apt-get update && apt-get install -y \
12
+ build-essential \
13
+ git \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+
17
+
18
+
19
+ # Create a non-root user and set permissions
20
+ RUN useradd -ms /bin/bash appuser
21
+ # Set the working directory in the container
22
+ WORKDIR /home/appuser/app
23
+
24
+
25
+ # Copy the requirements file and install dependencies
26
+ COPY requirements.txt .
27
+ RUN pip install --upgrade pip && pip install -r requirements.txt
28
+
29
+
30
+ # Switch to non-root user
31
+ USER appuser
32
+
33
+
34
+ # Copy the rest of the application code into the container
35
+ COPY --chown=appuser . /home/appuser/app
36
+
37
+
38
+ # Expose the port that the app runs on
39
+ EXPOSE 8501
40
+
41
+
42
+ # Command to run the application
43
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
app.py ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import streamlit as st
3
+ import sqlite3
4
+ import pandas as pd
5
+ import json
6
+ import re
7
+ import os
8
+ from datetime import date
9
+ from typing import TypedDict, List, Dict, Any
10
+
11
+ from openai import OpenAI
12
+ from langgraph.graph import StateGraph, END
13
+ from langchain_openai import ChatOpenAI
14
+ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
15
+ from langchain_core.tools import tool
16
+
17
+ # ── Page config ──────────────────────────────────────────────────────────────
18
+ st.set_page_config(
19
+ page_title="Kartify Support",
20
+ page_icon="πŸ›’",
21
+ layout="centered",
22
+ )
23
+
24
+ # ── Load secrets ─────────────────────────────────────────────────────────────
25
+
26
+
27
+ # ── LLMs ─────────────────────────────────────────────────────────────────────
28
+ @st.cache_resource
29
+ def load_llms():
30
+ llm = ChatOpenAI(model_name="gpt-4o-mini")
31
+ evaluate_llm = ChatOpenAI(model_name="gpt-4o")
32
+ return llm, evaluate_llm
33
+
34
+ llm, evaluate_llm = load_llms()
35
+
36
+ # ── State ─────────────────────────────────────────────────────────────────────
37
+ class OrderState(TypedDict):
38
+ cust_id: str
39
+ order_id: str
40
+ order_context: str
41
+ query: str
42
+ raw_agent_response: str
43
+ final_response: str
44
+ history: List[Dict[str, str]]
45
+ intent: str
46
+ evaluation: Dict[str, float]
47
+ guard_result: str
48
+ conv_guard_result: str
49
+
50
+ # ── Conversation memory ───────────────────────────────────────────────────────
51
+ class ConversationMemory:
52
+ def __init__(self):
53
+ self.history: List[Dict[str, str]] = []
54
+
55
+ def add(self, msg: dict):
56
+ self.history.append(msg)
57
+
58
+ def get(self) -> List[Dict[str, str]]:
59
+ return self.history
60
+
61
+ def clear(self):
62
+ self.history = []
63
+
64
+ # ── SQL tool ──────────────────────────────────────────────────────────────────
65
+ @tool
66
+ def fetch_order_details(order_id: str) -> str:
67
+ """
68
+ Fetch all order details for a given order_id from the Kartify database.
69
+ Use this tool whenever the customer's query requires order-specific information.
70
+ Returns a formatted string of order details, or an error message if not found.
71
+ """
72
+ if not re.match(r"^O\d+$", order_id.strip()):
73
+ return f"Invalid order ID format: '{order_id}'. Expected format: O followed by digits (e.g. O40327)."
74
+ try:
75
+ with sqlite3.connect("kartify.db") as conn:
76
+ df = pd.read_sql_query(
77
+ "SELECT * FROM orders WHERE order_id = ?",
78
+ conn,
79
+ params=(order_id.strip(),),
80
+ )
81
+ if df.empty:
82
+ return f"No order found with ID {order_id}."
83
+ return df.to_string(index=False)
84
+ except Exception as e:
85
+ return f"Database error while fetching order {order_id}: {str(e)}"
86
+
87
+ # ── System prompt ─────────────────────────────────────────────────────────────
88
+ SYSTEM_PROMPT = """You are a Kartify Customer Service Agent. You help customers with questions about their orders.
89
+
90
+ You have access to the following tool:
91
+ fetch_order_details(order_id) β€” retrieves all order information from the database.
92
+
93
+ Follow the ReAct pattern strictly:
94
+ Thought: <your reasoning about what to do next>
95
+ Action: fetch_order_details with the order_id from the customer's query
96
+ Observation: <tool result>
97
+ Thought: <reason about the observation and form your answer>
98
+ Final Answer: <short, polite, conversational reply β€” no greetings, no sign-off>
99
+
100
+ Policy rules (apply before writing Final Answer):
101
+ - If actual_delivery is null the order has not arrived yet β€” do not mention return/replacement eligibility.
102
+ - Only mention return or replacement terms when the customer explicitly asks.
103
+ - Never invent data. Only use what the tool returned.
104
+ - Keep the Final Answer concise and empathetic.
105
+ - Never reveal internal data fields or technical reasons in your reply (e.g. do not mention that actual_delivery is null or any other raw database values).
106
+ - If a customer asks why their order hasn't arrived yet, only state that it is still on the way and share the expected delivery date β€” never explain the technical reason behind the delay status.
107
+ - Never promise or suggest an early delivery. Always communicate the expected delivery date as-is without implying it could arrive sooner.
108
+ - If the order has not arrived by the expected delivery date, empathetically acknowledge the delay and advise the customer to wait a little longer or contact support β€” do not speculate on reasons.
109
+
110
+ Answer Guidelines:
111
+ - Only answer what is asked in the Query
112
+ - Check the Previous conversation (if any) before generating the reply
113
+ """
114
+
115
+ # ── Helpers ───────────────────────────────────────────────────────────────────
116
+ def extract_json_from_llm(text: str):
117
+ for pattern in [r"```json\s*(.*?)\s*```", r"\{.*\}", r"\[.*\]"]:
118
+ match = re.search(pattern, text, re.DOTALL)
119
+ if match:
120
+ try:
121
+ return json.loads(match.group(1) if "```" in pattern else match.group(0))
122
+ except Exception:
123
+ continue
124
+ return json.loads(text)
125
+
126
+ # ── Order agent ───────────────────────────────────────────────────────────────
127
+ def order_agent(query: str, order_id: str, history: list) -> tuple:
128
+ today = date.today().strftime("%d %B %Y")
129
+ llm_with_tools = llm.bind_tools([fetch_order_details])
130
+
131
+ history_text = ""
132
+ if history:
133
+ history_text = "\nPrevious conversation:\n" + "\n".join(
134
+ f"User: {h['user']}\nAssistant: {h['assistant']}" for h in history
135
+ ) + "\n"
136
+
137
+ user_content = (
138
+ f"Previous Conversation:{history_text}\n"
139
+ f"Customer query: {query}\n"
140
+ f"Order ID: {order_id}\n"
141
+ f"Today's date: {today}"
142
+ )
143
+
144
+ messages = [
145
+ SystemMessage(content=SYSTEM_PROMPT),
146
+ HumanMessage(content=user_content),
147
+ ]
148
+
149
+ order_context = ""
150
+ max_iterations = 5
151
+
152
+ for _ in range(max_iterations):
153
+ ai_msg = llm_with_tools.invoke(messages)
154
+ messages.append(ai_msg)
155
+
156
+ if not getattr(ai_msg, "tool_calls", None):
157
+ break
158
+
159
+ for tc in ai_msg.tool_calls:
160
+ if tc["name"] == "fetch_order_details":
161
+ result = fetch_order_details.invoke(tc["args"])
162
+ order_context = result
163
+ messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
164
+
165
+ final_response = ai_msg.content.strip()
166
+ for prefix in ("Final Answer:", "final answer:"):
167
+ if final_response.lower().startswith(prefix.lower()):
168
+ final_response = final_response[len(prefix):].strip()
169
+ break
170
+
171
+ return order_context, final_response
172
+
173
+ # ── Node functions ────────────────────────────────────────────────────────────
174
+ def user_input_node(state: OrderState):
175
+ return state
176
+
177
+ def memory_node(state: OrderState):
178
+ st.session_state.conversation_memory.add(
179
+ {"user": state["query"], "assistant": state["final_response"]}
180
+ )
181
+ return state
182
+
183
+ def order_agent_node(state: OrderState):
184
+ order_context, final_response = order_agent(
185
+ query=state["query"],
186
+ order_id=state["order_id"],
187
+ history=state["history"],
188
+ )
189
+ return {"order_context": order_context, "final_response": final_response}
190
+
191
+ def intent_node(state: OrderState):
192
+ prompt = f"""You are an intent classifier for customer service queries. Classify the user's query into one of these categories.
193
+ Return ONLY the numeric ID (0, 1, 2, or 3). No explanation.
194
+
195
+ 0 - Escalation: user is very angry/frustrated, wants a human now.
196
+ 1 - Exit: user is ending the conversation ("Thanks", "Bye", "Resolved").
197
+ 2 - Process: clear, actionable order query β€” proceed normally.
198
+ 3 - Random/Unrelated/Vulnerable: out-of-scope or potentially unsafe query.
199
+
200
+ Query: {state['query']}"""
201
+ result = llm.invoke([HumanMessage(content=prompt)]).content.strip()
202
+ return {"intent": result[:1]}
203
+
204
+ def router_node(state: OrderState):
205
+ return "order_agent" if state["intent"] == "2" else "exit_node"
206
+
207
+ def exit_node(state: OrderState):
208
+ mapping = {
209
+ "0": "Sorry for the inconvenience. A human support agent will assist you shortly.",
210
+ "1": "Thank you! I hope I was able to assist with your query.",
211
+ "3": "Apologies, I'm currently only able to help with information about your placed orders.",
212
+ }
213
+ return {"final_response": mapping.get(state["intent"], "How can I help you?")}
214
+
215
+ def evaluation_node(state: OrderState):
216
+ prompt = f"""Evaluate the assistant's response to a customer query using the provided order context.
217
+
218
+ Context: {state['order_context']}
219
+ Query: {state['query']}
220
+ Response: {state['final_response']}
221
+
222
+ Instructions:
223
+ 1. **Groundedness (0.0 to 1.0)**: Score based on how well the response is factually supported by the context.
224
+ - Score closer to 1 if all facts are accurate and derived from the context.
225
+ - Score closer to 0 if there is hallucination, guesswork, or any fabricated information.
226
+
227
+ 2. **Precision (0.0 to 1.0)**: Score based on how directly and accurately the assistant addresses the query.
228
+ - Score closer to 1 if the response is concise, focused, and answers the exact user query.
229
+ - Score closer to 0 if it includes irrelevant details or misses the main point.
230
+
231
+ Output format (JSON only):
232
+ groundedness: float between 0 and 1 ,
233
+ precision: float between 0 and 1
234
+
235
+ Return ONLY JSON:
236
+ {{
237
+ "groundedness": float,
238
+ "precision": float
239
+ }}
240
+ """
241
+ try:
242
+ raw = evaluate_llm.invoke([HumanMessage(content=prompt)]).content.strip()
243
+ evaluation = extract_json_from_llm(raw)
244
+ except Exception:
245
+ evaluation = {"groundedness": 0.0, "precision": 0.0}
246
+ return {"evaluation": evaluation}
247
+
248
+ def retry_router(state: OrderState):
249
+ score = state.get("evaluation", {})
250
+ if score.get("groundedness", 0) < 0.75 or score.get("precision", 0) < 0.75:
251
+ return "order_agent"
252
+ return "safety_check"
253
+
254
+ def guard_node(state: OrderState):
255
+ prompt = f"""You are a content safety assistant. Your task is to classify if the assistant's response is appropriate.
256
+ If the message contains:
257
+ - Requests for bank details, OTPs, account numbers
258
+ - Harassment or offensive tone
259
+ - Privacy concerns or unsafe advice
260
+ - Misunderstanding and miscommunication word
261
+ - Phrases like "please contact customer service" or redirection to a human agent
262
+ - Escalated this to our support team
263
+ Return: BLOCK
264
+ Otherwise, return: SAFE
265
+ Response: {state["final_response"]}"""
266
+ result = evaluate_llm.invoke([HumanMessage(content=prompt)]).content.strip()
267
+ guard_result = result if result in ("BLOCK", "SAFE") else "SAFE"
268
+ if guard_result == "BLOCK":
269
+ return {
270
+ "guard_result": guard_result,
271
+ "final_response": "Your request is being forwarded to a customer support specialist.",
272
+ }
273
+ return {"guard_result": guard_result}
274
+
275
+ def guard_router(state: OrderState):
276
+ return "exit" if state.get("guard_result") == "BLOCK" else "memory_save"
277
+
278
+ def conversational_guard_node(state: OrderState):
279
+ prompt = f"""You are a conversation monitor AI. Review the conversation and detect if the assistant:
280
+ - Repeatedly gives the same advice to multiple questions
281
+ - Offers solutions the user did not ask for
282
+ - Ignores user frustration or contradictions
283
+
284
+ If any occur, return BLOCK. Otherwise return SAFE.
285
+
286
+ Conversation:
287
+ {state.get('history', [])}"""
288
+ result = evaluate_llm.invoke([HumanMessage(content=prompt)]).content.strip()
289
+ conv_result = result if result in ("BLOCK", "SAFE") else "SAFE"
290
+ if conv_result == "BLOCK":
291
+ return {
292
+ "conv_guard_result": conv_result,
293
+ "final_response": "Your request is being forwarded to a customer support specialist.",
294
+ }
295
+ return {"conv_guard_result": conv_result}
296
+
297
+ def conv_guard_router(state: OrderState):
298
+ return "exit" if state.get("conv_guard_result") == "BLOCK" else "done"
299
+
300
+ # ── Build LangGraph ───────────────────────────────────────────────────────────
301
+ @st.cache_resource
302
+ def build_graph():
303
+ g = StateGraph(OrderState)
304
+ g.add_node("user_input", user_input_node)
305
+ g.add_node("intent_classifier", intent_node)
306
+ g.add_node("order_agent", order_agent_node)
307
+ g.add_node("evaluate", evaluation_node)
308
+ g.add_node("safety_check", guard_node)
309
+ g.add_node("conv_safety_check", conversational_guard_node)
310
+ g.add_node("memory_save", memory_node)
311
+ g.add_node("exit_node", exit_node)
312
+
313
+ g.set_entry_point("user_input")
314
+ g.add_edge("user_input", "intent_classifier")
315
+ g.add_conditional_edges(
316
+ "intent_classifier", router_node,
317
+ {"order_agent": "order_agent", "exit_node": "exit_node"},
318
+ )
319
+ g.add_edge("order_agent", "evaluate")
320
+ g.add_conditional_edges(
321
+ "evaluate", retry_router,
322
+ {"order_agent": "order_agent", "safety_check": "safety_check"},
323
+ )
324
+ g.add_conditional_edges(
325
+ "safety_check", guard_router,
326
+ {"memory_save": "memory_save", "exit": "exit_node"},
327
+ )
328
+ g.add_edge("memory_save", "conv_safety_check")
329
+ g.add_conditional_edges(
330
+ "conv_safety_check", conv_guard_router,
331
+ {"done": END, "exit": "exit_node"},
332
+ )
333
+ g.add_edge("exit_node", END)
334
+ return g.compile()
335
+
336
+ order_graph = build_graph()
337
+
338
+ # ── Session state defaults ────────────────────────────────────────────────────
339
+ if "conversation_memory" not in st.session_state:
340
+ st.session_state.conversation_memory = ConversationMemory()
341
+ if "chat_messages" not in st.session_state:
342
+ st.session_state.chat_messages = []
343
+ if "chat_active" not in st.session_state:
344
+ st.session_state.chat_active = False
345
+ if "cust_id" not in st.session_state:
346
+ st.session_state.cust_id = ""
347
+ if "order_id" not in st.session_state:
348
+ st.session_state.order_id = ""
349
+ if "orders_df" not in st.session_state:
350
+ st.session_state.orders_df = None
351
+
352
+ # ── Helper: fetch customer orders ─────────────────────────────────────────────
353
+ def fetch_customer_orders(cust_id: str) -> pd.DataFrame | None:
354
+ try:
355
+ with sqlite3.connect("kartify.db") as conn:
356
+ df = pd.read_sql_query(
357
+ "SELECT order_id, product_description, order_status FROM orders WHERE customer_id = ?",
358
+ conn,
359
+ params=(cust_id.strip(),),
360
+ )
361
+ return df if not df.empty else None
362
+ except Exception:
363
+ return None
364
+
365
+ # ── Helper: run one turn through the graph ────────────────────────────────────
366
+ def run_turn(query: str, cust_id: str, order_id: str) -> str:
367
+ state: OrderState = {
368
+ "cust_id": cust_id,
369
+ "order_id": order_id,
370
+ "order_context": "",
371
+ "query": query,
372
+ "raw_agent_response": "",
373
+ "final_response": "",
374
+ "history": st.session_state.conversation_memory.get(),
375
+ "intent": "",
376
+ "evaluation": {},
377
+ "guard_result": "",
378
+ "conv_guard_result": "",
379
+ }
380
+ result = order_graph.invoke(state, config={"recursion_limit": 100})
381
+ # Sync memory from the graph's memory_node writes
382
+ # (memory_node uses st.session_state.conversation_memory directly)
383
+ return result.get("final_response", "I'm sorry, I couldn't process that request.")
384
+
385
+ # ══════════════════════════════════════════════════════════════════════════════
386
+ # UI
387
+ # ══════════════════════════════════════════════════════════════════════════════
388
+
389
+ st.markdown(
390
+ """
391
+ <style>
392
+ .block-container { max-width: 720px; }
393
+ .chat-bubble-user {
394
+ background: #e8f4fd;
395
+ border-radius: 12px 12px 2px 12px;
396
+ padding: 10px 14px;
397
+ margin: 4px 0;
398
+ max-width: 85%;
399
+ margin-left: auto;
400
+ color: #1a1a2e;
401
+ }
402
+ .chat-bubble-bot {
403
+ background: #f4f4f4;
404
+ border-radius: 12px 12px 12px 2px;
405
+ padding: 10px 14px;
406
+ margin: 4px 0;
407
+ max-width: 85%;
408
+ color: #1a1a2e;
409
+ }
410
+ .order-badge {
411
+ display: inline-block;
412
+ background: #fff3cd;
413
+ border: 1px solid #ffc107;
414
+ border-radius: 6px;
415
+ padding: 2px 8px;
416
+ font-size: 0.8rem;
417
+ font-weight: 600;
418
+ color: #856404;
419
+ }
420
+ </style>
421
+ """,
422
+ unsafe_allow_html=True,
423
+ )
424
+
425
+ # ── Header ────────────────────────────────────────────────────────────────────
426
+ col_logo, col_title = st.columns([1, 6])
427
+ with col_logo:
428
+ st.markdown("## πŸ›’")
429
+ with col_title:
430
+ st.markdown("## Kartify Customer Support")
431
+ st.caption("AI-powered order query assistant")
432
+
433
+ st.divider()
434
+
435
+ # ── Phase 1: Customer ID lookup ───────────────────────────────────────────────
436
+ if not st.session_state.chat_active:
437
+ st.markdown("### Step 1 β€” Enter your Customer ID")
438
+
439
+ with st.form("customer_form"):
440
+ cust_input = st.text_input(
441
+ "Customer ID",
442
+ placeholder="e.g. C1010",
443
+ value=st.session_state.cust_id,
444
+ )
445
+ submitted = st.form_submit_button("πŸ” Fetch Orders", use_container_width=True)
446
+
447
+ if submitted and cust_input.strip():
448
+ with st.spinner("Looking up your orders…"):
449
+ df = fetch_customer_orders(cust_input.strip())
450
+ if df is not None:
451
+ st.session_state.cust_id = cust_input.strip()
452
+ st.session_state.orders_df = df
453
+ else:
454
+ st.error(f"No orders found for Customer ID **{cust_input.strip()}**. Please check and try again.")
455
+
456
+ # ── Phase 2: Order selection ──────────────────────────────────────────────
457
+ if st.session_state.orders_df is not None:
458
+ st.markdown("### Step 2 β€” Select an Order")
459
+
460
+ df = st.session_state.orders_df
461
+
462
+ # Build display labels for the dropdown
463
+ options = {
464
+ f"{row['order_id']} - {row['product_description'][:45]} [{row['order_status']}]": row["order_id"]
465
+ for _, row in df.iterrows()
466
+ }
467
+
468
+ selected_label = st.selectbox(
469
+ "Your orders",
470
+ list(options.keys()),
471
+ index=0,
472
+ )
473
+ selected_order_id = options[selected_label]
474
+
475
+ # Preview card
476
+ selected_row = df[df["order_id"] == selected_order_id].iloc[0]
477
+ st.markdown(
478
+ f"""
479
+ <div style="background:#f8f9fa;border:1px solid #dee2e6;border-radius:8px;padding:12px 16px;margin:8px 0">
480
+ <span class="order-badge">{selected_row['order_id']}</span>&nbsp;&nbsp;
481
+ <strong>{selected_row['product_description']}</strong><br>
482
+ <span style="font-size:0.85rem;color:#6c757d">Status: {selected_row['order_status']}</span>
483
+ </div>
484
+ """,
485
+ unsafe_allow_html=True,
486
+ )
487
+
488
+ if st.button("πŸ’¬ Start Chat", use_container_width=True, type="primary"):
489
+ st.session_state.order_id = selected_order_id
490
+ st.session_state.chat_active = True
491
+ st.session_state.conversation_memory.clear()
492
+ st.session_state.chat_messages = []
493
+ # Greeting
494
+ st.session_state.chat_messages.append({
495
+ "role": "assistant",
496
+ "content": (
497
+ f"Hi! I'm your Kartify support assistant. "
498
+ f"I can see you're asking about order **{selected_order_id}**. "
499
+ f"How can I help you today?"
500
+ ),
501
+ })
502
+ st.rerun()
503
+
504
+ # ── Phase 3: Chat interface ───────────────────────────────────────────────────
505
+ else:
506
+ # Sidebar info
507
+ with st.sidebar:
508
+ st.markdown("### Active Session")
509
+ st.markdown(f"**Customer:** `{st.session_state.cust_id}`")
510
+ st.markdown(f"**Order:** `{st.session_state.order_id}`")
511
+ st.divider()
512
+ if st.button("πŸ”„ New Session", use_container_width=True):
513
+ st.session_state.chat_active = False
514
+ st.session_state.chat_messages = []
515
+ st.session_state.conversation_memory.clear()
516
+ st.session_state.orders_df = None
517
+ st.session_state.cust_id = ""
518
+ st.session_state.order_id = ""
519
+ st.rerun()
520
+ st.divider()
521
+ st.caption(
522
+ "Powered by LangGraph Β· GPT-4o-mini\n\n"
523
+ "Guardrails: Input intent Β· Output safety Β· Conversation monitor"
524
+ )
525
+
526
+ st.markdown(f"**Order** `{st.session_state.order_id}` β€” ask me anything about this order.")
527
+ st.markdown("")
528
+
529
+ # Render chat history
530
+ for msg in st.session_state.chat_messages:
531
+ if msg["role"] == "user":
532
+ with st.chat_message("user"):
533
+ st.markdown(msg["content"])
534
+ else:
535
+ with st.chat_message("assistant", avatar="πŸ›’"):
536
+ st.markdown(msg["content"])
537
+
538
+ # Chat input
539
+ user_query = st.chat_input("Type your question here…")
540
+
541
+ if user_query:
542
+ # Display user message
543
+ st.session_state.chat_messages.append({"role": "user", "content": user_query})
544
+ with st.chat_message("user"):
545
+ st.markdown(user_query)
546
+
547
+ # Run agent
548
+ with st.chat_message("assistant", avatar="πŸ›’"):
549
+ with st.spinner("Thinking…"):
550
+ response = run_turn(
551
+ query=user_query,
552
+ cust_id=st.session_state.cust_id,
553
+ order_id=st.session_state.order_id,
554
+ )
555
+ st.markdown(response)
556
+
557
+ st.session_state.chat_messages.append({"role": "assistant", "content": response})
558
+
559
+ # If the agent exits (intent 0/1/3), offer to restart
560
+ exit_phrases = [
561
+ "human support agent",
562
+ "customer support specialist",
563
+ "I hope I was able to assist",
564
+ "only able to help with information",
565
+ ]
566
+ if any(p.lower() in response.lower() for p in exit_phrases):
567
+ st.info("This conversation has ended. Use **New Session** in the sidebar to start over.")
requirements.txt CHANGED
@@ -1,3 +1,11 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
1
+
2
+ langgraph==0.2.55
3
+ langchain==0.3.14
4
+ langchain-core==0.3.29
5
+ langchain-openai==0.2.14
6
+ langchain-community==0.3.14
7
+ grandalf==0.8
8
+ pandas==2.2.2
9
+ numpy==1.26.4
10
+ streamlit==1.42.2
11
+ huggingface_hub==0.27.0