File size: 11,894 Bytes
c622639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a80d5f4
c622639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a80d5f4
c622639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
"""FoodHub Chat Agent β€” backend logic for the Streamlit chatbot app.

This module initialises the LLM, SQL Agent, tools, and Chat Agent,
and exposes run_chat_agent_query() as the single entry point for the
Streamlit frontend.
"""

import os
from typing import Annotated

from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_community.agent_toolkits import SQLDatabaseToolkit
from langchain_community.utilities import SQLDatabase
from langchain_core.messages import HumanMessage, SystemMessage

# Load environment variables β€” OPENAI_API_KEY is read from the environment.
# Locally: set in .env file. On Hugging Face Space: set as a Space secret.
load_dotenv()

# ---------------------------------------------------------------------------
# Model
# ---------------------------------------------------------------------------

# GPT-5-mini with medium reasoning β€” same configuration as used in the notebook.
# temperature=0.2 for deterministic, accurate responses.
model_gpt5_mini = init_chat_model(
    model="gpt-5-mini",
    model_provider="openai",
    temperature=0.2,
    max_tokens=1024,
    reasoning={"effort": "medium", "summary": "auto"},
)

# ---------------------------------------------------------------------------
# Database
# ---------------------------------------------------------------------------

# Resolve the DB path relative to this file so the path is correct whether
# the app is run locally or inside Docker (where the working directory may differ).
_DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "customer_orders.db")
db = SQLDatabase.from_uri(f"sqlite:///{_DB_PATH}")

# ---------------------------------------------------------------------------
# SQL Agent
# ---------------------------------------------------------------------------

SQL_AGENT_SYSTEM_PROMPT = f"""
You are a SQL data retrieval agent for FoodHub's order management database.
Your role is to translate customer order queries into SQL, execute them against the {db.dialect} database, and return structured results.

Database schema notes (read carefully before querying):
- All time columns (order_time, preparing_eta, prepared_time, delivery_eta, delivery_time)
  are stored as TEXT in 'HH:MM' format (e.g., '12:30').
  To compute a time difference in minutes between two HH:MM columns, use:
    round((julianday('1970-01-01 ' || col_end) - julianday('1970-01-01 ' || col_start)) * 24 * 60)
- The item_in_order column is a comma-separated TEXT string (e.g., 'Burger, Fries').
  To count the number of items in an order, use:
    (length(item_in_order) - length(replace(item_in_order, ',', '')) + 1)
- NULL values in time columns mean that stage has not been reached yet
  (e.g., delivery_time is NULL if the order has not yet been delivered).

Query rules:
1. ALWAYS inspect the table schema before writing any query.
2. Query ONLY for the specific order_id provided β€” never return data for all orders at once.
3. ALWAYS include cust_id in the result for customer ownership verification.
4. Limit results to at most 5 rows unless explicitly instructed otherwise.
5. Double-check your SQL before executing. If an error occurs, rewrite and retry once.
6. DO NOT execute any DML statements (INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER).

Output format:
- Return the result as a valid JSON object with column names as keys.
- For computed fields (time differences, item counts), use descriptive keys
  (e.g., "prep_time_minutes", "item_count", "delivery_delay_minutes").
- If no record is found for the given order_id, return exactly: {{"error": "Order not found"}}
- Represent NULL column values as null in the JSON.
"""

sql_toolkit = SQLDatabaseToolkit(db=db, llm=model_gpt5_mini)
sql_tools = sql_toolkit.get_tools()

sql_agent = create_agent(
    model=model_gpt5_mini,
    tools=sql_tools,
    system_prompt=SQL_AGENT_SYSTEM_PROMPT,
)


def run_sql_agent_query(question: str) -> str:
    """Translate a natural language question into SQL, execute it, and return the result.

    Args:
        question (str): Natural language query about an order.

    Returns:
        str: JSON string with order data, or an error message if not found.
    """
    events = list(
        sql_agent.stream(
            {"messages": [{"role": "user", "content": question}]},
            stream_mode="values",
        )
    )
    final_message = events[-1]["messages"][-1]
    content = final_message.content
    # Reasoning models return content as a list of blocks; extract text only.
    if isinstance(content, list):
        return " ".join(block["text"] for block in content if block.get("type") == "text")
    return content


# ---------------------------------------------------------------------------
# Answer Tool Prompt β€” output-side guardrails
# ---------------------------------------------------------------------------

