Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/google/adk/agents/callback_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ async def save_artifact(self, filename: str, artifact: types.Part) -> int:
Returns:
The version of the artifact.
"""
if (
hasattr(artifact.inline_data, 'display_name')
and artifact.inline_data.display_name
):
filename = artifact.inline_data.display_name
if self._invocation_context.artifact_service is None:
raise ValueError("Artifact service is not initialized.")
version = await self._invocation_context.artifact_service.save_artifact(
Expand Down
107 changes: 80 additions & 27 deletions src/google/adk/cli/agent_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,16 @@ async def build_graph(
Returns:
None
"""
dark_green = '#0F5223'
light_green = '#69CB87'
light_gray = '#cccccc'
# Gradient with more contrast: Very Dark Blue/Slate to a Lighter Blue. Google Blue for edges.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you attach a screenshot?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attaching before and after

image image

gradient_start_color = '#1B2336' # Very Dark Slate/Blue-Charcoal
gradient_end_color = '#133874' # Lighter, Brighter Blue (Material Blue 400)
highlight_border_color = '#133874' # Match very dark start of gradient
highlight_font_color = '#FFFFFF' # White font
highlight_edge_color = '#4285F4' # Standard Google Blue for edges
white = '#ffffff'

light_gray = '#cccccc' # For non-highlighted font

def get_node_name(tool_or_agent: Union[BaseAgent, BaseTool]):
if isinstance(tool_or_agent, BaseAgent):
# Added Workflow Agent checks for different agent types
Expand All @@ -79,17 +84,47 @@ def get_node_name(tool_or_agent: Union[BaseAgent, BaseTool]):
def get_node_caption(tool_or_agent: Union[BaseAgent, BaseTool]):

if isinstance(tool_or_agent, BaseAgent):
return '🤖 ' + tool_or_agent.name
agent_name = (
tool_or_agent.displayName
if hasattr(tool_or_agent, 'displayName')
and tool_or_agent.displayName
else tool_or_agent.name
)
return '🤖 ' + agent_name
elif retrieval_tool_module_loaded and isinstance(
tool_or_agent, BaseRetrievalTool
):
return '🔎 ' + tool_or_agent.name
tool_name = (
tool_or_agent.displayName
if hasattr(tool_or_agent, 'displayName')
and tool_or_agent.displayName
else tool_or_agent.name
)
return '🔎 ' + tool_name
elif isinstance(tool_or_agent, FunctionTool):
return '🔧 ' + tool_or_agent.name
tool_name = (
tool_or_agent.displayName
if hasattr(tool_or_agent, 'displayName')
and tool_or_agent.displayName
else tool_or_agent.name
)
return '🔧 ' + tool_name
elif isinstance(tool_or_agent, AgentTool):
return '🤖 ' + tool_or_agent.name
agent_name = (
tool_or_agent.displayName
if hasattr(tool_or_agent, 'displayName')
and tool_or_agent.displayName
else tool_or_agent.name
)
return '🤖 ' + agent_name
elif isinstance(tool_or_agent, BaseTool):
return '🔧 ' + tool_or_agent.name
tool_name = (
tool_or_agent.displayName
if hasattr(tool_or_agent, 'displayName')
and tool_or_agent.displayName
else tool_or_agent.name
)
return '🔧 ' + tool_name
else:
logger.warning(
'Unsupported tool, type: %s, obj: %s',
Expand Down Expand Up @@ -219,11 +254,11 @@ async def draw_node(tool_or_agent: Union[BaseAgent, BaseTool]):
graph.node(
name,
caption,
style='filled,rounded',
fillcolor=dark_green,
color=dark_green,
style='filled,rounded', # All highlighted nodes are rounded
fillcolor=f'{gradient_start_color}:{gradient_end_color}',
color=highlight_border_color,
shape=shape,
fontcolor=light_gray,
fontcolor=highlight_font_color,
)
return
# if not in highlight, draw non-highlight node
Expand All @@ -240,9 +275,7 @@ async def draw_node(tool_or_agent: Union[BaseAgent, BaseTool]):
name,
caption,
shape=shape,
style='rounded',
color=light_gray,
fontcolor=light_gray,
# style will be inherited from node_attr
)

return
Expand All @@ -251,21 +284,22 @@ def draw_edge(from_name, to_name):
if highlight_pairs:
for highlight_from, highlight_to in highlight_pairs:
if from_name == highlight_from and to_name == highlight_to:
graph.edge(from_name, to_name, color=light_green)
graph.edge(
from_name, to_name, color=highlight_edge_color, penwidth='2.0'
)
return
elif from_name == highlight_to and to_name == highlight_from:
graph.edge(from_name, to_name, color=light_green, dir='back')
graph.edge(
from_name,
to_name,
color=highlight_edge_color,
penwidth='2.0',
dir='back',
)
return
# if no need to highlight, color gray
if should_build_agent_cluster(agent):

graph.edge(
from_name,
to_name,
color=light_gray,
)
else:
graph.edge(from_name, to_name, arrowhead='none', color=light_gray)
# Color will be inherited from graph.edge_attr. Using 'normal' arrowhead.
graph.edge(from_name, to_name, arrowhead='normal')

