From 01ecc4c0610a221ff46d7fe77ae032e34c4a1367 Mon Sep 17 00:00:00 2001 From: Sebastian Hirt Date: Mon, 6 Oct 2025 22:31:08 +0200 Subject: [PATCH] feat: optionally show tool calls in CLI --- pydantic_ai_slim/pydantic_ai/_cli.py | 17 +++++++++++++++-- pydantic_ai_slim/pydantic_ai/agent/abstract.py | 9 ++++++++- tests/test_cli.py | 4 ++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/_cli.py b/pydantic_ai_slim/pydantic_ai/_cli.py index 95fcf8b520..e8638e311e 100644 --- a/pydantic_ai_slim/pydantic_ai/_cli.py +++ b/pydantic_ai_slim/pydantic_ai/_cli.py @@ -18,7 +18,7 @@ from ._run_context import AgentDepsT from .agent import AbstractAgent, Agent from .exceptions import UserError -from .messages import ModelMessage, ModelResponse +from .messages import FunctionToolCallEvent, FunctionToolResultEvent, ModelMessage, ModelResponse from .models import KnownModelName, infer_model from .output import OutputDataT @@ -229,6 +229,7 @@ async def run_chat( config_dir: Path | None = None, deps: AgentDepsT = None, message_history: list[ModelMessage] | None = None, + show_tool_calls: bool = False, ) -> int: prompt_history_path = (config_dir or PYDANTIC_AI_HOME) / PROMPT_HISTORY_FILENAME prompt_history_path.parent.mkdir(parents=True, exist_ok=True) @@ -255,7 +256,7 @@ async def run_chat( return exit_value else: try: - messages = await ask_agent(agent, text, stream, console, code_theme, deps, messages) + messages = await ask_agent(agent, text, stream, console, code_theme, deps, messages, show_tool_calls) except CancelledError: # pragma: no cover console.print('[dim]Interrupted[/dim]') except Exception as e: # pragma: no cover @@ -273,6 +274,7 @@ async def ask_agent( code_theme: str, deps: AgentDepsT = None, messages: list[ModelMessage] | None = None, + show_tool_calls: bool = False, ) -> list[ModelMessage]: status = Status('[dim]Working on it…[/dim]', console=console) @@ -294,6 +296,17 @@ async def ask_agent( async for content in handle_stream.stream_output(debounce_by=None): live.update(Markdown(str(content), code_theme=code_theme)) + elif show_tool_calls and Agent.is_call_tools_node(node): + async with node.stream(agent_run.ctx) as handle_stream: + async for event in handle_stream: + if isinstance(event, FunctionToolCallEvent): + console.print( + Markdown(f'[Tool] {event.part.tool_name!r} called with args={event.part.args}') + ) + elif isinstance(event, FunctionToolResultEvent): + console.print( + Markdown(f'[Tool] {event.result.tool_name!r} returned => {event.result.content}') + ) assert agent_run.result is not None return agent_run.result.all_messages() diff --git a/pydantic_ai_slim/pydantic_ai/agent/abstract.py b/pydantic_ai_slim/pydantic_ai/agent/abstract.py index c5d59f7561..e3317140b5 100644 --- a/pydantic_ai_slim/pydantic_ai/agent/abstract.py +++ b/pydantic_ai_slim/pydantic_ai/agent/abstract.py @@ -1113,6 +1113,7 @@ async def to_cli( deps: AgentDepsT = None, prog_name: str = 'pydantic-ai', message_history: list[_messages.ModelMessage] | None = None, + show_tool_calls: bool = False, ) -> None: """Run the agent in a CLI chat interface. @@ -1120,6 +1121,7 @@ async def to_cli( deps: The dependencies to pass to the agent. prog_name: The name of the program to use for the CLI. Defaults to 'pydantic-ai'. message_history: History of the conversation so far. + show_tool_calls: Whether to show tool calls in the CLI. Example: ```python {title="agent_to_cli.py" test="skip"} @@ -1143,6 +1145,7 @@ async def main(): code_theme='monokai', prog_name=prog_name, message_history=message_history, + show_tool_calls=show_tool_calls, ) def to_cli_sync( @@ -1150,6 +1153,7 @@ def to_cli_sync( deps: AgentDepsT = None, prog_name: str = 'pydantic-ai', message_history: list[_messages.ModelMessage] | None = None, + show_tool_calls: bool = False, ) -> None: """Run the agent in a CLI chat interface with the non-async interface. @@ -1157,6 +1161,7 @@ def to_cli_sync( deps: The dependencies to pass to the agent. prog_name: The name of the program to use for the CLI. Defaults to 'pydantic-ai'. message_history: History of the conversation so far. + show_tool_calls: Whether to show tool calls in the CLI. ```python {title="agent_to_cli_sync.py" test="skip"} from pydantic_ai import Agent @@ -1167,5 +1172,7 @@ def to_cli_sync( ``` """ return get_event_loop().run_until_complete( - self.to_cli(deps=deps, prog_name=prog_name, message_history=message_history) + self.to_cli( + deps=deps, prog_name=prog_name, message_history=message_history, show_tool_calls=show_tool_calls + ) ) diff --git a/tests/test_cli.py b/tests/test_cli.py index e95ff09141..10d686ff2e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -293,6 +293,7 @@ def test_agent_to_cli_sync(mocker: MockerFixture, env: TestEnv): prog_name='pydantic-ai', deps=None, message_history=None, + show_tool_calls=False, ) @@ -309,6 +310,7 @@ async def test_agent_to_cli_async(mocker: MockerFixture, env: TestEnv): prog_name='pydantic-ai', deps=None, message_history=None, + show_tool_calls=False, ) @@ -329,6 +331,7 @@ async def test_agent_to_cli_with_message_history(mocker: MockerFixture, env: Tes prog_name='pydantic-ai', deps=None, message_history=test_messages, + show_tool_calls=False, ) @@ -348,4 +351,5 @@ def test_agent_to_cli_sync_with_message_history(mocker: MockerFixture, env: Test prog_name='pydantic-ai', deps=None, message_history=test_messages, + show_tool_calls=False, )