Skip to content

Commit ba039fd

Browse files
authored
fix: Make human-in-the-loop more consistent between graph state and chat history (#550)
## Summary This PR addresses inconsistencies between the UI display and the agent's internal LangGraph state during the ticket booking process. Previously, hardcoded messages spread across the UI, the backend request handlers, and agent nodes, led to a disjointed UX where chat history, user prompts, and agent responses could become out of sync, particularly after a page refresh. The core of this change is the introduction of a single, internal helper function (__booking_handler) that centralizes the logic for both confirming and declining a ticket booking. This refactoring ensures that all UI components and chat history entries are a direct reflection of the agent's state. ## Changes * Created a common `__booking_handler` to manage both booking "accept" and "decline" actions, eliminating code duplication and streamlining maintenance. * Replaced hardcoded UI text for confirmations and responses with messages sourced directly from the agent's graph state. This guarantees consistency between what the user sees and the agent's conversational history. * The user's choice (e.g., `"Looks good to me. Book it!"`) and the agent's final response are now reliably added to the user session's chat history for both success and decline scenarios. * Added a crucial comment explaining the rationale for injecting the "decline" message to the langgraph state *before* invoking the agent. * Removed unnecessary `params` from the request handlers for better code hygiene.
1 parent e88a04f commit ba039fd

File tree

4 files changed

+62
-41
lines changed

4 files changed

+62
-41
lines changed

agent/agent.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,23 @@ def __init__(self):
5151
def user_session_exist(self, uuid: str) -> bool:
5252
return uuid in self._user_sessions
5353

54-
async def user_session_insert_ticket(self, uuid: str, params: str) -> Any:
55-
response = await self.user_session_invoke(uuid, None)
56-
return "ticket booking success"
54+
async def user_session_insert_ticket(self, uuid: str) -> Any:
55+
return await self.user_session_invoke(uuid, None)
5756

5857
async def user_session_decline_ticket(self, uuid: str) -> dict[str, Any]:
5958
config = self.get_config(uuid)
59+
60+
# The user has declined the pending ticket booking. We inject a
61+
# synthetic HumanMessage to simulate the user explicitly canceling. This
62+
# state update causes langgraph to re-run its conditional logic. Instead
63+
# of proceeding with the interrupted tool call, the graph routes to the
64+
# main agent, which formulates a natural response to the user's
65+
# "cancellation".
6066
human_message = HumanMessage(
6167
content="I changed my mind. Decline ticket booking."
6268
)
6369
self._langgraph_app.update_state(config, {"messages": [human_message]})
64-
response = await self.user_session_invoke(uuid, None)
65-
return response
70+
return await self.user_session_invoke(uuid, None)
6671

6772
async def user_session_create(self, session: dict[str, Any]):
6873
"""Create and load an agent executor with tools and LLM."""

agent/react_graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ async def insert_ticket_node(state: UserState):
166166
tool_call = last_message.tool_calls[0]
167167
tool_args = tool_call.get("args")
168168
output = await insert_ticket.ainvoke(tool_args)
169-
human_message = HumanMessage(content="Looks good to me.")
169+
human_message = HumanMessage(content="Looks good to me. Book it!")
170170
ai_message = AIMessage(
171171
content=(
172172
"Your flight has been successfully booked."

app.py

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -155,56 +155,71 @@ async def chat_handler(request: Request, prompt: str = Body(embed=True)):
155155
output = response.get("output")
156156
confirmation = response.get("confirmation")
157157
trace = response.get("trace")
158+
request.session["history"].append({"type": "ai", "data": {"content": output}})
158159
# Return assistant response
159160
if confirmation:
160161
return json.dumps(
161-
{"type": "confirmation", "content": confirmation, "trace": trace}
162+
{
163+
"type": "confirmation",
164+
"content": {"output": output, **confirmation},
165+
"trace": trace,
166+
}
162167
)
163168
else:
164-
request.session["history"].append({"type": "ai", "data": {"content": output}})
165169
return json.dumps(
166-
{"type": "message", "content": markdown(output), "trace": trace}
170+
{
171+
"type": "message",
172+
"content": markdown(output),
173+
"trace": trace,
174+
}
167175
)
168176

169177

170-
@routes.post("/book/flight", response_class=PlainTextResponse)
171-
async def book_flight(request: Request, params: str = Body(embed=True)):
172-
"""Handler for LangChain chat requests"""
173-
# Retrieve the params for the booking
174-
if not params:
175-
raise HTTPException(status_code=400, detail="Error: No booking params")
178+
async def __booking_handler(request: Request, is_confirmed: bool):
179+
"""Common booking handler for flight confirmation and decline"""
180+
# Note in the history, that the ticket was not booked.
181+
# This is helpful in case of reloads so there doesn't seem to be a break in
182+
# communication.
176183
if "uuid" not in request.session:
177184
raise HTTPException(
178185
status_code=400, detail="Error: Invoke index handler before start chatting"
179186
)
180187
agent = request.app.state.agent
181-
response = await agent.user_session_insert_ticket(request.session["uuid"], params)
182-
# Note in the history, that the ticket has been successfully booked
183-
request.session["history"].append(
184-
{"type": "ai", "data": {"content": "Your flight has been successfully booked."}}
188+
uuid = request.session["uuid"]
189+
190+
# Determine the correct agent action and user message based on confirmation
191+
# status.
192+
if is_confirmed:
193+
action = agent.user_session_insert_ticket(uuid)
194+
content = "Looks good to me. Book it!"
195+
else:
196+
action = agent.user_session_decline_ticket(uuid)
197+
content = "I changed my mind. Decline ticket booking."
198+
199+
response = await action
200+
output = response.get("output")
201+
request.session["history"].extend(
202+
[
203+
{
204+
"type": "human",
205+
"data": {"content": content},
206+
},
207+
{"type": "ai", "data": {"content": output}},
208+
]
185209
)
186-
return response
210+
return output
211+
212+
213+
@routes.post("/book/flight", response_class=PlainTextResponse)
214+
async def book_flight(request: Request):
215+
"""Handler for booking confirmation requests"""
216+
return await __booking_handler(request, is_confirmed=True)
187217

188218

189219
@routes.post("/book/flight/decline", response_class=PlainTextResponse)
190220
async def decline_flight(request: Request):
191-
"""Handler for LangChain chat requests"""
192-
# Note in the history, that the ticket was not booked
193-
# This is helpful in case of reloads so there doesn't seem to be a break in communication.
194-
agent = request.app.state.agent
195-
response = await agent.user_session_decline_ticket(request.session["uuid"])
196-
response = response["output"]
197-
request.session["history"].append(
198-
{"type": "ai", "data": {"content": "Please confirm if you would like to book."}}
199-
)
200-
request.session["history"].append(
201-
{
202-
"type": "human",
203-
"data": {"content": "I changed my mind. Decline ticket booking."},
204-
}
205-
)
206-
request.session["history"].append({"type": "ai", "data": {"content": response}})
207-
return response
221+
"""Handler for booking decline requests"""
222+
return await __booking_handler(request, is_confirmed=False)
208223

209224

210225
@routes.post("/reset")

static/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ function logMessage(name, msg, trace) {
156156
function buildConfirmation(confirmation, messageId) {
157157
if (["Insert Ticket","insert_ticket"].includes(confirmation.tool)) {
158158
const params = confirmation.params;
159+
const output = confirmation.output;
159160
const message_id = messageId;
160161
confirmations[message_id] = params
161162
const from = params.departure_airport;
@@ -169,7 +170,7 @@ function buildConfirmation(confirmation, messageId) {
169170
const message = `<div class="chat-bubble ai" id="${message_id}">
170171
<div class="sender-icon"><img src="static/logo.png"></div>
171172
<div class="ticket-confirmation">
172-
Please confirm the details below to complete your booking
173+
${output}
173174
<div class="ticket-header"></div>
174175
<div class="ticket">
175176
<div class="from">${from}</div>
@@ -181,7 +182,7 @@ function buildConfirmation(confirmation, messageId) {
181182
${buildBox('left', 205, 35, 15, "Flight", flight)}
182183
${buildBox('left', 265, 35, 15, "Passenger", userName, "")}
183184
${buildButton("Looks good to me. Book it!", 342, "#805e9d", "#FFF", "confirmTicket" + message_id)}
184-
${buildButton("I changed my mind.", 395, "#f8f8f8", "#181a23", "cancelTicket" + message_id)}
185+
${buildButton("I changed my mind. Decline ticket booking.", 395, "#f8f8f8", "#181a23", "cancelTicket" + message_id)}
185186
</div></div>`;
186187
$('.inner-content').append(message);
187188
$('.chat-content').scrollTop($('.chat-content').prop("scrollHeight"));
@@ -262,7 +263,7 @@ async function cancelTicket(id) {
262263
'Content-Type': 'application/json'
263264
}
264265
});
265-
logMessage("human", "I changed my mind.")
266+
logMessage("human", "I changed my mind. Decline ticket booking.")
266267
removeTicketChoices(id);
267268

268269
if (response.ok) {
@@ -274,7 +275,7 @@ async function cancelTicket(id) {
274275
}
275276

276277
async function confirmTicket(id) {
277-
logMessage("human", "Looks good to me.")
278+
logMessage("human", "Looks good to me. Book it!")
278279
const params = JSON.stringify(confirmations[id]);
279280
const response = await fetch('book/flight', {
280281
method: 'POST',

0 commit comments

Comments
 (0)