Skip to content

Commit 55ed91e

Browse files
authored
feat: log usage tools when called by LLM (#2916)
* feat: log usage tools when called by LLM * feat: print llm tool usage in console
1 parent e676c83 commit 55ed91e

File tree

7 files changed

+363
-21
lines changed

7 files changed

+363
-21
lines changed

src/crewai/llm.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import threading
66
import warnings
77
from collections import defaultdict
8-
from contextlib import contextmanager, redirect_stderr, redirect_stdout
8+
from contextlib import contextmanager
99
from typing import (
1010
Any,
1111
DefaultDict,
@@ -18,7 +18,7 @@
1818
Union,
1919
cast,
2020
)
21-
21+
from datetime import datetime
2222
from dotenv import load_dotenv
2323
from litellm.types.utils import ChatCompletionDeltaToolCall
2424
from pydantic import BaseModel, Field
@@ -30,6 +30,11 @@
3030
LLMCallType,
3131
LLMStreamChunkEvent,
3232
)
33+
from crewai.utilities.events.tool_usage_events import (
34+
ToolUsageStartedEvent,
35+
ToolUsageFinishedEvent,
36+
ToolUsageErrorEvent,
37+
)
3338

3439
with warnings.catch_warnings():
3540
warnings.simplefilter("ignore", UserWarning)
@@ -833,7 +838,26 @@ def _handle_tool_call(
833838
fn = available_functions[function_name]
834839

835840
# --- 3.2) Execute function
841+
assert hasattr(crewai_event_bus, "emit")
842+
started_at = datetime.now()
843+
crewai_event_bus.emit(
844+
self,
845+
event=ToolUsageStartedEvent(
846+
tool_name=function_name,
847+
tool_args=function_args,
848+
),
849+
)
836850
result = fn(**function_args)
851+
crewai_event_bus.emit(
852+
self,
853+
event=ToolUsageFinishedEvent(
854+
output=result,
855+
tool_name=function_name,
856+
tool_args=function_args,
857+
started_at=started_at,
858+
finished_at=datetime.now(),
859+
),
860+
)
837861

838862
# --- 3.3) Emit success event
839863
self._handle_emit_call_events(result, LLMCallType.TOOL_CALL)
@@ -849,6 +873,14 @@ def _handle_tool_call(
849873
self,
850874
event=LLMCallFailedEvent(error=f"Tool execution error: {str(e)}"),
851875
)
876+
crewai_event_bus.emit(
877+
self,
878+
event=ToolUsageErrorEvent(
879+
tool_name=function_name,
880+
tool_args=function_args,
881+
error=f"Tool execution error: {str(e)}"
882+
),
883+
)
852884
return None
853885

854886
def call(

src/crewai/utilities/events/event_listener.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Any, Dict
33

44
from pydantic import Field, PrivateAttr
5-
5+
from crewai.llm import LLM
66
from crewai.task import Task
77
from crewai.telemetry.telemetry import Telemetry
88
from crewai.utilities import Logger
@@ -283,27 +283,43 @@ def on_method_execution_failed(source, event: MethodExecutionFailedEvent):
283283

284284
@crewai_event_bus.on(ToolUsageStartedEvent)
285285
def on_tool_usage_started(source, event: ToolUsageStartedEvent):
286-
self.formatter.handle_tool_usage_started(
287-
self.formatter.current_agent_branch,
288-
event.tool_name,
286+
if isinstance(source, LLM):
287+
self.formatter.handle_llm_tool_usage_started(
288+
event.tool_name,
289+
)
290+
else:
291+
self.formatter.handle_tool_usage_started(
292+
self.formatter.current_agent_branch,
293+
event.tool_name,
289294
self.formatter.current_crew_tree,
290295
)
291296

292297
@crewai_event_bus.on(ToolUsageFinishedEvent)
293298
def on_tool_usage_finished(source, event: ToolUsageFinishedEvent):
294-
self.formatter.handle_tool_usage_finished(
295-
self.formatter.current_tool_branch,
296-
event.tool_name,
297-
self.formatter.current_crew_tree,
298-
)
299+
if isinstance(source, LLM):
300+
self.formatter.handle_llm_tool_usage_finished(
301+
event.tool_name,
302+
)
303+
else:
304+
self.formatter.handle_tool_usage_finished(
305+
self.formatter.current_tool_branch,
306+
event.tool_name,
307+
self.formatter.current_crew_tree,
308+
)
299309

300310
@crewai_event_bus.on(ToolUsageErrorEvent)
301311
def on_tool_usage_error(source, event: ToolUsageErrorEvent):
302-
self.formatter.handle_tool_usage_error(
303-
self.formatter.current_tool_branch,
304-
event.tool_name,
305-
event.error,
306-
self.formatter.current_crew_tree,
312+
if isinstance(source, LLM):
313+
self.formatter.handle_llm_tool_usage_error(
314+
event.tool_name,
315+
event.error,
316+
)
317+
else:
318+
self.formatter.handle_tool_usage_error(
319+
self.formatter.current_tool_branch,
320+
event.tool_name,
321+
event.error,
322+
self.formatter.current_crew_tree,
307323
)
308324

309325
# ----------- LLM EVENTS -----------

src/crewai/utilities/events/tool_usage_events.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
class ToolUsageEvent(BaseEvent):
88
"""Base event for tool usage tracking"""
99

10-
agent_key: str
11-
agent_role: str
10+
agent_key: Optional[str] = None
11+
agent_role: Optional[str] = None
1212
tool_name: str
1313
tool_args: Dict[str, Any] | str
14-
tool_class: str
14+
tool_class: Optional[str] = None
1515
run_attempts: int | None = None
1616
delegations: int | None = None
1717
agent: Optional[Any] = None

src/crewai/utilities/events/utils/console_formatter.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ConsoleFormatter:
1717
current_lite_agent_branch: Optional[Tree] = None
1818
tool_usage_counts: Dict[str, int] = {}
1919
current_reasoning_branch: Optional[Tree] = None # Track reasoning status
20+
current_llm_tool_tree: Optional[Tree] = None
2021

2122
def __init__(self, verbose: bool = False):
2223
self.console = Console(width=None)
@@ -426,6 +427,51 @@ def update_method_status(
426427
self.print()
427428
return method_branch
428429

430+
def get_llm_tree(self, tool_name: str):
431+
text = Text()
432+
text.append(f"🔧 Using {tool_name} from LLM available_function", style="yellow")
433+
434+
tree = self.current_flow_tree or self.current_crew_tree
435+
436+
if tree:
437+
tree.add(text)
438+
439+
return tree or Tree(text)
440+
441+
def handle_llm_tool_usage_started(
442+
self,
443+
tool_name: str,
444+
):
445+
tree = self.get_llm_tree(tool_name)
446+
self.add_tree_node(tree, "🔄 Tool Usage Started", "green")
447+
self.print(tree)
448+
self.print()
449+
return tree
450+
451+
def handle_llm_tool_usage_finished(
452+
self,
453+
tool_name: str,
454+
):
455+
tree = self.get_llm_tree(tool_name)
456+
self.add_tree_node(tree, "✅ Tool Usage Completed", "green")
457+
self.print(tree)
458+
self.print()
459+
460+
def handle_llm_tool_usage_error(
461+
self,
462+
tool_name: str,
463+
error: str,
464+
):
465+
tree = self.get_llm_tree(tool_name)
466+
self.add_tree_node(tree, "❌ Tool Usage Failed", "red")
467+
self.print(tree)
468+
self.print()
469+
470+
error_content = self.create_status_content(
471+
"Tool Usage Failed", tool_name, "red", Error=error
472+
)
473+
self.print_panel(error_content, "Tool Error", "red")
474+
429475
def handle_tool_usage_started(
430476
self,
431477
agent_branch: Optional[Tree],
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": "What is the weather in New York?"}],
4+
"model": "gpt-4o", "stop": [], "stream": true, "stream_options": {"include_usage":
5+
true}, "tools": [{"type": "function", "function": {"name": "get_weather", "description":
6+
"Get the current weather in a given location", "parameters": {"type": "object",
7+
"properties": {"location": {"type": "string", "description": "The city and state,
8+
e.g. San Francisco, CA"}}, "required": ["location"]}}}]}'
9+
headers:
10+
accept:
11+
- application/json
12+
accept-encoding:
13+
- gzip, deflate
14+
connection:
15+
- keep-alive
16+
content-length:
17+
- '470'
18+
content-type:
19+
- application/json
20+
cookie:
21+
- _cfuvid=3UeEmz_rnmsoZxrVUv32u35gJOi766GDWNe5_RTjiPk-1736537376739-0.0.1.1-604800000
22+
host:
23+
- api.openai.com
24+
user-agent:
25+
- OpenAI/Python 1.68.2
26+
x-stainless-arch:
27+
- arm64
28+
x-stainless-async:
29+
- 'false'
30+
x-stainless-lang:
31+
- python
32+
x-stainless-os:
33+
- MacOS
34+
x-stainless-package-version:
35+
- 1.68.2
36+
x-stainless-raw-response:
37+
- 'true'
38+
x-stainless-read-timeout:
39+
- '600.0'
40+
x-stainless-retry-count:
41+
- '0'
42+
x-stainless-runtime:
43+
- CPython
44+
x-stainless-runtime-version:
45+
- 3.12.9
46+
method: POST
47+
uri: https://api.openai.com/v1/chat/completions
48+
response:
49+
body:
50+
string: 'data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_UkMsNK0RTJ1nlT19WqgLJYV9","type":"function","function":{"name":"get_weather","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}
51+
52+
53+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}
54+
55+
56+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"location"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
57+
58+
59+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}
60+
61+
62+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"New"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
63+
64+
65+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"
66+
York"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
67+
68+
69+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":","}}]},"logprobs":null,"finish_reason":null}],"usage":null}
70+
71+
72+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"
73+
NY"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
74+
75+
76+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
77+
78+
79+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null}
80+
81+
82+
data: {"id":"chatcmpl-BcY6NFDeu4HFOAIarpwSNAUEMuPTg","object":"chat.completion.chunk","created":1748527251,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_07871e2ad8","choices":[],"usage":{"prompt_tokens":68,"completion_tokens":17,"total_tokens":85,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}
83+
84+
85+
data: [DONE]
86+
87+
88+
'
89+
headers:
90+
CF-RAY:
91+
- 947685373af8a435-GRU
92+
Connection:
93+
- keep-alive
94+
Content-Type:
95+
- text/event-stream; charset=utf-8
96+
Date:
97+
- Thu, 29 May 2025 14:00:51 GMT
98+
Server:
99+
- cloudflare
100+
Set-Cookie:
101+
- __cf_bm=fFoq7oCHLgmljA4hsHWxTGHMEWJ.0t1XTuDptZPPkOc-1748527251-1.0.1.1-PP3Hd7XzA4AQFn0JQWjuQdhFwey0Pj9maUWKfFG16Bkl69Uk65A8XKN73UbsvO327TruwxameKb_m_HDePCR.YN0TZlE8Pu45WsA9shDwKY;
102+
path=/; expires=Thu, 29-May-25 14:30:51 GMT; domain=.api.openai.com; HttpOnly;
103+
Secure; SameSite=None
104+
- _cfuvid=ut1CVX5GOYnv03fiV2Dsv7cm5soJmwgSutkPAEuVXWg-1748527251565-0.0.1.1-604800000;
105+
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
106+
Transfer-Encoding:
107+
- chunked
108+
X-Content-Type-Options:
109+
- nosniff
110+
access-control-expose-headers:
111+
- X-Request-ID
112+
alt-svc:
113+
- h3=":443"; ma=86400
114+
cf-cache-status:
115+
- DYNAMIC
116+
openai-organization:
117+
- crewai-iuxna1
118+
openai-processing-ms:
119+
- '332'
120+
openai-version:
121+
- '2020-10-01'
122+
strict-transport-security:
123+
- max-age=31536000; includeSubDomains; preload
124+
x-envoy-upstream-service-time:
125+
- '334'
126+
x-ratelimit-limit-requests:
127+
- '10000'
128+
x-ratelimit-limit-tokens:
129+
- '30000000'
130+
x-ratelimit-remaining-requests:
131+
- '9999'
132+
x-ratelimit-remaining-tokens:
133+
- '29999989'
134+
x-ratelimit-reset-requests:
135+
- 6ms
136+
x-ratelimit-reset-tokens:
137+
- 0s
138+
x-request-id:
139+
- req_1dc91fc964a8d23ee023693400e5c181
140+
status:
141+
code: 200
142+
message: OK
143+
version: 1

0 commit comments

Comments
 (0)