From 76f1712c0a94e725eb677caba2263a1f10bc443d Mon Sep 17 00:00:00 2001 From: Roman Tejada Date: Wed, 3 Sep 2025 18:05:44 -0400 Subject: [PATCH 1/5] feat: Allow overriding web UI path with ADK_WEB_DIR --- src/google/adk/cli/fast_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 7d93b54360..452f8966e6 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -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"): + 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, ) From 847574846bb752f6cc19667ebebce95ef183552a Mon Sep 17 00:00:00 2001 From: Roman Tejada Date: Wed, 3 Sep 2025 18:23:03 -0400 Subject: [PATCH 2/5] feat: Add displayName to FunctionTool for improved graph readability --- src/google/adk/cli/agent_graph.py | 40 +++++++++++++++++++++++---- src/google/adk/tools/function_tool.py | 39 ++++++++++++++++++-------- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/google/adk/cli/agent_graph.py b/src/google/adk/cli/agent_graph.py index e919010cce..0e54d377de 100644 --- a/src/google/adk/cli/agent_graph.py +++ b/src/google/adk/cli/agent_graph.py @@ -79,17 +79,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', diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index a3a580662a..641e990e21 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -40,39 +40,54 @@ 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, ): - """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. """ - 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 + self._require_confirmation = False @override def _get_declaration(self) -> Optional[types.FunctionDeclaration]: From 081e15a8223e59cef5fb5a8dc5db5d5d77f23e7c Mon Sep 17 00:00:00 2001 From: Roman Tejada Date: Thu, 4 Sep 2025 17:37:39 -0400 Subject: [PATCH 3/5] style: Add gradient background to graph nodes --- src/google/adk/cli/agent_graph.py | 67 +++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/src/google/adk/cli/agent_graph.py b/src/google/adk/cli/agent_graph.py index 0e54d377de..2f44191d56 100644 --- a/src/google/adk/cli/agent_graph.py +++ b/src/google/adk/cli/agent_graph.py @@ -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. + 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 @@ -249,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 @@ -270,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 @@ -281,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: @@ -315,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: From 64ff784644745cd7712b1d4d3f2ac4fe833d89b5 Mon Sep 17 00:00:00 2001 From: Roman Tejada Date: Thu, 4 Sep 2025 18:07:45 -0400 Subject: [PATCH 4/5] feat: Use artifact displayName for filename --- src/google/adk/agents/callback_context.py | 5 +++++ src/google/adk/runners.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/google/adk/agents/callback_context.py b/src/google/adk/agents/callback_context.py index f42d344ad5..9e5d16a8c1 100644 --- a/src/google/adk/agents/callback_context.py +++ b/src/google/adk/agents/callback_context.py @@ -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( diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index bcf29839d1..d4159d5d2c 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -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, From 304b2f414b23928d3243faec476b4c558a967de0 Mon Sep 17 00:00:00 2001 From: Roman Tejada Date: Thu, 4 Sep 2025 20:15:46 -0400 Subject: [PATCH 5/5] fix: Restore require_confirmation in FunctionTool --- src/google/adk/tools/function_tool.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 641e990e21..8e46e3d851 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -42,9 +42,11 @@ class FunctionTool(BaseTool): def __init__( self, func: Callable[..., Any], + *, name: Optional[str] = None, description: Optional[str] = None, displayName: Optional[str] = None, + require_confirmation: bool = False, ): """Initializes the FunctionTool. @@ -56,6 +58,8 @@ def __init__( 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. """ inferred_name = '' inferred_description = '' @@ -87,7 +91,7 @@ def __init__( self.func = func self.displayName = displayName self._ignore_params = ['tool_context', 'input_stream'] - self._require_confirmation = False + self._require_confirmation = require_confirmation @override def _get_declaration(self) -> Optional[types.FunctionDeclaration]: