From a072d8afb9afc842fc53030dd49f1f014725267b Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Tue, 16 Sep 2025 14:47:54 -0300 Subject: [PATCH 1/3] fix: handle empty model responses --- backend/apps/chatbot/agent/react_agent.py | 9 +++++++++ backend/apps/chatbot/utils/stream.py | 23 ++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/apps/chatbot/agent/react_agent.py b/backend/apps/chatbot/agent/react_agent.py index cedc333b..1d97df9b 100644 --- a/backend/apps/chatbot/agent/react_agent.py +++ b/backend/apps/chatbot/agent/react_agent.py @@ -13,6 +13,7 @@ from langgraph.graph.message import add_messages from langgraph.managed import IsLastStep, RemainingSteps from langgraph.prebuilt import ToolNode +from loguru import logger from chatbot.agents.utils import async_delete_checkpoints, delete_checkpoints @@ -75,6 +76,10 @@ def _call_model(self, state: State, config: RunnableConfig) -> dict[str, list[Ba response: AIMessage = self.model_runnable.invoke(messages, config) + if not response.content and not response.tool_calls: + logger.warning("Received empty response from model, skipping message list update") + return {"messages": []} + if is_last_step and response.tool_calls or remaining_steps < 2 and response.tool_calls: return { "messages": [ @@ -108,6 +113,10 @@ async def _acall_model( response: AIMessage = await self.model_runnable.ainvoke(messages, config) + if not response.content and not response.tool_calls: + logger.warning("Received empty response from model, skipping message list update") + return {"messages": []} + if is_last_step and response.tool_calls or remaining_steps < 2 and response.tool_calls: return { "messages": [ diff --git a/backend/apps/chatbot/utils/stream.py b/backend/apps/chatbot/utils/stream.py index a691f2d1..9acad7c2 100644 --- a/backend/apps/chatbot/utils/stream.py +++ b/backend/apps/chatbot/utils/stream.py @@ -146,7 +146,16 @@ def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None: - None for ignored chunks """ if "agent" in chunk: - message: AIMessage = chunk["agent"]["messages"][0] + ai_messages: list[AIMessage] = chunk["agent"]["messages"] + + # If no messages are returned, the model returned an empty response + # with no tool calls. This also counts as a final (but empty) answer. + if not ai_messages: + event_type = "final_answer" + event_data = EventData(content="") + return StreamEvent(type=event_type, data=event_data) + + message = ai_messages[0] if message.tool_calls: tool_calls = [ @@ -161,12 +170,12 @@ def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None: return StreamEvent(type=event_type, data=event_data) elif "tools" in chunk: - messages: list[ToolMessage] = chunk["tools"]["messages"] + tool_messages: list[ToolMessage] = chunk["tools"]["messages"] tool_outputs = [] - for msg in messages: - content, truncated = _truncate_content(msg.content, MAX_BYTES) + for message in tool_messages: + content, truncated = _truncate_content(message.content, MAX_BYTES) if truncated: metadata = {"truncated": True} @@ -175,9 +184,9 @@ def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None: tool_outputs.append( ToolOutput( - status=msg.status, - tool_call_id=msg.tool_call_id, - tool_name=msg.name, + status=message.status, + tool_call_id=message.tool_call_id, + tool_name=message.name, output=content, metadata=metadata, ) From 0c0feb58c03db210dc97cf187e422123979cd4e5 Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Tue, 16 Sep 2025 16:42:54 -0300 Subject: [PATCH 2/3] fix: dict truncation --- backend/apps/chatbot/utils/stream.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/apps/chatbot/utils/stream.py b/backend/apps/chatbot/utils/stream.py index 9acad7c2..1ce39426 100644 --- a/backend/apps/chatbot/utils/stream.py +++ b/backend/apps/chatbot/utils/stream.py @@ -85,15 +85,15 @@ def _truncate_dict(data: dict, max_bytes: int) -> dict: Returns: dict: Truncated dictionary that fits within the byte limit. """ - items = data.items() + items = list(data.items()) left, right = 0, len(items) best_size = 0 while left <= right: mid = (left + right) // 2 - test_dict = dict(items[:mid]) - size = len(json.dumps(test_dict, ensure_ascii=False, indent=2).encode("utf-8")) + sub_dict = dict(items[:mid]) + size = len(json.dumps(sub_dict, ensure_ascii=False, indent=2).encode("utf-8")) if size <= max_bytes: best_size = mid @@ -119,8 +119,8 @@ def _truncate_list(data: list, max_bytes: int) -> list: while left <= right: mid = (left + right) // 2 - test_list = data[:mid] - size = len(json.dumps(test_list, ensure_ascii=False, indent=2).encode("utf-8")) + sub_list = data[:mid] + size = len(json.dumps(sub_list, ensure_ascii=False, indent=2).encode("utf-8")) if size <= max_bytes: best_size = mid From bd07fffabb45593d1d64aab64e1b34f9b89ef8a1 Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Tue, 16 Sep 2025 17:11:00 -0300 Subject: [PATCH 3/3] minor code formatting changes --- backend/apps/chatbot/utils/stream.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/apps/chatbot/utils/stream.py b/backend/apps/chatbot/utils/stream.py index 1ce39426..ce8bca26 100644 --- a/backend/apps/chatbot/utils/stream.py +++ b/backend/apps/chatbot/utils/stream.py @@ -151,9 +151,7 @@ def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None: # If no messages are returned, the model returned an empty response # with no tool calls. This also counts as a final (but empty) answer. if not ai_messages: - event_type = "final_answer" - event_data = EventData(content="") - return StreamEvent(type=event_type, data=event_data) + return StreamEvent(type="final_answer", data=EventData(content="")) message = ai_messages[0]