mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-02 13:48:09 +00:00
refactor: streamline LiteAgent tool handling and enhance after-LLM call hooks
- Simplified the conversion of tools to OpenAI schema by removing redundant mapping. - Updated the after-LLM call hooks to support list-type answers, ensuring native tool calls are returned unchanged. - Added tests to verify correct usage count for native tools and ensure proper handling of duplicate tool names. - Enhanced existing tests to confirm functionality with after-LLM hooks active, addressing previous issues with tool call processing.
This commit is contained in:
@@ -914,14 +914,10 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
Returns:
|
||||
AgentFinish: The final result of the agent execution.
|
||||
"""
|
||||
openai_tools, available_functions, _ = convert_tools_to_openai_schema(
|
||||
self.tools
|
||||
openai_tools, available_functions, original_tools_by_name = (
|
||||
convert_tools_to_openai_schema(self.tools)
|
||||
)
|
||||
|
||||
original_tools_by_name: dict[str, BaseTool] = {
|
||||
sanitize_tool_name(t.name): t for t in self.tools
|
||||
}
|
||||
|
||||
while True:
|
||||
try:
|
||||
if has_reached_max_iterations(self._iterations, self.max_iterations):
|
||||
@@ -1348,9 +1344,6 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
color="red",
|
||||
)
|
||||
|
||||
if original_tool:
|
||||
original_tool.current_usage_count += 1
|
||||
|
||||
if not error_event_emitted:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
|
||||
@@ -1245,26 +1245,34 @@ def _setup_before_llm_call_hooks(
|
||||
|
||||
def _setup_after_llm_call_hooks(
|
||||
executor_context: CrewAgentExecutor | AgentExecutor | LiteAgent | None,
|
||||
answer: str | BaseModel,
|
||||
answer: str | BaseModel | list[Any],
|
||||
printer: Printer,
|
||||
verbose: bool = True,
|
||||
) -> str | BaseModel:
|
||||
) -> str | BaseModel | list[Any]:
|
||||
"""Setup and invoke after_llm_call hooks for the executor context.
|
||||
|
||||
Args:
|
||||
executor_context: The executor context to setup the hooks for.
|
||||
answer: The LLM response (string or Pydantic model).
|
||||
answer: The LLM response (string, Pydantic model, or list of native
|
||||
tool calls).
|
||||
printer: Printer instance for error logging.
|
||||
verbose: Whether to print output.
|
||||
|
||||
Returns:
|
||||
The potentially modified response (string or Pydantic model).
|
||||
The potentially modified response. List-type answers (native tool
|
||||
calls) are always returned unchanged so that callers can rely on
|
||||
``isinstance(answer, list)`` checks.
|
||||
"""
|
||||
if executor_context and executor_context.after_llm_call_hooks:
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
|
||||
original_messages = executor_context.messages
|
||||
|
||||
# Native tool-call lists must survive hooks unchanged. We provide a
|
||||
# stringified representation to hook context for observability but
|
||||
# always return the original list so callers can detect tool calls.
|
||||
is_tool_call_list = isinstance(answer, list)
|
||||
|
||||
# For Pydantic models, serialize to JSON for hooks
|
||||
if isinstance(answer, BaseModel):
|
||||
pydantic_answer = answer
|
||||
@@ -1303,6 +1311,9 @@ def _setup_after_llm_call_hooks(
|
||||
else:
|
||||
executor_context.messages = []
|
||||
|
||||
if is_tool_call_list:
|
||||
return answer
|
||||
|
||||
# If hooks modified the response, update answer accordingly
|
||||
if pydantic_answer is not None:
|
||||
# For Pydantic models, reparse the JSON if it was modified
|
||||
|
||||
@@ -1343,3 +1343,132 @@ def test_lite_agent_native_parallel_tool_calls():
|
||||
]
|
||||
assert len(assistant_tc_messages) == 1
|
||||
assert len(assistant_tc_messages[0]["tool_calls"]) == 2
|
||||
|
||||
|
||||
def test_lite_agent_native_tool_usage_count_no_double_increment():
|
||||
"""current_usage_count must increment exactly once per native tool call.
|
||||
|
||||
BaseTool.run() already increments the counter internally, so the native
|
||||
tool call handler must not add a second increment.
|
||||
"""
|
||||
tool_call = [_make_openai_tool_call("call_1", "calculate", '{"expression": "1+1"}')]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_call], final_answer="2")
|
||||
calc_tool = CalculatorTool()
|
||||
assert calc_tool.current_usage_count == 0
|
||||
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[calc_tool],
|
||||
)
|
||||
agent.kickoff("What is 1+1?")
|
||||
|
||||
assert calc_tool.current_usage_count == 1
|
||||
|
||||
|
||||
def test_lite_agent_native_tool_max_usage_count_respected():
|
||||
"""A tool with max_usage_count=1 should be usable exactly once, not blocked after 1 call."""
|
||||
call_round_1 = [_make_openai_tool_call("c1", "calculate", '{"expression": "1+1"}')]
|
||||
call_round_2 = [_make_openai_tool_call("c2", "calculate", '{"expression": "2+2"}')]
|
||||
|
||||
llm = _NativeToolCallLLM(
|
||||
tool_calls=[call_round_1, call_round_2], final_answer="done"
|
||||
)
|
||||
calc_tool = CalculatorTool()
|
||||
calc_tool.max_usage_count = 2
|
||||
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[calc_tool],
|
||||
)
|
||||
agent.kickoff("Compute 1+1 then 2+2")
|
||||
|
||||
executed = [r for r in agent.tools_results if "usage limit" not in r["result"]]
|
||||
assert len(executed) == 2
|
||||
assert calc_tool.current_usage_count == 2
|
||||
|
||||
|
||||
def test_lite_agent_native_tool_calls_with_after_llm_hook():
|
||||
"""Native tool calls must be processed even when after_llm_call hooks are active.
|
||||
|
||||
Regression test: _setup_after_llm_call_hooks was converting the list of
|
||||
tool calls to a string via str(), causing isinstance(answer, list) to fail
|
||||
in _invoke_loop_native_tools and silently returning the stringified list as
|
||||
the agent's final answer.
|
||||
"""
|
||||
hook_called = {"count": 0}
|
||||
|
||||
def after_hook(context):
|
||||
hook_called["count"] += 1
|
||||
return None
|
||||
|
||||
tool_call = [_make_openai_tool_call("call_1", "calculate", '{"expression": "6*7"}')]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_call], final_answer="The answer is 42")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
agent._after_llm_call_hooks.append(after_hook)
|
||||
|
||||
result = agent.kickoff("What is 6 * 7?")
|
||||
|
||||
assert hook_called["count"] >= 1
|
||||
assert len(agent.tools_results) == 1
|
||||
assert agent.tools_results[0]["tool_name"] == "calculate"
|
||||
assert "42" in result.raw
|
||||
|
||||
|
||||
def test_lite_agent_native_parallel_tool_calls_with_after_llm_hook():
|
||||
"""Multiple native tool calls in a single response must work with hooks active."""
|
||||
hook_called = {"count": 0}
|
||||
|
||||
def after_hook(context):
|
||||
hook_called["count"] += 1
|
||||
return None
|
||||
|
||||
tool_calls = [
|
||||
_make_openai_tool_call("call_1", "calculate", '{"expression": "2+3"}'),
|
||||
_make_openai_tool_call("call_2", "calculate", '{"expression": "4+5"}'),
|
||||
]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_calls], final_answer="5 and 9")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
agent._after_llm_call_hooks.append(after_hook)
|
||||
|
||||
result = agent.kickoff("What is 2+3 and 4+5?")
|
||||
|
||||
assert hook_called["count"] >= 1
|
||||
assert len(agent.tools_results) == 2
|
||||
tool_names = [r["tool_name"] for r in agent.tools_results]
|
||||
assert tool_names == ["calculate", "calculate"]
|
||||
|
||||
|
||||
def test_lite_agent_native_duplicate_tool_names_resolved():
|
||||
"""Two tools with the same sanitized name should both be usable via dedup suffixes.
|
||||
|
||||
convert_tools_to_openai_schema renames duplicates (e.g. calculate -> calculate_2).
|
||||
The original_tools_by_name mapping must honour these deduplicated names so
|
||||
result_as_answer, max_usage_count, and usage tracking work for every tool.
|
||||
"""
|
||||
tool_a = CalculatorTool()
|
||||
tool_a.result_as_answer = True
|
||||
|
||||
tool_b = CalculatorTool()
|
||||
|
||||
tool_call = [
|
||||
_make_openai_tool_call("c1", "calculate_2", '{"expression": "9+1"}'),
|
||||
]
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_call], final_answer="fallback")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[tool_a, tool_b],
|
||||
)
|
||||
result = agent.kickoff("What is 9+1?")
|
||||
|
||||
assert len(agent.tools_results) == 1
|
||||
assert agent.tools_results[0]["tool_name"] == "calculate_2"
|
||||
assert "10" in agent.tools_results[0]["result"]
|
||||
|
||||
Reference in New Issue
Block a user