Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions lib/crewai/src/crewai/agents/crew_agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,20 @@ def _invoke_loop(self) -> AgentFinish:
)

self._invoke_step_callback(formatted_answer) # type: ignore[arg-type]
self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined]

# Properly attribute messages to avoid LLM hallucination of observations:
# - LLM's response goes as assistant message
# - Tool observation goes as user message (not assistant)
if isinstance(formatted_answer, AgentAction) and formatted_answer.llm_response:
# For tool use: append LLM response as assistant, observation as user
self._append_message(formatted_answer.llm_response)
if formatted_answer.result:
self._append_message(
f"Observation: {formatted_answer.result}", role="user"
)
else:
# For final answer or other cases: append text as assistant
self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined]

except OutputParserError as e:
formatted_answer = handle_output_parser_exception( # type: ignore[assignment]
Expand Down Expand Up @@ -431,7 +444,20 @@ async def _ainvoke_loop(self) -> AgentFinish:
)

self._invoke_step_callback(formatted_answer) # type: ignore[arg-type]
self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined]

# Properly attribute messages to avoid LLM hallucination of observations:
# - LLM's response goes as assistant message
# - Tool observation goes as user message (not assistant)
if isinstance(formatted_answer, AgentAction) and formatted_answer.llm_response:
# For tool use: append LLM response as assistant, observation as user
self._append_message(formatted_answer.llm_response)
if formatted_answer.result:
self._append_message(
f"Observation: {formatted_answer.result}", role="user"
)
else:
# For final answer or other cases: append text as assistant
self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined]

except OutputParserError as e:
formatted_answer = handle_output_parser_exception( # type: ignore[assignment]
Expand Down Expand Up @@ -481,13 +507,18 @@ def _handle_agent_action(
Updated action or final answer.
"""
# Special case for add_image_tool
# Note: Even for add_image_tool, we should not attribute tool output to assistant
# to avoid LLM hallucination. The LLM's action is stored as assistant message,
# and the tool result (image) is stored as user message.
add_image_tool = self._i18n.tools("add_image")
if (
isinstance(add_image_tool, dict)
and formatted_answer.tool.casefold().strip()
== add_image_tool.get("name", "").casefold().strip()
):
self.messages.append({"role": "assistant", "content": tool_result.result})
# Store original LLM response for proper message attribution
formatted_answer.llm_response = formatted_answer.text
formatted_answer.result = tool_result.result
return formatted_answer

return handle_agent_action_core(
Expand Down
1 change: 1 addition & 0 deletions lib/crewai/src/crewai/agents/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class AgentAction:
tool_input: str
text: str
result: str | None = None
llm_response: str | None = None # Original LLM response before observation appended


@dataclass
Expand Down
24 changes: 19 additions & 5 deletions lib/crewai/src/crewai/experimental/crew_agent_executor_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,18 @@ def execute_tool_action(self) -> Literal["tool_completed", "tool_result_is_final
# Invoke step callback if configured
self._invoke_step_callback(result)

# Append result message to conversation state
if hasattr(result, "text"):
# Properly attribute messages to avoid LLM hallucination of observations:
# - LLM's response goes as assistant message
# - Tool observation goes as user message (not assistant)
if isinstance(result, AgentAction) and result.llm_response:
# For tool use: append LLM response as assistant, observation as user
self._append_message_to_state(result.llm_response)
if result.result:
self._append_message_to_state(
f"Observation: {result.result}", role="user"
)
elif hasattr(result, "text"):
# For final answer or other cases: append text as assistant
self._append_message_to_state(result.text)

# Check if tool result became a final answer (result_as_answer flag)
Expand Down Expand Up @@ -537,15 +547,19 @@ def _handle_agent_action(
Returns:
Updated action or final answer.
"""
# Special case for add_image_tool
# Note: Even for add_image_tool, we should not attribute tool output to assistant
# to avoid LLM hallucination. The LLM's action is stored as assistant message,
# and the tool result (image) is stored as user message.
add_image_tool = self._i18n.tools("add_image")
if (
isinstance(add_image_tool, dict)
and formatted_answer.tool.casefold().strip()
== add_image_tool.get("name", "").casefold().strip()
):
self.state.messages.append(
{"role": "assistant", "content": tool_result.result}
)
# Store original LLM response for proper message attribution
formatted_answer.llm_response = formatted_answer.text
formatted_answer.result = tool_result.result
return formatted_answer

return handle_agent_action_core(
Expand Down
14 changes: 13 additions & 1 deletion lib/crewai/src/crewai/lite_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,19 @@ def _invoke_loop(self) -> AgentFinish:
show_logs=self._show_logs,
)

self._append_message(formatted_answer.text, role="assistant")
# Properly attribute messages to avoid LLM hallucination of observations:
# - LLM's response goes as assistant message
# - Tool observation goes as user message (not assistant)
if isinstance(formatted_answer, AgentAction) and formatted_answer.llm_response:
# For tool use: append LLM response as assistant, observation as user
self._append_message(formatted_answer.llm_response, role="assistant")
if formatted_answer.result:
self._append_message(
f"Observation: {formatted_answer.result}", role="user"
)
else:
# For final answer or other cases: append text as assistant
self._append_message(formatted_answer.text, role="assistant")
except OutputParserError as e: # noqa: PERF203
self._printer.print(
content="Failed to parse LLM output. Retrying...",
Expand Down
11 changes: 11 additions & 0 deletions lib/crewai/src/crewai/utilities/agent_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,21 @@ def handle_agent_action_core(

Notes:
- TODO: Remove messages parameter and its usage.
- The observation is appended to formatted_answer.text for logging/trace
purposes, but callers should use formatted_answer.llm_response for the
assistant message (without observation) and append the observation
separately as a user message to avoid LLM hallucination of observations.
"""
if step_callback:
step_callback(tool_result)

# Store the original LLM response before appending observation
# This is used by executors to correctly attribute messages:
# - llm_response goes as assistant message
# - observation goes as user message (to prevent LLM hallucination)
formatted_answer.llm_response = formatted_answer.text

# Append observation to text for logging/trace purposes
formatted_answer.text += f"\nObservation: {tool_result.result}"
formatted_answer.result = tool_result.result

Expand Down
Loading
Loading