3232from ...agents .parallel_agent import ParallelAgent
3333from ...agents .sequential_agent import SequentialAgent
3434from ...tools .example_tool import ExampleTool
35+ from ...workflow ._base_node import BaseNode
36+ from ...workflow ._base_node import START
37+ from ...workflow ._workflow import Workflow
3538from ..experimental import a2a_experimental
3639
3740logger = logging .getLogger ('google_adk.' + __name__ )
3841
3942
4043@a2a_experimental
4144class AgentCardBuilder :
42- """Builder class for creating agent cards from ADK agents.
45+ """Builder class for creating agent cards from ADK agents or workflows .
4346
44- This class provides functionality to convert ADK agents into A2A agent cards,
45- including extracting skills, capabilities, and metadata from various agent
46- types .
47+ This class provides functionality to convert an ADK BaseAgent (e.g. LlmAgent)
48+ or a Workflow into an A2A agent card, including extracting skills,
49+ capabilities, and metadata .
4750 """
4851
4952 def __init__ (
5053 self ,
5154 * ,
52- agent : BaseAgent ,
55+ agent : BaseAgent | Workflow ,
5356 rpc_url : Optional [str ] = None ,
5457 capabilities : Optional [AgentCapabilities ] = None ,
5558 doc_url : Optional [str ] = None ,
@@ -59,6 +62,11 @@ def __init__(
5962 ):
6063 if not agent :
6164 raise ValueError ('Agent cannot be None or empty.' )
65+ if not isinstance (agent , (BaseAgent , Workflow )):
66+ raise TypeError (
67+ 'AgentCardBuilder requires a BaseAgent or Workflow, got '
68+ f'{ type (agent ).__name__ } .'
69+ )
6270
6371 self ._agent = agent
6472 self ._rpc_url = rpc_url or 'http://localhost:80/a2a'
@@ -96,8 +104,17 @@ async def build(self) -> AgentCard:
96104
97105
98106# Module-level helper functions
99- async def _build_primary_skills (agent : BaseAgent ) -> List [AgentSkill ]:
100- """Build skills for any agent type."""
107+ def _iter_child_nodes (agent : BaseNode ) -> List [BaseNode ]:
108+ """Returns the immediate child nodes of an agent or a workflow."""
109+ if isinstance (agent , BaseAgent ):
110+ return list (agent .sub_agents )
111+ if isinstance (agent , Workflow ) and agent .graph is not None :
112+ return [n for n in agent .graph .nodes if n .name != START .name ]
113+ return []
114+
115+
116+ async def _build_primary_skills (agent : BaseNode ) -> List [AgentSkill ]:
117+ """Build skills for any node type."""
101118 if isinstance (agent , LlmAgent ):
102119 return await _build_llm_agent_skills (agent )
103120 else :
@@ -140,10 +157,10 @@ async def _build_llm_agent_skills(agent: LlmAgent) -> List[AgentSkill]:
140157 return skills
141158
142159
143- async def _build_sub_agent_skills (agent : BaseAgent ) -> List [AgentSkill ]:
144- """Build skills for all sub-agents."""
160+ async def _build_sub_agent_skills (agent : BaseNode ) -> List [AgentSkill ]:
161+ """Build skills for all child nodes ( sub-agents or workflow nodes) ."""
145162 sub_agent_skills = []
146- for sub_agent in agent . sub_agents :
163+ for sub_agent in _iter_child_nodes ( agent ) :
147164 try :
148165 sub_skills = await _build_primary_skills (sub_agent )
149166 for skill in sub_skills :
@@ -225,8 +242,8 @@ def _build_code_executor_skill(agent: LlmAgent) -> AgentSkill:
225242 )
226243
227244
228- async def _build_non_llm_agent_skills (agent : BaseAgent ) -> List [AgentSkill ]:
229- """Build skills for non-LLM agents."""
245+ async def _build_non_llm_agent_skills (agent : BaseNode ) -> List [AgentSkill ]:
246+ """Build skills for non-LLM agents and workflow nodes ."""
230247 skills = []
231248
232249 # 1. Agent skill (main agent skill)
@@ -249,8 +266,8 @@ async def _build_non_llm_agent_skills(agent: BaseAgent) -> List[AgentSkill]:
249266 )
250267 )
251268
252- # 2. Sub-agent orchestration skill (for agents with sub-agents )
253- if agent . sub_agents :
269+ # 2. Orchestration skill (for agents/workflows with child nodes )
270+ if _iter_child_nodes ( agent ) :
254271 orchestration_skill = _build_orchestration_skill (agent , agent_type )
255272 if orchestration_skill :
256273 skills .append (orchestration_skill )
@@ -259,11 +276,11 @@ async def _build_non_llm_agent_skills(agent: BaseAgent) -> List[AgentSkill]:
259276
260277
261278def _build_orchestration_skill (
262- agent : BaseAgent , agent_type : str
279+ agent : BaseNode , agent_type : str
263280) -> Optional [AgentSkill ]:
264- """Build orchestration skill for agents with sub-agents ."""
281+ """Build orchestration skill for agents/workflows with child nodes ."""
265282 sub_agent_descriptions = []
266- for sub_agent in agent . sub_agents :
283+ for sub_agent in _iter_child_nodes ( agent ) :
267284 description = sub_agent .description or 'No description'
268285 sub_agent_descriptions .append (f'{ sub_agent .name } : { description } ' )
269286
@@ -281,7 +298,7 @@ def _build_orchestration_skill(
281298 )
282299
283300
284- def _get_agent_type (agent : BaseAgent ) -> str :
301+ def _get_agent_type (agent : BaseNode ) -> str :
285302 """Get the agent type for tagging."""
286303 if isinstance (agent , LlmAgent ):
287304 return 'llm'
@@ -291,21 +308,23 @@ def _get_agent_type(agent: BaseAgent) -> str:
291308 return 'parallel_workflow'
292309 elif isinstance (agent , LoopAgent ):
293310 return 'loop_workflow'
311+ elif isinstance (agent , Workflow ):
312+ return 'graph_workflow'
294313 else :
295314 return 'custom_agent'
296315
297316
298- def _get_agent_skill_name (agent : BaseAgent ) -> str :
317+ def _get_agent_skill_name (agent : BaseNode ) -> str :
299318 """Get the skill name based on agent type."""
300319 if isinstance (agent , LlmAgent ):
301320 return 'model'
302- elif isinstance (agent , (SequentialAgent , ParallelAgent , LoopAgent )):
321+ elif isinstance (agent , (SequentialAgent , ParallelAgent , LoopAgent , Workflow )):
303322 return 'workflow'
304323 else :
305324 return 'custom'
306325
307326
308- def _build_agent_description (agent : BaseAgent ) -> str :
327+ def _build_agent_description (agent : BaseNode ) -> str :
309328 """Build agent description from agent.description and workflow-specific descriptions."""
310329 description_parts = []
311330
@@ -382,9 +401,9 @@ def _replace_pronouns(text: str) -> str:
382401 )
383402
384403
385- def _get_workflow_description (agent : BaseAgent ) -> Optional [str ]:
386- """Get workflow-specific description for non-LLM agents."""
387- if not agent . sub_agents :
404+ def _get_workflow_description (agent : BaseNode ) -> Optional [str ]:
405+ """Get workflow-specific description for non-LLM agents and workflows ."""
406+ if not _iter_child_nodes ( agent ) :
388407 return None
389408
390409 if isinstance (agent , SequentialAgent ):
@@ -393,6 +412,8 @@ def _get_workflow_description(agent: BaseAgent) -> Optional[str]:
393412 return _build_parallel_description (agent )
394413 elif isinstance (agent , LoopAgent ):
395414 return _build_loop_description (agent )
415+ elif isinstance (agent , Workflow ):
416+ return _build_graph_workflow_description (agent )
396417
397418 return None
398419
@@ -448,13 +469,32 @@ def _build_loop_description(agent: LoopAgent) -> str:
448469 )
449470
450471
451- def _get_default_description (agent : BaseAgent ) -> str :
472+ def _build_graph_workflow_description (workflow : Workflow ) -> str :
473+ """Build description for a graph-based Workflow."""
474+ child_nodes = _iter_child_nodes (workflow )
475+ descriptions = []
476+ for node in child_nodes :
477+ node_description = (
478+ node .description .rstrip ('.' )
479+ if node .description
480+ else f'execute the { node .name } node'
481+ )
482+ descriptions .append (f'{ node .name } : { node_description } ' )
483+ return (
484+ 'This workflow orchestrates the following nodes: '
485+ + '; ' .join (descriptions )
486+ + '.'
487+ )
488+
489+
490+ def _get_default_description (agent : BaseNode ) -> str :
452491 """Get default description based on agent type."""
453492 agent_type_descriptions = {
454493 LlmAgent : 'An LLM-based agent' ,
455494 SequentialAgent : 'A sequential workflow agent' ,
456495 ParallelAgent : 'A parallel workflow agent' ,
457496 LoopAgent : 'A loop workflow agent' ,
497+ Workflow : 'A graph-based workflow agent' ,
458498 }
459499
460500 for agent_type , description in agent_type_descriptions .items ():
@@ -492,7 +532,7 @@ def _extract_inputs_from_examples(examples: Optional[list[dict]]) -> list[str]:
492532
493533
494534async def _extract_examples_from_agent (
495- agent : BaseAgent ,
535+ agent : BaseNode ,
496536) -> Optional [List [Dict ]]:
497537 """Extract examples from example_tool if configured; otherwise, from agent instruction."""
498538 if not isinstance (agent , LlmAgent ):
@@ -558,7 +598,7 @@ def _extract_examples_from_instruction(
558598 return examples if examples else None
559599
560600
561- def _get_input_modes (agent : BaseAgent ) -> Optional [List [str ]]:
601+ def _get_input_modes (agent : BaseNode ) -> Optional [List [str ]]:
562602 """Get input modes based on agent model."""
563603 if not isinstance (agent , LlmAgent ):
564604 return None
@@ -568,7 +608,7 @@ def _get_input_modes(agent: BaseAgent) -> Optional[List[str]]:
568608 return None
569609
570610
571- def _get_output_modes (agent : BaseAgent ) -> Optional [List [str ]]:
611+ def _get_output_modes (agent : BaseNode ) -> Optional [List [str ]]:
572612 """Get output modes from Agent.generate_content_config.response_modalities."""
573613 if not isinstance (agent , LlmAgent ):
574614 return None
0 commit comments