-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Add display banner #5754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v2-main
Are you sure you want to change the base?
Add display banner #5754
Changes from 5 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| """Agent banner display — logo, agent info, and next-steps guide. | ||
|
|
||
| Prints a branded banner to stderr the first time an agent runs. Subsequent calls | ||
| for the same agent instance are no-ops. Each unique agent instance gets its own | ||
| banner. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import platform | ||
| import sys | ||
| from datetime import datetime, timezone | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from . import __version__ | ||
|
|
||
| if TYPE_CHECKING: | ||
| from .agent import Agent as _Agent | ||
|
|
||
| # Track which agent instances have already been displayed (process-wide). | ||
| _displayed_agent_ids: set[int] = set() | ||
|
|
||
| LOGO_PREAMBLE = r""" | ||
| pydantic-ai v{version} • Python {python_version} | ||
| """ | ||
|
|
||
| LOGO = r""" | ||
| /\ | ||
| / \ | ||
| / \ | ||
| / \ | ||
| /________\ | ||
| / || \ | ||
| / || \ | ||
| \_____||_____/ | ||
| """ | ||
|
|
||
| LOGO_TEXT = r""" | ||
| [agent] | ||
| name: {name} | ||
| model: {model} | ||
| output_type: {output_type} | ||
| tools: {tools} | ||
| logfire: {logfire_status} | ||
|
|
||
|
|
||
| """ | ||
|
|
||
| # line by line join of Logo and text for easier maintenance | ||
| LOGO_PLUS_TEXT = '\n'.join( | ||
| f'{logo_line}{text_line}' | ||
| for logo_line, text_line in zip( | ||
| LOGO.splitlines(), | ||
| LOGO_TEXT.splitlines(), | ||
| ) | ||
| ) | ||
|
|
||
| NEXT_STEPS = """ | ||
| ─── Next steps ──────────────────────────────────── | ||
| • Setup Logfire (Free): https://ai.pydantic.dev/logfire/ | ||
| • Configure Logfire: logfire.configure(...) | ||
| • Instrument this agent: logfire.instrument_pydantic_ai() | ||
| • Instrument its calls: .instrument_httpx · .instrument_openai | ||
| • Export anywhere: OTLP → Logfire · etc | ||
|
|
||
| • Docs: https://ai.pydantic.dev/logfire/ | ||
| • Turn off this banner: os.environ['PYDANTIC_AI_HIDE_BANNER'] = '1' | ||
|
|
||
| """ | ||
|
|
||
| BANNER = f'{LOGO_PREAMBLE}\n{LOGO_PLUS_TEXT}\n{NEXT_STEPS}' | ||
|
|
||
|
|
||
| def _get_model_name(agent: _Agent[Any, Any]) -> str: | ||
| """Get a display-friendly model name from the agent.""" | ||
| model = agent.model | ||
| if model is None: | ||
| return '(not set)' | ||
| if isinstance(model, str): | ||
| return model | ||
| # Model instance | ||
| try: | ||
| return model.model_id | ||
| except Exception: | ||
| return getattr(model, 'model_name', str(model)) | ||
|
|
||
|
|
||
| def _get_output_type_name(agent: _Agent[Any, Any]) -> str: | ||
| """Get a display-friendly output type name.""" | ||
| try: | ||
| ot = agent.output_type | ||
| return getattr(ot, '__name__', str(ot)) | ||
| except Exception: | ||
| return 'str' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOW — Fallback The exception path returns the literal string |
||
|
|
||
|
|
||
| def _get_tool_names(agent: _Agent[Any, Any]) -> str: | ||
| """Get comma-separated tool names from the agent's function toolset.""" | ||
| try: | ||
| tools = agent._function_toolset.tools # type: ignore[union-attr] | ||
| if tools: | ||
| return ', '.join(tools.keys()) | ||
| return '(none)' | ||
| except Exception: | ||
| return '(none)' | ||
|
|
||
|
|
||
| def _get_logfire_status(agent: _Agent[Any, Any]) -> str: | ||
| """Check whether Logfire instrumentation is configured.""" | ||
| try: | ||
| settings = agent._resolve_instrumentation_settings() | ||
| return 'configured' if settings is not None else 'not configured' | ||
| except Exception: | ||
| return 'unknown' | ||
|
|
||
|
|
||
| def display_agent_banner(agent: _Agent[Any, Any]) -> None: | ||
| """Display the agent banner to stderr, once per agent instance per process. | ||
|
|
||
| Args: | ||
| agent: The agent to display the banner for. | ||
| """ | ||
| import os | ||
|
|
||
| if os.environ.get('PYDANTIC_AI_HIDE_BANNER'): | ||
| return | ||
|
|
||
| agent_id = id(agent) | ||
|
strawgate marked this conversation as resolved.
Outdated
|
||
| if agent_id in _displayed_agent_ids: | ||
| return | ||
| _displayed_agent_ids.add(agent_id) | ||
|
|
||
| name = agent.name or '(unnamed)' | ||
| model = _get_model_name(agent) | ||
| output_type = _get_output_type_name(agent) | ||
| tools = _get_tool_names(agent) | ||
| logfire_status = _get_logfire_status(agent) | ||
| python_version = platform.python_version() | ||
| timestamp = datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S') | ||
|
strawgate marked this conversation as resolved.
Outdated
|
||
|
|
||
| banner = BANNER.format( | ||
| version=__version__, | ||
| python_version=python_version, | ||
| timestamp=timestamp, | ||
| name=name, | ||
| model=model, | ||
| output_type=output_type, | ||
| tools=tools, | ||
| logfire_status=logfire_status, | ||
| ) | ||
|
|
||
| print(f'\n{banner}\n', file=sys.stderr) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -872,6 +872,24 @@ def event_stream_handler(self) -> EventStreamHandler[AgentDepsT] | None: | |
| def __repr__(self) -> str: | ||
| return f'{type(self).__name__}(model={self.model!r}, name={self.name!r}, end_strategy={self.end_strategy!r}, model_settings={self.model_settings!r}, output_type={self.output_type!r})' | ||
|
|
||
| def display_banner(self) -> None: | ||
|
strawgate marked this conversation as resolved.
|
||
| """Display a banner with logo, agent info, and next steps. | ||
|
|
||
| Prints to stderr once per agent instance. Subsequent calls are no-ops. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOW — Stale docstring says "once per agent instance" The actual behavior is once per process (driven by |
||
| Called automatically on the first `run()` / `run_sync()` / `iter()` call. | ||
|
|
||
| Example: | ||
| ```python | ||
| from pydantic_ai import Agent | ||
|
|
||
| agent = Agent('openai:gpt-5.2', name='my_agent') | ||
| agent.display_banner() | ||
| ``` | ||
| """ | ||
| from .._display import display_agent_banner | ||
|
|
||
| display_agent_banner(self) | ||
|
|
||
| @overload | ||
| def iter( | ||
| self, | ||
|
|
@@ -1036,6 +1054,11 @@ async def main(): | |
| if infer_name and self.name is None: | ||
| self._infer_name(inspect.currentframe()) | ||
|
|
||
| # Display agent banner on first run (once per agent instance, process-wide) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOW — Stale comment: "once per agent instance, process-wide" Same wording bug as the |
||
| from .._display import display_agent_banner | ||
|
|
||
| display_agent_banner(self) | ||
|
|
||
| # Tool retries cannot be overridden per run: `int` is treated as the output budget. An explicit | ||
| # `retries={'tools': ...}` is rejected so the value isn't silently dropped. | ||
| retry_overrides = _normalize_agent_retry_overrides(retries, int_means='output') | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.