Skip to content
Merged
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
34 changes: 32 additions & 2 deletions src/google/adk/a2a/converters/to_adk_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,29 @@ def _parse_adk_metadata_value(value: Any) -> Any:
return value


_PEER_SETTABLE_ACTION_FIELDS = frozenset({
"escalate",
"skip_summarization",
"skipSummarization",
})
"""EventActions fields a remote A2A peer may set on the event we emit for it.

Every other field either mutates the caller's own session (state and artifact
deltas, requested auth configs and tool confirmations) or drives the caller's
control flow and persistence (agent transfer, agent state, compaction, rewind),
so it must never be rebuilt from metadata the peer controls. Serialized
metadata uses the camelCase aliases, so both spellings are listed.
"""


def _extract_event_actions(
metadata: Optional[dict[str, Any]],
) -> EventActions:
"""Extracts ADK event actions from A2A metadata."""
"""Extracts ADK event actions from A2A metadata.

The metadata is supplied by the remote peer, so only the inert fields in
``_PEER_SETTABLE_ACTION_FIELDS`` are honored; anything else is dropped.
"""
if not metadata:
return EventActions()

Expand All @@ -245,8 +264,19 @@ def _extract_event_actions(
)
return EventActions()

peer_actions = {
key: value
for key, value in parsed_actions.items()
if key in _PEER_SETTABLE_ACTION_FIELDS
}
if len(peer_actions) != len(parsed_actions):
logger.debug(
"Dropping ADK actions metadata fields that a peer may not set: %s",
sorted(set(parsed_actions) - set(peer_actions)),
)

try:
return EventActions.model_validate(parsed_actions)
return EventActions.model_validate(peer_actions)
except ValidationError as error:
logger.warning("Ignoring invalid ADK actions metadata: %s", error)
return EventActions()
Expand Down
97 changes: 7 additions & 90 deletions src/google/adk/a2a/utils/agent_card_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from __future__ import annotations

import logging
import re
from typing import Dict
from typing import List
from typing import Optional
Expand Down Expand Up @@ -108,8 +107,10 @@ async def _build_llm_agent_skills(agent: LlmAgent) -> List[AgentSkill]:
"""Build skills for LLM agent."""
skills = []

# 1. Agent skill (main model skill)
agent_description = _build_llm_agent_description_with_instructions(agent)
# 1. Agent skill (main model skill). The card is a discovery document served
# without authentication, so the description comes from the agent's own
# public description and never from its instructions.
agent_description = _build_agent_description(agent)
agent_examples = await _extract_examples_from_agent(agent)

skills.append(
Expand Down Expand Up @@ -326,62 +327,6 @@ def _build_agent_description(agent: BaseAgent) -> str:
)


def _build_llm_agent_description_with_instructions(agent: LlmAgent) -> str:
"""Build agent description including instructions for LlmAgents."""
description_parts = []

# Add agent description
if agent.description:
description_parts.append(agent.description)

# Add instruction (with pronoun replacement) - only for LlmAgent
if agent.instruction:
instruction = _replace_pronouns(agent.instruction)
description_parts.append(instruction)

# Add global instruction (with pronoun replacement) - only for LlmAgent
if agent.global_instruction:
global_instruction = _replace_pronouns(agent.global_instruction)
description_parts.append(global_instruction)

return (
' '.join(description_parts)
if description_parts
else _get_default_description(agent)
)


def _replace_pronouns(text: str) -> str:
"""Replace pronouns and conjugate common verbs for agent description.

(e.g., "You are" -> "I am", "your" -> "my").
"""
pronoun_map = {
# Longer phrases with verb conjugations
'you are': 'I am',
'you were': 'I was',
"you're": 'I am',
"you've": 'I have',
# Standalone pronouns
'yours': 'mine',
'your': 'my',
'you': 'I',
}

# Sort keys by length (descending) to ensure longer phrases are matched first.
# This prevents "you" in "you are" from being replaced on its own.
sorted_keys = sorted(pronoun_map.keys(), key=len, reverse=True)

pattern = r'\b(' + '|'.join(re.escape(key) for key in sorted_keys) + r')\b'

return re.sub(
pattern,
lambda match: pronoun_map[match.group(1).lower()],
text,
flags=re.IGNORECASE,
)


def _get_workflow_description(agent: BaseAgent) -> Optional[str]:
"""Get workflow-specific description for non-LLM agents."""
if not agent.sub_agents:
Expand Down Expand Up @@ -494,7 +439,7 @@ def _extract_inputs_from_examples(examples: Optional[list[dict]]) -> list[str]:
async def _extract_examples_from_agent(
agent: BaseAgent,
) -> Optional[List[Dict]]:
"""Extract examples from example_tool if configured; otherwise, from agent instruction."""
"""Extract examples from example_tool if configured, otherwise none."""
if not isinstance(agent, LlmAgent):
return None

Expand All @@ -507,10 +452,8 @@ async def _extract_examples_from_agent(
except Exception as e:
logger.warning('Failed to extract examples from tools: %s', e)

# If no example_tool found, try to extract examples from instruction
if agent.instruction:
return _extract_examples_from_instruction(agent.instruction)

# Examples come only from a declared example_tool, never mined out of the
# instruction, which is not publishable content.
return None


Expand All @@ -532,32 +475,6 @@ def _convert_example_tool_examples(tool: ExampleTool) -> List[Dict]:
return examples


def _extract_examples_from_instruction(
instruction: str,
) -> Optional[List[Dict]]:
"""Extract examples from agent instruction text using regex patterns."""
examples = []

# Look for common example patterns in instructions
example_patterns = [
r'Example Query:\s*["\']([^"\']+)["\']',
r'Example Response:\s*["\']([^"\']+)["\']',
r'Example:\s*["\']([^"\']+)["\']',
]

for pattern in example_patterns:
matches = re.findall(pattern, instruction, re.IGNORECASE)
if matches:
for i in range(0, len(matches), 2):
if i + 1 < len(matches):
examples.append({
'input': {'text': matches[i]},
'output': [{'text': matches[i + 1]}],
})

return examples if examples else None


def _get_input_modes(agent: BaseAgent) -> Optional[List[str]]:
"""Get input modes based on agent model."""
if not isinstance(agent, LlmAgent):
Expand Down
Loading
Loading