SudeendraMG commited on
Commit
6a7793f
·
verified ·
1 Parent(s): 00e62c8

Upload 2 files

Browse files
Files changed (2) hide show
  1. agent/chat_agent.py +362 -0
  2. agent/sql_agent.py +575 -0
agent/chat_agent.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 12.2
2
+ # ================================================================
3
+ # FILE: chat_agent.py
4
+ # ---------------------------------------------------------------
5
+ # FoodHub Conversational Assistant (Groq-exclusive version)
6
+ # ---------------------------------------------------------------
7
+ # PURPOSE:
8
+ # - Handles all user-facing chat interactions for FoodHub.
9
+ # - Uses Groq-hosted LLaMA 4 model for short (<80 words), polite,
10
+ # and context-aware responses.
11
+ # - Detects intent (promo, refund, handoff, farewell, etc.)
12
+ # and responds accordingly.
13
+ # - Enforces data privacy and safety policies.
14
+ # ================================================================
15
+
16
+ import os
17
+ import re
18
+ import streamlit as st
19
+ import sys
20
+ from langchain_groq import ChatGroq
21
+
22
+ from langchain.agents import initialize_agent, Tool
23
+ from langchain_core.messages import SystemMessage, HumanMessage
24
+ from langchain.agents.agent_types import AgentType
25
+
26
+ import warnings
27
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
28
+
29
+ # ================================================================
30
+ # SECTION 1: LLM Initialization (Low Temperature)
31
+ # ---------------------------------------------------------------
32
+ # Purpose:
33
+ # Sets up a deterministic Groq-powered Large Language Model (LLM)
34
+ # with low temperature (0.0) for predictable and consistent outputs.
35
+ # Fetches the API key securely from Streamlit secrets or environment
36
+ # variables and stops execution if missing.
37
+ # ================================================================
38
+
39
+ @st.cache_resource
40
+ def initialize_llm_high():
41
+ """
42
+ Initialize the Groq-based LLM with high creativity (temperature = 0.7).
43
+
44
+ Workflow:
45
+ 1️⃣ Retrieve Groq API key (from Streamlit secrets or environment variable).
46
+ 2️⃣ Validate key existence; stop execution if not found.
47
+ 3️⃣ Configure and return a ChatGroq instance for deterministic responses.
48
+ """
49
+
50
+ # ------------------------------------------------------------
51
+ # Step 1: Retrieve Groq API Key
52
+ # Attempt to load the API key securely from Streamlit secrets;
53
+ # if not found, fallback to system environment variable.
54
+ # ------------------------------------------------------------
55
+ try:
56
+ groq_api_key = st.secrets["GROQ_API_KEY"]
57
+ except:
58
+ groq_api_key = os.getenv("GROQ_API_KEY")
59
+
60
+ # ------------------------------------------------------------
61
+ # Step 2: Validate API Key
62
+ # If the key is missing, display a helpful error message
63
+ # and stop further execution to prevent runtime failures.
64
+ # ------------------------------------------------------------
65
+ if not groq_api_key:
66
+ st.error("⚠️ GROQ_API_KEY Environment Variable Not Found! Please set the environment variable.")
67
+ st.info("Please create a `.streamlit/secrets.toml` file with:\n```\nGROQ_API_KEY = \"your-api-key-here\"\n```")
68
+ st.stop()
69
+
70
+ # ------------------------------------------------------------
71
+ # Step 3: Configure and Initialize Groq LLM
72
+ # Create a ChatGroq instance using a high-temperature setup
73
+ # for Conversational and natural sounding responses.
74
+ # ------------------------------------------------------------
75
+ llmh = ChatGroq(
76
+ model="meta-llama/llama-4-scout-17b-16e-instruct", # Groq-hosted LLaMA model
77
+ temperature=0.7, # High temperature → Conversational output
78
+ max_tokens=200, # Limit response size
79
+ max_retries=0, # No automatic retries
80
+ groq_api_key=groq_api_key # Secure API key injection
81
+ )
82
+
83
+ # ------------------------------------------------------------
84
+ # Step 4: Return Cached LLM Instance
85
+ # The LLM object is cached to avoid reinitialization overhead.
86
+ # ------------------------------------------------------------
87
+ return llmh
88
+
89
+
90
+ # ================================================================
91
+ # SECTION 2: Create Global LLM Instance
92
+ # ---------------------------------------------------------------
93
+ # Initializes the cached High-temperature LLM for consistent use
94
+ # across the Streamlit app pipeline for conversational response.
95
+ # ================================================================
96
+ llm_high = initialize_llm_high()
97
+
98
+
99
+ # ================================================================
100
+ # SECTION 3: Escalation Detection
101
+ # ---------------------------------------------------------------
102
+ # Purpose:
103
+ # Identifies user queries that indicate unresolved issues,
104
+ # urgency, dissatisfaction, or explicit requests to speak
105
+ # with a human support representative.
106
+ # Helps route critical or frustrated customer messages
107
+ # to human agents for faster resolution.
108
+ # ================================================================
109
+
110
+ def check_escalation(user_query: str) -> str:
111
+ """
112
+ Detects whether a user's message requires escalation to human support.
113
+ Logic:
114
+ - Scans the user query for specific keywords or phrases that suggest:
115
+ * Repeated complaints or unresolved issues.
116
+ * Requests for urgent or immediate attention.
117
+ * Direct mentions of escalation, dissatisfaction, or need for human help.
118
+ - Returns:
119
+ * "Escalated" → if any escalation keyword is detected.
120
+ * "Not Escalated" → if no escalation indicators are present.
121
+ """
122
+
123
+ # ------------------------------------------------------------
124
+ # Step 1: Define escalation-related keywords and phrases
125
+ # These capture user frustration, urgency, or explicit escalation intent.
126
+ # ------------------------------------------------------------
127
+ escalation_kw_list = [
128
+ "issue persists", "not resolved", "complaint", "contact human",
129
+ "priority", "immediate", "service failure", "speak to manager",
130
+ "support required", "help me now", "not satisfied", "request escalation",
131
+ "critical issue", "issue unresolved", "need assistance", "escalation",
132
+ "problem still exists", "no response", "cannot resolve", "urgent",
133
+ "multiple times", "immediate response", "problem", "escalate",
134
+ "still not working"
135
+ ]
136
+
137
+ # ------------------------------------------------------------
138
+ # Step 2: Check for escalation triggers in the user’s query
139
+ # Perform a case-insensitive match of any keyword in the query text.
140
+ # ------------------------------------------------------------
141
+ if any(keyword in user_query.lower() for keyword in escalation_kw_list):
142
+ return "Escalated" # 🚨 Escalation required — route to human support
143
+
144
+ # ------------------------------------------------------------
145
+ # Step 3: No escalation keywords found — proceed normally
146
+ # ------------------------------------------------------------
147
+ return "Not Escalated"
148
+
149
+
150
+ # ================================================================
151
+ # SECTION 4: Order Cancellation Handler
152
+ # ---------------------------------------------------------------
153
+ # Purpose:
154
+ # Processes and validates customer cancellation requests
155
+ # based on the current order status.
156
+ # Ensures cancellations are not permitted for orders that
157
+ # are already delivered, canceled, or beyond the preparation stage.
158
+ # ================================================================
159
+
160
+ def handle_cancellation(user_query: str, raw_orders: str, order_status: str) -> str:
161
+ """
162
+ Handles customer order cancellation requests logically and politely.
163
+ Logic:
164
+ - Identifies if the user’s message contains a cancellation intent.
165
+ - Evaluates the current order status and determines whether cancellation
166
+ is still possible.
167
+ - Returns a context-appropriate message explaining the outcome.
168
+ """
169
+
170
+ # ------------------------------------------------------------
171
+ # Step 1: Detect cancellation intent in the user’s query
172
+ # If the message doesn’t contain the word “cancel”, skip processing.
173
+ # ------------------------------------------------------------
174
+ if "cancel" not in user_query.lower():
175
+ return ""
176
+
177
+ # ------------------------------------------------------------
178
+ # Step 2: Check if order is already completed or canceled
179
+ # In such cases, cancellation cannot be performed again.
180
+ # ------------------------------------------------------------
181
+ if order_status and order_status.lower() in ["delivered", "canceled"]:
182
+ return (
183
+ f"Your order has already been {order_status.lower()}. "
184
+ "Cancellation is therefore not possible. We appreciate your understanding!"
185
+ )
186
+
187
+ # ------------------------------------------------------------
188
+ # Step 3: Check if order is already being prepared or picked up
189
+ # Once food preparation or pickup starts, cancellations are disallowed.
190
+ # ------------------------------------------------------------
191
+ elif order_status and order_status.lower() in ["preparing food", "picked up"]:
192
+ return (
193
+ f"Your order is currently {order_status.lower()}. "
194
+ "Unfortunately, cancellations are not permitted at this stage. Thank you for your understanding!"
195
+ )
196
+
197
+ # ------------------------------------------------------------
198
+ # Step 4: Default case — cancellation not allowed for unspecified reasons
199
+ # ------------------------------------------------------------
200
+ else:
201
+ return (
202
+ "Your order cannot be canceled at this moment. "
203
+ "We appreciate your patience and look forward to serving you again!"
204
+ )
205
+
206
+ # ================================================================
207
+ # SECTION 5: Answer Tool — Final Response Generator
208
+ # ---------------------------------------------------------------
209
+ # Purpose:
210
+ # Processes the structured output from `OrderQueryTool`,
211
+ # interprets order details, applies escalation or cancellation logic,
212
+ # and generates a natural, customer-friendly response using the LLM.
213
+ # ================================================================
214
+
215
+ # ----------------------------------------------------------------
216
+ # Function: answer_tool_func()
217
+ # Description:
218
+ # - Receives a stringified dictionary from the previous tool.
219
+ # - Parses and validates it.
220
+ # - Checks for escalation or cancellation triggers.
221
+ # - Uses the LLM to craft the final user-facing message.
222
+ # ----------------------------------------------------------------
223
+ def answer_tool_func(answertool_input: str) -> str:
224
+ """
225
+ Receives the output from OrderQueryTool as stringified dict,
226
+ parses it, and generates the final friendly message.
227
+ """
228
+ # ------------------------------------------------------------
229
+ # Step 1: Parse the input dictionary safely
230
+ # ------------------------------------------------------------
231
+ try:
232
+ data = ast.literal_eval(answertool_input)
233
+ cust_id = data.get("cust_id", "Unknown")
234
+ user_query = data.get("orig_query", "")
235
+ db_response = data.get("db_response", "No order details found.")
236
+ except Exception:
237
+ # Handle invalid or malformed data gracefully
238
+ return "⚠️ Error: Could not parse order data properly."
239
+
240
+ # Initialize key order-related variables
241
+ order_status = None
242
+ item_in_order = None
243
+ preparing_eta = None
244
+ delivery_time = None
245
+
246
+ print('answer_tool_func : LEVEL-1 Done',flush=True)
247
+ print('cust_id = ',cust_id, flush=True)
248
+ print('orig_query = ',user_query, flush=True)
249
+ print('db_response = ',db_response, flush=True)
250
+ sys.stdout.flush()
251
+
252
+ # ------------------------------------------------------------
253
+ # Step 2: Extract order details from db_response text
254
+ # ------------------------------------------------------------
255
+ for line in db_response.splitlines():
256
+ if "Order Status" in line:
257
+ order_status = line.split(":", 1)[1].strip()
258
+ elif "Preparing ETA" in line:
259
+ preparing_eta = line.split(":", 1)[1].strip()
260
+ elif "Delivery Time" in line:
261
+ delivery_time = line.split(":", 1)[1].strip()
262
+
263
+ # ------------------------------------------------------------
264
+ # Step 3: Detect if query needs escalation (critical or unresolved issues)
265
+ # ------------------------------------------------------------
266
+ escalation_var = check_escalation(user_query)
267
+ if escalation_var == "Escalated":
268
+ return (
269
+ f"The current status of your order is: {order_status.lower()}. " +
270
+ "⚠️ This issue needs urgent attention. " +
271
+ "Your request has been escalated to a human support agent who will reach out to you soon."
272
+ )
273
+
274
+ #print('answer_tool_func : LEVEL-2 Done',flush=True)
275
+ #sys.stdout.flush()
276
+
277
+ # ------------------------------------------------------------
278
+ # Step 4: Check for order cancellation requests
279
+ # ------------------------------------------------------------
280
+ cancel_response = handle_cancellation(user_query, db_response, order_status)
281
+ if cancel_response: # Return cancellation message if applicable
282
+ return cancel_response
283
+
284
+ #print('answer_tool_func : LEVEL-3 Done',flush=True)
285
+ #sys.stdout.flush()
286
+
287
+ #return "Forced: Thank you and your order conatins Steak..!"
288
+
289
+ # ------------------------------------------------------------
290
+ # Step 5: Build the system prompt for LLM to interpret and respond
291
+ # ------------------------------------------------------------
292
+ system_prompt = f"""
293
+ You are a warm and helpful customer support assistant for FoodHub.
294
+ Customer ID: {cust_id}
295
+ Below is the customer's order information retrieved from the database:
296
+ {db_response}
297
+ Sample raw_orders format:
298
+ order_id: O12493,
299
+ cust_id: C1018,
300
+ order_time: 12:35,
301
+ order_status: picked up,
302
+ payment_status: COD,
303
+ item_in_order: Steak,
304
+ preparing_eta: 12:50,
305
+ prepared_time: 12:50,
306
+ delivery_eta: 1:10,
307
+ delivery_time: None
308
+ Response Instructions:
309
+ 1. Respond in a friendly, natural, and concise tone — keep replies short.
310
+ 2. Use only the details from `db_response`. Do not infer or create extra info.
311
+ 3. Convert database text into polite, human-readable responses.
312
+ 4. When order_status = 'preparing food':
313
+ - Include both 'preparing_eta' and 'delivery_eta'.
314
+ - If 'delivery_eta' is missing or None, say: "Your order is being prepared, and the delivery ETA will be available soon."
315
+ 5. When order_status = 'delivered', include 'delivery_time' in the message.
316
+ 6. When order_status = 'canceled', explain politely and empathetically.
317
+ 7. When order_status = 'picked up':
318
+ - Include 'delivery_eta' if available.
319
+ - If 'delivery_eta' is missing or None, say: "Your order has been picked up, and the delivery ETA will be available soon."
320
+ 8. If the user query contains “Where is my order”, include the current 'order_status'.
321
+ 9. If the user query includes “How many items”, count the 'item_in_order' list and reply like:
322
+ "Your order includes 3 items."
323
+ """
324
+
325
+ # ------------------------------------------------------------
326
+ # Step 6: Build and send user-specific prompt to LLM
327
+ # ------------------------------------------------------------
328
+ user_prompt = f"User Query: {user_query}"
329
+
330
+ # Generate final response using the configured LLM
331
+ response_msg = llm_high.predict_messages([
332
+ SystemMessage(content=system_prompt),
333
+ HumanMessage(content=user_prompt)
334
+ ])
335
+
336
+ # ------------------------------------------------------------
337
+ # Step 7: Clean and finalize the LLM response
338
+ # ------------------------------------------------------------
339
+ response = response_msg.content.strip()
340
+
341
+ #print('answer_tool_func : LEVEL-4 Done; response = ',response, flush=True)
342
+ #sys.stdout.flush()
343
+
344
+ # Provide fallback message in case of empty or invalid response
345
+ if not response:
346
+ return "Sorry, we could not extract your order details at this time. Please try again later.."
347
+
348
+ # Return the final generated response
349
+ return response
350
+
351
+
352
+ # ================================================================
353
+ # SECTION 6: LangChain Tool Wrapper
354
+ # ---------------------------------------------------------------
355
+ # Wraps the chat handler as a LangChain Tool so that it can be
356
+ # called within multi-agent workflows or pipelines.
357
+ # ================================================================
358
+ #AnswerTool = Tool(
359
+ # name="answer_tool",
360
+ # func=answer_tool_func,
361
+ # description="Format raw DB results into a brief, polite user-facing message. Enforces business rules (cancelled/completed messaging, escalation)."
362
+ #)
agent/sql_agent.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 12.1
2
+ # ================================================================
3
+ # FILE: sql_agent.py
4
+ # MODULE: FoodHub Secure SQL Query Handler (Groq-exclusive)
5
+ # ---------------------------------------------------------------
6
+ # PURPOSE:
7
+ # Safely processes natural language queries into secure, read-only
8
+ # SQL statements using Groq-powered deterministic LLM reasoning.
9
+ #
10
+ # KEY FEATURES:
11
+ # ✅ SELECT-only enforcement (no data modification)
12
+ # ✅ Restricted to specific cust_id
13
+ # ✅ Anti-enumeration and anti-destructive query filters
14
+ # ✅ Dynamic schema inspection and caching
15
+ # ✅ Deterministic (low-temperature) LLM for reproducibility
16
+ # ================================================================
17
+
18
+ import os
19
+ import re
20
+ import sqlite3
21
+ import textwrap
22
+ import traceback
23
+ import pandas as pd
24
+ import ast
25
+ import sys
26
+ import streamlit as st
27
+
28
+ from functools import lru_cache
29
+ from typing import Any, Dict, List, Tuple
30
+
31
+ from langchain.agents import create_sql_agent, initialize_agent, Tool
32
+ from langchain_core.messages import SystemMessage, HumanMessage
33
+ from langchain.agents.agent_types import AgentType
34
+ from langchain.sql_database import SQLDatabase
35
+ from langchain.agents.agent_toolkits import SQLDatabaseToolkit
36
+ from langchain_groq import ChatGroq
37
+ import warnings
38
+
39
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
40
+
41
+ # ================================================================
42
+ # SECTION 1: Database Initialization
43
+ # ---------------------------------------------------------------
44
+ # Purpose:
45
+ # Establishes a connection to the SQLite database used by the
46
+ # FoodHub Chatbot. Ensures that the file exists before proceeding
47
+ # and gracefully handles missing database scenarios.
48
+ # ================================================================
49
+
50
+ @st.cache_resource
51
+ def create_database():
52
+ """
53
+ Initialize and cache the database connection.
54
+
55
+ Workflow:
56
+ 1️⃣ Define database file path.
57
+ 2️⃣ Validate file existence.
58
+ 3️⃣ Establish SQLite connection via LangChain SQLDatabase.
59
+ 4️⃣ Cache the connection using Streamlit’s resource cache.
60
+ """
61
+ # ------------------------------------------------------------
62
+ # Step 1: Define Database Path
63
+ # Specify the location of the SQLite database file.
64
+ # ------------------------------------------------------------
65
+ db_path = "customer_orders.db"
66
+
67
+ # ------------------------------------------------------------
68
+ # Step 2: Validate Database Existence
69
+ # If the file is not found, display a Streamlit error message
70
+ # and halt further execution to prevent runtime failures.
71
+ # ------------------------------------------------------------
72
+ if not os.path.exists(db_path):
73
+ st.error(f"Database file not found at: {db_path}")
74
+ st.stop()
75
+
76
+ # ------------------------------------------------------------
77
+ # Step 3: Establish Connection
78
+ # Create a LangChain SQLDatabase object from the SQLite file.
79
+ # ------------------------------------------------------------
80
+ db = SQLDatabase.from_uri(f"sqlite:///{db_path}")
81
+
82
+ # ------------------------------------------------------------
83
+ # Step 4: Return Cached Connection
84
+ # The connection is cached using Streamlit's @st.cache_resource
85
+ # decorator to avoid redundant initialization.
86
+ # ------------------------------------------------------------
87
+ return db
88
+
89
+
90
+ # ================================================================
91
+ # SECTION 2: Database Instance Creation
92
+ # ---------------------------------------------------------------
93
+ # Creates the global database object by invoking create_database().
94
+ # This instance will be shared across all app components.
95
+ # ================================================================
96
+ db_orders = create_database()
97
+
98
+
99
+ # ================================================================
100
+ # SECTION 3: LLM Initialization (Low Temperature)
101
+ # ---------------------------------------------------------------
102
+ # Purpose:
103
+ # Sets up a deterministic Groq-powered Large Language Model (LLM)
104
+ # with low temperature (0.0) for predictable and consistent outputs.
105
+ # Fetches the API key securely from Streamlit secrets or environment
106
+ # variables and stops execution if missing.
107
+ # ================================================================
108
+
109
+ @st.cache_resource
110
+ def initialize_llm_low():
111
+ """
112
+ Initialize the Groq-based LLM with low creativity (temperature = 0).
113
+
114
+ Workflow:
115
+ 1️⃣ Retrieve Groq API key (from Streamlit secrets or environment variable).
116
+ 2️⃣ Validate key existence; stop execution if not found.
117
+ 3️⃣ Configure and return a ChatGroq instance for deterministic responses.
118
+ """
119
+
120
+ # ------------------------------------------------------------
121
+ # Step 1: Retrieve Groq API Key
122
+ # Attempt to load the API key securely from Streamlit secrets;
123
+ # if not found, fallback to system environment variable.
124
+ # ------------------------------------------------------------
125
+ try:
126
+ groq_api_key = st.secrets["GROQ_API_KEY"]
127
+ except:
128
+ groq_api_key = os.getenv("GROQ_API_KEY")
129
+
130
+ # ------------------------------------------------------------
131
+ # Step 2: Validate API Key
132
+ # If the key is missing, display a helpful error message
133
+ # and stop further execution to prevent runtime failures.
134
+ # ------------------------------------------------------------
135
+ if not groq_api_key:
136
+ st.error("⚠️ GROQ_API_KEY Environment Variable Not Found! Please set the environment variable.")
137
+ st.info("Please create a `.streamlit/secrets.toml` file with:\n```\nGROQ_API_KEY = \"your-api-key-here\"\n```")
138
+ st.stop()
139
+
140
+ # ------------------------------------------------------------
141
+ # Step 3: Configure and Initialize Groq LLM
142
+ # Create a ChatGroq instance using a low-temperature setup
143
+ # for deterministic and reliable responses.
144
+ # ------------------------------------------------------------
145
+ llm = ChatGroq(
146
+ model="meta-llama/llama-4-scout-17b-16e-instruct", # Groq-hosted LLaMA model
147
+ temperature=0, # Low temperature → consistent output
148
+ max_tokens=200, # Limit response size
149
+ max_retries=0, # No automatic retries
150
+ groq_api_key=groq_api_key # Secure API key injection
151
+ )
152
+
153
+ # ------------------------------------------------------------
154
+ # Step 4: Return Cached LLM Instance
155
+ # The LLM object is cached to avoid reinitialization overhead.
156
+ # ------------------------------------------------------------
157
+ return llm
158
+
159
+
160
+ # ================================================================
161
+ # SECTION 4: Create Global LLM Instance
162
+ # ---------------------------------------------------------------
163
+ # Initializes the cached low-temperature LLM for consistent use
164
+ # across the Streamlit app pipeline.
165
+ # ================================================================
166
+ llm_low = initialize_llm_low()
167
+
168
+ # ================================================================
169
+ # SECTION 5: Database Agent Setup
170
+ # ---------------------------------------------------------------
171
+ # Purpose:
172
+ # Initializes the SQL Agent responsible for interacting with
173
+ # the SQLite database containing customer order information.
174
+ # The agent follows strict query and safety policies to ensure
175
+ # correct and limited database access.
176
+ # ================================================================
177
+
178
+ # ---------------------------------------------------------------
179
+ # Step 1: Define System Message
180
+ # ---------------------------------------------------------------
181
+ # The system message defines the agent’s behavior and rules.
182
+ # It strictly limits queries to the 'orders' table and enforces
183
+ # a one-to-one mapping between cust_id and order_id.
184
+ # ---------------------------------------------------------------
185
+ system_message = """
186
+ You are a SQLite database agent.
187
+ Your database contains customer orders.
188
+ Table and schema:
189
+ orders (
190
+ order_id TEXT,
191
+ cust_id TEXT,
192
+ order_time TEXT,
193
+ order_status TEXT,
194
+ payment_status TEXT,
195
+ item_in_order TEXT,
196
+ preparing_eta TEXT,
197
+ prepared_time TEXT,
198
+ delivery_eta TEXT,
199
+ delivery_time TEXT
200
+ )
201
+ Instructions:
202
+ - Always query the orders table only — do not reference or search other tables.
203
+ - Each cust_id corresponds to exactly one order_id.
204
+ - Return one SQL query along with its direct result only.
205
+ - Do not execute loops, retries, or multiple queries for a single request.
206
+ - If no record exists for the given cust_id, return: "No cust_id found".
207
+ - Display only the query result, with no explanations or extra text.
208
+ - The column item_in_order may include several items separated by commas (e.g., "Fish, Juice, Nachos").
209
+ """
210
+
211
+ # ---------------------------------------------------------------
212
+ # Step 2: Initialize SQL Toolkit
213
+ # ---------------------------------------------------------------
214
+ # Combines the SQLite database connection with the Groq-powered LLM.
215
+ # This toolkit provides SQL-aware reasoning capabilities to the agent.
216
+ # ---------------------------------------------------------------
217
+ toolkit = SQLDatabaseToolkit(db=db_orders, llm=llm_low)
218
+
219
+ # ---------------------------------------------------------------
220
+ # Step 3: Create SQL Agent
221
+ # ---------------------------------------------------------------
222
+ # Constructs the SQL Agent with the following properties:
223
+ # - Uses the low-temperature LLM (deterministic responses)
224
+ # - Handles parsing errors gracefully
225
+ # - Operates with ZERO_SHOT_REACT_DESCRIPTION reasoning type
226
+ # ---------------------------------------------------------------
227
+ sql_db_agent = create_sql_agent(
228
+ llm=llm_low, # Deterministic Groq LLM
229
+ toolkit=toolkit, # SQL toolkit for database access
230
+ verbose=False, # Suppress console logs
231
+ system_message=SystemMessage(system_message), # Behavioral and rule definition
232
+ handle_parsing_errors=True, # Recover from minor parsing issues
233
+ agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION # React-style reasoning agent
234
+ )
235
+
236
+ # ================================================================
237
+ def _query_id_match(cust_id: str, query: str) -> bool:
238
+ """Verify that cust_id exists in at least one expected table."""
239
+ # STEP 1: Resolve file path and connect to SQLite
240
+ conn = sqlite3.connect("customer_orders.db")
241
+ cur = conn.cursor()
242
+
243
+ # Step 2: Run SQL directly using the connection
244
+ qc = f"SELECT order_id FROM orders WHERE cust_id='{cust_id}';"
245
+ db_order_id = pd.read_sql_query(qc, conn)
246
+
247
+ # STEP 3:
248
+ # Extract customer ID if present in the query
249
+ return_value = True
250
+ qc_cid = []
251
+ cidcnt = 0
252
+ for match in re.findall(r"\bC\d{4}\b", query, flags=re.IGNORECASE):
253
+ if match:
254
+ cidcnt += 1
255
+ qc_cid = match.upper()
256
+ print('qc_cid = ', qc_cid)
257
+ if qc_cid != cust_id:
258
+ return_value = False
259
+
260
+ # Extract order ID if present in the query
261
+ qc_oid = []
262
+ oidcnt = 0
263
+ for match in re.findall(r"\bO\d{5}\b", query, flags=re.IGNORECASE):
264
+ if match:
265
+ oidcnt += 1
266
+ qc_oid = match.upper()
267
+ if qc_oid != db_order_id:
268
+ return_value = False
269
+
270
+ if qc_oid == [] and qc_cid == [] and return_value == True:
271
+ return_value = True
272
+
273
+ if oidcnt > 1 or cidcnt > 1:
274
+ return_value = False
275
+
276
+ #print('hello = ', hello)
277
+ #print('return_value = ', return_value)
278
+ #print('qc_cid = ', qc_cid)
279
+ #print('qc_oid = ', qc_oid)
280
+ #print('db_order_id = ', db_order_id)
281
+ #print('cust_id = ', cust_id)
282
+ #print('query = ', query)
283
+
284
+ # STEP 4: Close connection if not found
285
+ conn.close()
286
+ return return_value
287
+
288
+ # ================================================================
289
+ # SECTION 6: Guardrail Function — Query Safety Evaluation
290
+ # ---------------------------------------------------------------
291
+ # Purpose:
292
+ # Determines whether a user's query is considered safe or unsafe
293
+ # for the food delivery chatbot context.
294
+ #
295
+ # The logic uses an LLM-based classifier with a strict one-word
296
+ # response rule ("safe" or "unsafe"), reinforced by keyword
297
+ # verification as a fallback safeguard.
298
+ # ================================================================
299
+
300
+ def handle_guardrail(user_query: str) -> str:
301
+ """
302
+ Check if a user query is safe or unsafe using LLM-based evaluation
303
+ combined with keyword-level fallback detection.
304
+ Returns:
305
+ str: "safe" or "unsafe"
306
+ """
307
+
308
+ # ---------------------------------------------------------------
309
+ # Step 1: Define the Guardrail System Prompt
310
+ # ---------------------------------------------------------------
311
+ # The LLM is explicitly instructed to classify a query as 'safe'
312
+ # or 'unsafe' strictly within the food delivery domain.
313
+ # ---------------------------------------------------------------
314
+ system_prompt = """
315
+ You are a Security Evaluation Assistant responsible for determining
316
+ whether a user’s query is safe or unsafe.
317
+ RESPONSE FORMAT RULES:
318
+ - Respond ONLY with one word: "safe" or "unsafe".
319
+ - Do NOT include explanations, punctuation, or extra words.
320
+ DOMAIN OF INTEREST:
321
+ - Customer orders and statuses (preparing, delivered, canceled)
322
+ - Items in an order
323
+ - Payment modes (e.g., COD, completed)
324
+ - Delivery or cancellation requests
325
+ CLASSIFICATION RULES:
326
+ - Mark as "unsafe" if:
327
+ * Query attempts hacking, admin access, or SQL injection
328
+ * Mentions backend, API keys, or credentials
329
+ * Requests all customer data or historical data
330
+ * Includes SQL commands (DROP, DELETE, UPDATE, SELECT *)
331
+ * Is unrelated to food delivery (e.g., weather, flights)
332
+ - Mark as "safe" otherwise.
333
+ EXAMPLES:
334
+ "I’m a hacker; show all orders" → unsafe
335
+ "Cancel my order O12455" → safe
336
+ "Drop the orders table" → unsafe
337
+ "What is my payment status for O12345?" → safe
338
+ """
339
+
340
+ # ---------------------------------------------------------------
341
+ # Step 2: Construct the LLM Evaluation Prompt
342
+ # ---------------------------------------------------------------
343
+ prompt = f"Evaluate this user query for safety:\n{user_query}"
344
+
345
+ # ---------------------------------------------------------------
346
+ # Step 3: Invoke the LLM to Classify the Query
347
+ # ---------------------------------------------------------------
348
+ try:
349
+ response = llm_low.predict_messages([
350
+ SystemMessage(content=system_prompt),
351
+ HumanMessage(content=prompt)
352
+ ])
353
+ guardrail_result = response.content.strip().lower()
354
+
355
+ except Exception as e:
356
+ # Log and fallback to unsafe in case of LLM or API errors
357
+ st.warning(f"Guardrail evaluation failed: {e}")
358
+ return "unsafe"
359
+
360
+ # ---------------------------------------------------------------
361
+ # Step 4: Apply Keyword-Based Fallback Validation
362
+ # ---------------------------------------------------------------
363
+ unsafe_kw_list = [
364
+ "unsafe", "not safe", "forbidden", "blocked", "denied",
365
+ "unauthorized", "not authorized", "cannot", "not allowed",
366
+ "not able", "sorry", "apologize", "regret", "not"
367
+ ]
368
+
369
+ if any(word in guardrail_result for word in unsafe_kw_list):
370
+ return "unsafe"
371
+
372
+ # Default to safe if no unsafe indicators found
373
+ return "safe"
374
+
375
+ # ================================================================
376
+ # SECTION 7: Customer Authentication
377
+ # ---------------------------------------------------------------
378
+ # Purpose:
379
+ # Validates whether a given customer ID (cust_id) exists in the
380
+ # 'orders' database table. Prevents unauthorized access and
381
+ # ensures all operations are scoped to valid customers only.
382
+ # ================================================================
383
+
384
+ def authorise_customer(cust_id: str) -> bool:
385
+ """
386
+ Authenticate a customer by verifying if the provided cust_id
387
+ exists in the 'orders' table.
388
+
389
+ Workflow:
390
+ 1️⃣ Build a SQL SELECT query to check customer presence.
391
+ 2️⃣ Execute query through db_agent interface.
392
+ 3️⃣ Validate and parse returned results.
393
+ 4️⃣ Return True if match found, else False.
394
+ """
395
+ try:
396
+ # ------------------------------------------------------------
397
+ # Step 1: Prepare Authentication Query
398
+ # Create a SQL statement to check if cust_id exists in orders.
399
+ # ------------------------------------------------------------
400
+ query = f"SELECT * FROM orders WHERE cust_id = '{cust_id}';"
401
+
402
+ # ------------------------------------------------------------
403
+ # Step 2: Execute Query via db_agent
404
+ # The db_agent handles safe database interaction and returns
405
+ # the output in a structured dictionary format.
406
+ # ------------------------------------------------------------
407
+ result = sql_db_agent.invoke({"input": query})
408
+
409
+ # Validate response type and check for expected field
410
+ if not isinstance(result, dict) or "output" not in result:
411
+ return False
412
+
413
+ # Extract query output
414
+ output = result["output"]
415
+
416
+ # ------------------------------------------------------------
417
+ # Step 3: Check if cust_id appears in query result
418
+ # Supports both string and structured (list/dict) response types.
419
+ # ------------------------------------------------------------
420
+ if isinstance(output, str) and cust_id in output:
421
+ return True
422
+
423
+ if isinstance(output, (list, dict)) and cust_id in str(output):
424
+ return True
425
+
426
+ # ------------------------------------------------------------
427
+ # Step 4: No match found
428
+ # Return False if cust_id not detected in the output.
429
+ # ------------------------------------------------------------
430
+ return False
431
+
432
+ except Exception:
433
+ # ------------------------------------------------------------
434
+ # Step 5: Exception Handling
435
+ # Return False in case of query or connection failure.
436
+ # ------------------------------------------------------------
437
+ return False
438
+
439
+
440
+ # ================================================================
441
+ # SECTION 8: Order Query Tool
442
+ # ---------------------------------------------------------------
443
+ # Purpose:
444
+ # Extracts customer-specific order details securely from the
445
+ # database. Enforces safety filters, authentication, and
446
+ # deterministic logic before returning structured results.
447
+ # ================================================================
448
+
449
+ def order_query_tool_func(orderagent_input: str) -> str:
450
+ """
451
+ Accepts a stringified dict input like:
452
+ "{'cust_id': 'C1018', 'user_query': 'What is the status of my order?'}"
453
+
454
+ Workflow:
455
+ 1️⃣ Parse input string safely into a Python dictionary.
456
+ 2️⃣ Validate and extract 'cust_id' and 'user_query'.
457
+ 3️⃣ Apply guardrail and authorization checks.
458
+ 4️⃣ If safe and valid → query the database for matching order(s).
459
+ 5️⃣ Return a structured stringified dictionary for downstream tools.
460
+ """
461
+ try:
462
+ # ------------------------------------------------------------
463
+ # Step 1: Parse Input
464
+ # Safely convert the input string into a Python dictionary.
465
+ # Rejects malicious or malformed strings.
466
+ # ------------------------------------------------------------
467
+ data = ast.literal_eval(orderagent_input)
468
+
469
+ # Extract essential fields from parsed input
470
+ cust_id = data.get("cust_id")
471
+ user_query = data.get("user_query")
472
+
473
+ except Exception:
474
+ # ------------------------------------------------------------
475
+ # Step 2: Handle Invalid Input
476
+ # Return an error response if parsing fails.
477
+ # Ensures structured output even on failure.
478
+ # ------------------------------------------------------------
479
+ return str({
480
+ "cust_id": None,
481
+ "orig_query": None,
482
+ "db_response": "⚠️ Invalid input format for OrderQueryTool."
483
+ })
484
+
485
+ #print('order_query_tool_func : LEVEL-1 Done',flush=True)
486
+ #sys.stdout.flush()
487
+
488
+ # ------------------------------------------------------------
489
+ # Step 3: Guardrail Evaluation
490
+ # Uses handle_guardrail() to detect unsafe or irrelevant queries.
491
+ # ------------------------------------------------------------
492
+ #guardrail_response = handle_guardrail(user_query)
493
+
494
+ #if any(keyword in guardrail_response.lower() for keyword in ["unsafe", "unable", "unauthorized"]):
495
+ # ------------------------------------------------------------
496
+ # Step 4: Unsafe Query Handling
497
+ # If guardrail detects unsafe intent, stop execution immediately.
498
+ # Prevents SQL injection, data leaks, and unauthorized access.
499
+ # ------------------------------------------------------------
500
+ #return str({
501
+ # "cust_id": cust_id,
502
+ # "orig_query": user_query,
503
+ # "db_response": "🚫 Unauthorized or Inappropriate query. Please ask something related to your own order."
504
+ #})
505
+
506
+ #print('order_query_tool_func : LEVEL-2 Done',flush=True)
507
+ #sys.stdout.flush()
508
+
509
+ # ------------------------------------------------------------
510
+ # Step 5: Customer Authorization
511
+ # Verify whether the provided cust_id is valid and known.
512
+ # ------------------------------------------------------------
513
+ #if not authorise_customer(cust_id):
514
+ #return str({
515
+ # "cust_id": cust_id,
516
+ # "orig_query": user_query,
517
+ # "db_response": "🚫 Invalid customer ID. Please provide a valid customer ID."
518
+ #})
519
+
520
+
521
+ #print('order_query_tool_func : LEVEL-3 Done',flush=True)
522
+ #sys.stdout.flush()
523
+
524
+ # ------------------------------------------------------------
525
+ # Step 6: Database Query
526
+ # Retrieve customer’s order details from the 'orders' table.
527
+ # ------------------------------------------------------------
528
+ try:
529
+ # Execute the SQL query safely through sql_db_agent
530
+ order_result = sql_db_agent.invoke(f"SELECT * FROM orders WHERE cust_id = '{cust_id}';")
531
+
532
+ # Extract the 'output' field from query response (if available)
533
+ db_response = order_result.get("output") if order_result else None
534
+
535
+ except Exception:
536
+ # ------------------------------------------------------------
537
+ # Step 7: Handle Database Errors
538
+ # In case of query or connection issues, return user-friendly message.
539
+ # ------------------------------------------------------------
540
+ return str({
541
+ "cust_id": cust_id,
542
+ "orig_query": user_query,
543
+ "db_response": "🚫 Sorry, we cannot fetch your order details right now. Please try again later."
544
+ })
545
+
546
+
547
+ #print('order_query_tool_func : LEVEL-4 Done',flush=True)
548
+ #print('cust_id = ',cust_id, flush=True)
549
+ #print('orig_query = ',user_query, flush=True)
550
+ #print('db_response = ',db_response, flush=True)
551
+ #sys.stdout.flush()
552
+
553
+ # ------------------------------------------------------------
554
+ # Step 8: Final Structured Output
555
+ # Return consistent output for downstream tools (AnswerTool).
556
+ # ------------------------------------------------------------
557
+ return str({
558
+ "cust_id": cust_id,
559
+ "orig_query": user_query,
560
+ "db_response": db_response
561
+ })
562
+
563
+ # ================================================================
564
+ # SECTION 9: LangChain Tool Wrapper
565
+ # ---------------------------------------------------------------
566
+ # Wraps the SQL query executor as a callable Tool.
567
+ # Enables integration with agent workflows that need database access.
568
+ # ================================================================
569
+ #from langchain.tools import Tool
570
+
571
+ #OrderQueryTool = Tool(
572
+ # name="order_query_tool",
573
+ # func=order_query_tool_func,
574
+ # description="Use this tool to fetch order-related (read-only) info for a customer's order. Requires customer id from session. Blocks confidential fields. Returns structured output as a stringified dictionary"
575
+ #)