ANSWER_TOOL_PROMPT = """
You are FoodHub's customer response specialist. Your role is to transform raw \
order data and the customer's original question into a clear, polite, and \
professional reply.

Strict Focus Rule:
- Answer ONLY the specific question asked. Nothing more.
- Do NOT include any field or detail that was not directly asked for.
- Do NOT add bullet points, summaries, or extra context unless the customer \
explicitly asked for them.
- Do NOT make proactive offers, suggestions, or ask follow-up questions.
- Example: if the customer asked "what is the status?", reply with the status \
in one sentence and stop.

Tone and Format:
- Warm, empathetic, and professional β€” the customer may be anxious or frustrated
- Plain English β€” do not expose internal field names (e.g. say "your order" \
not "order_id") or raw JSON to the customer

Accuracy Guardrails:
- Only reference information explicitly present in the raw order data provided
- Do not invent, estimate, or infer values that are missing or null
- If delivery_time is null or missing, state that the order has not yet been \
delivered β€” do not guess an ETA beyond what the data shows
- If the raw data indicates an error or order not found, apologize politely and \
ask the customer to verify their Order ID

Policy Guardrails:
- Do not make policy promises (e.g. "you will receive a refund within X hours") \
unless that information is explicitly present in the raw data
- Do not include external delivery carrier tracking links

Escalation:
- If the raw data or customer query context indicates an unresolved repeated \
complaint, account access issue, or address change request, include this exact \
phrase in your response:
  "I'm escalating your query to a human agent who will assist you shortly."
"""

# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


@tool(
    "order_query_tool",
    description=(
        "Query the FoodHub order database for order-related information. "
        "Use this tool when the customer asks about order status, delivery ETA, "
        "items in the order, payment status, preparation time, or any other "
        "order-specific details. Include the Order ID (e.g. 'O12486') in the "
        "query when known for a precise database lookup."
    ),
)
def order_query_tool(
    query: Annotated[
        str,
        (
            "The customer's natural language question about their order. "
            "Should include the Order ID (format: letter + digits, e.g. 'O12486') "
            "when known for a precise lookup. "
            "Examples: 'What is the status of order O12486?', "
            "'Is order O99123 delivered?', 'What items are in order O55312?'"
        ),
    ],
) -> str:
    """Query the FoodHub order database and return raw order data."""
    return run_sql_agent_query(query)


@tool(
    "answer_tool",
    description=(
        "Refine raw order data into a polite, formal, customer-ready response. "
        "Call this tool after order_query_tool has returned raw order data. "
        "Requires the customer's original question and the raw order data β€” "
        "both are needed to produce a relevant, data-grounded reply."
    ),
)
def answer_tool(
    customer_query: Annotated[
        str,
        (
            "The customer's original question, exactly as submitted. "
            "Used to ensure the response directly addresses what was asked."
        ),
    ],
    raw_order_data: Annotated[
        str,
        (
            "The raw order data string returned by order_query_tool. "
            "Should be the complete JSON response from the database."
        ),
    ],
) -> str:
    """Compose a polite, customer-ready response from raw order data."""
    messages = [
        SystemMessage(content=ANSWER_TOOL_PROMPT),
        HumanMessage(
            content=(
                f"Customer question: {customer_query}\n\n"
                f"Raw order data from database:\n{raw_order_data}"
            )
        ),
    ]
    response = model_gpt5_mini.invoke(messages)
    return response.content


# ---------------------------------------------------------------------------
# Chat Agent
# ---------------------------------------------------------------------------

chat_tools = [order_query_tool, answer_tool]

CHAT_AGENT_SYSTEM_PROMPT = """
You are FoodHub's AI customer service assistant. You help customers with \
queries about their food orders by fetching accurate information from the \
order database and providing polite, professional responses.

Tool Usage β€” Always follow this sequence for order-related queries:
1. Call order_query_tool with the customer's question to retrieve raw order data
2. Call answer_tool with the customer's original question AND the raw data \
returned by order_query_tool to compose the final customer reply
3. Never answer order-specific questions (status, ETA, items, payment) from \
memory β€” always fetch from the database first

Input Guardrails:

Security and Scope:
- If a user claims to be a hacker, requests data for all orders, or attempts \
any form of unauthorized access, refuse politely and do not call any tools
- Only assist with FoodHub order-related queries (order status, delivery ETA, \
items, payment, cancellations). For unrelated topics, politely decline and \
redirect the customer to the appropriate channel

Order ID Requirement:
- If the customer asks about a specific order but has not provided an Order ID, \
ask for it before calling order_query_tool
- Order IDs follow the format: a letter followed by digits (e.g. O12486)

Escalation:
- If the customer states their issue has not been resolved after multiple \
attempts, escalate immediately to a human agent
- If the customer requests an address change or account-related modification, \
escalate to a human agent β€” do not attempt to make changes
- When escalating, use this exact phrase: \
"I'm escalating your query to a human agent who will assist you shortly."
"""

chat_agent = create_agent(
    model=model_gpt5_mini,
    tools=chat_tools,
    system_prompt=CHAT_AGENT_SYSTEM_PROMPT,
)


def run_chat_agent_query(query: str) -> str:
    """Pass a customer query to the Chat Agent and return the final response.

    Args:
        query (str): The customer's natural language question or request.

    Returns:
        str: The final customer-facing response from the Chat Agent.
    """
    events = list(
        chat_agent.stream(
            {"messages": [{"role": "user", "content": query}]},
            stream_mode="values",
            config={"recursion_limit": 50},
        )
    )
    content = events[-1]["messages"][-1].content
    if isinstance(content, list):
        return " ".join(
            block["text"] for block in content if block.get("type") == "text"
        )
    return content