await draw_node(agent)
for sub_agent in agent.sub_agents:
Expand All @@ -285,7 +319,26 @@ def draw_edge(from_name, to_name):
async def get_agent_graph(root_agent, highlights_pairs, image=False):
print('build graph')
graph = graphviz.Digraph(
graph_attr={'rankdir': 'LR', 'bgcolor': '#333537'}, strict=True
graph_attr={
'rankdir': 'LR',
'bgcolor': '#333537',
'splines': 'spline', # Changed from 'curved'
'concentrate': 'true',
'overlap': 'false', # Added to prevent node overlap
},
node_attr={
'fontname': 'Arial',
'fontsize': '12',
'style': 'filled,rounded', # Default to rounded corners
'shape': 'box', # Default shape
'fillcolor': '#424242', # Slightly darker gray for non-highlighted nodes
'fontcolor': '#E8EAED',
'color': '#5F6368', # Border color
},
edge_attr={
'color': '#757575', # Darker gray for non-highlighted edges
'arrowsize': '0.7',
},
)
await build_graph(graph, root_agent, highlights_pairs)
if image:
Expand Down
10 changes: 8 additions & 2 deletions src/google/adk/cli/fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,14 @@ def tear_down_observer(observer: Observer, _: AdkWebServer):
)

if web:
BASE_DIR = Path(__file__).parent.resolve()
ANGULAR_DIST_PATH = BASE_DIR / "browser"
# Default to the pre-packaged UI, but allow overriding for local development.
if adk_web_dir_override := os.environ.get("ADK_WEB_DIR"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to stick to the command-line flag. When the CLI flag is not set adk web should use current folder.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A CLI flag is interesting but couldn't find one that's used to specify the web directory, can you tell me which one? the ADK_WEB_DIR env var is another option to support where developers want to build a customized version of the adk-web UI and need to serve it with the adk-web command for testing and demos.

ANGULAR_DIST_PATH = Path(adk_web_dir_override)
logger.info("Serving ADK web UI from: %s", ANGULAR_DIST_PATH)
else:
BASE_DIR = Path(__file__).parent.resolve()
ANGULAR_DIST_PATH = BASE_DIR / "browser"
logger.info("Serving pre-packaged ADK web UI.")
extra_fast_api_args.update(
web_assets_dir=ANGULAR_DIST_PATH,
)
Expand Down
7 changes: 6 additions & 1 deletion src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,12 @@ async def _append_new_message_to_session(
for i, part in enumerate(new_message.parts):
if part.inline_data is None:
continue
file_name = f'artifact_{invocation_context.invocation_id}_{i}'
file_name = (
part.inline_data.display_name
if hasattr(part.inline_data, 'display_name')
and part.inline_data.display_name
else f'artifact_{invocation_context.invocation_id}_{i}'
)
await self.artifact_service.save_artifact(
app_name=self.app_name,
user_id=session.user_id,
Expand Down
41 changes: 30 additions & 11 deletions src/google/adk/tools/function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,37 +40,56 @@ class FunctionTool(BaseTool):
"""

def __init__(
self, func: Callable[..., Any], *, require_confirmation: bool = False
self,
func: Callable[..., Any],
*,
name: Optional[str] = None,
description: Optional[str] = None,
displayName: Optional[str] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to change the core interface just for display purposes. We want the adk web to show the exact name so it's easier for developers to map what they see in the UI with the code.

require_confirmation: bool = False,
):
"""Initializes the FunctionTool. Extracts metadata from a callable object.
"""Initializes the FunctionTool.

Args:
func: The function to wrap.
require_confirmation: Whether the tool call requires user confirmation.
func: The callable to be wrapped as a tool.
name: Optional. The internal name of the tool. If None, it's inferred
from the function.
description: Optional. A description of what the tool does. If None,
it's inferred from the function's docstring.
displayName: Optional. A user-friendly name for display purposes. If
None, the internal name might be used as a fallback by consumers.
require_confirmation: If true, the tool requires user's confirmation before
execution.
"""
name = ''
doc = ''
inferred_name = ''
inferred_description = ''
# Handle different types of callables
if hasattr(func, '__name__'):
# Regular functions, unbound methods, etc.
name = func.__name__
inferred_name = func.__name__
elif hasattr(func, '__class__'):
# Callable objects, bound methods, etc.
name = func.__class__.__name__
inferred_name = func.__class__.__name__

# Get documentation (prioritize direct __doc__ if available)
if hasattr(func, '__doc__') and func.__doc__:
doc = inspect.cleandoc(func.__doc__)
inferred_description = inspect.cleandoc(func.__doc__)
elif (
hasattr(func, '__call__')
and hasattr(func.__call__, '__doc__')
and func.__call__.__doc__
):
# For callable objects, try to get docstring from __call__ method
doc = inspect.cleandoc(func.__call__.__doc__)
inferred_description = inspect.cleandoc(func.__call__.__doc__)

tool_name = name if name is not None else inferred_name
tool_description = (
description if description is not None else inferred_description
)

super().__init__(name=name, description=doc)
super().__init__(name=tool_name, description=tool_description)
self.func = func
self.displayName = displayName
self._ignore_params = ['tool_context', 'input_stream']
self._require_confirmation = require_confirmation

Expand Down