mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 07:15:10 +00:00
fix: after_llm_call hooks no longer break native tool execution (#6531)
* fix: don't clobber native tool-call responses in after-LLM hooks Registering any `after_llm_call` hook broke native tool execution: the executor invokes `_setup_after_llm_call_hooks` on the intermediate response that carries the model's tool calls, the non-str payload was stringified for the response-rewrite pass, and the executor then treated that string as a final answer instead of executing the tools. Structured payloads (neither `str` nor `BaseModel`) now pass through untouched, mirroring the isinstance guard `_invoke_after_llm_call_hooks` already applies on the direct-call path; hooks still fire on the follow-up textual response. Fixes #6529 * style: shorten the tool-call guard comment
This commit is contained in:
@@ -1751,6 +1751,12 @@ def _setup_after_llm_call_hooks(
|
||||
if executor_context and executor_context.after_llm_call_hooks:
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
|
||||
# Don't stringify structured tool-call payloads: the executor would
|
||||
# treat the result as a final answer and skip tool execution (#6529).
|
||||
# Hooks still run on the follow-up textual response.
|
||||
if not isinstance(answer, (str, BaseModel)):
|
||||
return answer
|
||||
|
||||
original_messages = executor_context.messages
|
||||
|
||||
if isinstance(answer, BaseModel):
|
||||
|
||||
@@ -272,6 +272,40 @@ class TestLLMHooksIntegration:
|
||||
|
||||
assert result == "Original [hook1] [hook2]"
|
||||
|
||||
def test_after_hooks_do_not_clobber_native_tool_call_responses(
|
||||
self, mock_executor
|
||||
):
|
||||
"""A registered after hook must not break native tool execution.
|
||||
|
||||
Regression for crewAIInc/crewAI#6529: `_setup_after_llm_call_hooks`
|
||||
stringified structured tool-call payloads, so the executor treated the
|
||||
raw tool call as the final answer and never executed the tool. Non-str,
|
||||
non-BaseModel responses now pass through untouched; hooks still fire on
|
||||
textual responses.
|
||||
"""
|
||||
from crewai.utilities.agent_utils import _setup_after_llm_call_hooks
|
||||
|
||||
observed = []
|
||||
|
||||
def observer(context):
|
||||
observed.append(context.response)
|
||||
return None
|
||||
|
||||
register_after_llm_call_hook(observer)
|
||||
mock_executor.after_llm_call_hooks = get_after_llm_call_hooks()
|
||||
|
||||
tool_calls = [Mock()] # structured native tool-call payload
|
||||
result = _setup_after_llm_call_hooks(
|
||||
mock_executor, tool_calls, printer=Mock(), verbose=False
|
||||
)
|
||||
assert result is tool_calls
|
||||
|
||||
text = _setup_after_llm_call_hooks(
|
||||
mock_executor, "final answer", printer=Mock(), verbose=False
|
||||
)
|
||||
assert text == "final answer"
|
||||
assert observed == ["final answer"]
|
||||
|
||||
def test_unregister_before_hook(self):
|
||||
"""Test that before hooks can be unregistered."""
|
||||
def test_hook(context):
|
||||
|
||||
Reference in New Issue
Block a user