From 2de21a075b60eab4aed1e12203a389062fb7049a Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 7 Jan 2026 17:10:47 -0500 Subject: [PATCH] feat: generate agent card from server config or agent --- lib/crewai/src/crewai/a2a/config.py | 29 ++++++- lib/crewai/src/crewai/a2a/utils/agent_card.py | 87 ++++++++++++++----- 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/lib/crewai/src/crewai/a2a/config.py b/lib/crewai/src/crewai/a2a/config.py index 5f2ed1cbb..b0ac83405 100644 --- a/lib/crewai/src/crewai/a2a/config.py +++ b/lib/crewai/src/crewai/a2a/config.py @@ -81,7 +81,7 @@ class A2AConfig(BaseModel): ) -class A2AClientConfig(A2AConfig): +class A2AClientConfig(BaseModel): """Configuration for connecting to remote A2A agents. Attributes: @@ -99,6 +99,33 @@ class A2AClientConfig(A2AConfig): extensions: Extension URIs the client supports. """ + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + endpoint: Url = Field(description="A2A agent endpoint URL") + auth: AuthScheme | None = Field( + default=None, + description="Authentication scheme", + ) + timeout: int = Field(default=120, description="Request timeout in seconds") + max_turns: int = Field( + default=10, description="Maximum conversation turns with A2A agent" + ) + response_model: type[BaseModel] | None = Field( + default=None, + description="Optional Pydantic model for structured A2A agent responses", + ) + fail_fast: bool = Field( + default=True, + description="If True, raise error when agent unreachable; if False, skip", + ) + trust_remote_completion_status: bool = Field( + default=False, + description="If True, return A2A result directly when completed", + ) + updates: UpdateConfig = Field( + default_factory=_get_default_update_config, + description="Update mechanism config", + ) accepted_output_modes: list[str] = Field( default_factory=lambda: ["application/json"], description="Media types the client can accept in responses", diff --git a/lib/crewai/src/crewai/a2a/utils/agent_card.py b/lib/crewai/src/crewai/a2a/utils/agent_card.py index 20e621473..bd8c4db65 100644 --- a/lib/crewai/src/crewai/a2a/utils/agent_card.py +++ b/lib/crewai/src/crewai/a2a/utils/agent_card.py @@ -21,6 +21,7 @@ from crewai.a2a.auth.utils import ( configure_auth_client, retry_on_401, ) +from crewai.a2a.config import A2AServerConfig from crewai.crew import Crew @@ -30,6 +31,26 @@ if TYPE_CHECKING: from crewai.task import Task +def _get_server_config(agent: Agent) -> A2AServerConfig | None: + """Get A2AServerConfig from an agent's a2a configuration. + + Args: + agent: The Agent instance to check. + + Returns: + A2AServerConfig if present, None otherwise. + """ + if agent.a2a is None: + return None + if isinstance(agent.a2a, A2AServerConfig): + return agent.a2a + if isinstance(agent.a2a, list): + for config in agent.a2a: + if isinstance(config, A2AServerConfig): + return config + return None + + def fetch_agent_card( endpoint: str, auth: AuthScheme | None = None, @@ -298,6 +319,8 @@ def _crew_to_agent_card(crew: Crew, url: str) -> AgentCard: def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard: """Generate an A2A AgentCard from an Agent instance. + Uses A2AServerConfig values when available, falling back to agent properties. + Args: agent: The Agent instance to generate a card for. url: The base URL where this agent will be exposed. @@ -305,40 +328,53 @@ def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard: Returns: AgentCard describing the agent's capabilities. """ + server_config = _get_server_config(agent) or A2AServerConfig() + + name = server_config.name or agent.role + description_parts = [agent.goal] if agent.backstory: description_parts.append(agent.backstory) + description = server_config.description or " ".join(description_parts) - skills: list[AgentSkill] = [] - - if agent.tools: - for tool in agent.tools: - tool_name = getattr(tool, "name", None) or tool.__class__.__name__ - tool_desc = getattr(tool, "description", None) or f"Tool: {tool_name}" - skills.append(_tool_to_skill(tool_name, tool_desc)) + skills: list[AgentSkill] = ( + server_config.skills.copy() if server_config.skills else [] + ) if not skills: - skills.append( - AgentSkill( - id=agent.role.lower().replace(" ", "_"), - name=agent.role, - description=agent.goal, - tags=[agent.role.lower().replace(" ", "-")], + if agent.tools: + for tool in agent.tools: + tool_name = getattr(tool, "name", None) or tool.__class__.__name__ + tool_desc = getattr(tool, "description", None) or f"Tool: {tool_name}" + skills.append(_tool_to_skill(tool_name, tool_desc)) + + if not skills: + skills.append( + AgentSkill( + id=agent.role.lower().replace(" ", "_"), + name=agent.role, + description=agent.goal, + tags=[agent.role.lower().replace(" ", "-")], + ) ) - ) return AgentCard( - name=agent.role, - description=" ".join(description_parts), + name=name, + description=description, url=url, - version="1.0.0", - capabilities=AgentCapabilities( - streaming=True, - push_notifications=True, - ), - default_input_modes=["text/plain", "application/json"], - default_output_modes=["text/plain", "application/json"], + version=server_config.version, + capabilities=server_config.capabilities, + default_input_modes=server_config.default_input_modes, + default_output_modes=server_config.default_output_modes, skills=skills, + protocol_version=server_config.protocol_version, + provider=server_config.provider, + documentation_url=server_config.documentation_url, + icon_url=server_config.icon_url, + additional_interfaces=server_config.additional_interfaces, + security=server_config.security, + security_schemes=server_config.security_schemes, + supports_authenticated_extended_card=server_config.supports_authenticated_extended_card, ) @@ -348,9 +384,14 @@ def inject_a2a_server_methods(target: Crew | Agent) -> None: Adds a `to_agent_card(url: str) -> AgentCard` method to the target instance that generates an A2A-compliant AgentCard. + For Agents, this only injects methods if an A2AServerConfig is present. + For Crews, methods are always injected. + Args: target: The Crew or Agent instance to inject methods onto. """ + if not isinstance(target, Crew) and _get_server_config(target) is None: + return def _to_agent_card(self: Crew | Agent, url: str) -> AgentCard: if isinstance(self, Crew):