mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-16 04:18:35 +00:00
Compare commits
4 Commits
1.6.1
...
devin/1762
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea38280d7 | ||
|
|
7e154ebc16 | ||
|
|
416c2665a7 | ||
|
|
d141078e72 |
@@ -753,3 +753,99 @@ def get_a2a_agents_and_response_model(
|
||||
"""
|
||||
a2a_agents, agent_ids = extract_a2a_agent_ids_from_config(a2a_config=a2a_config)
|
||||
return a2a_agents, create_agent_response_model(agent_ids)
|
||||
|
||||
|
||||
def extract_agent_identifiers_from_cards(
|
||||
a2a_agents: list[A2AConfig],
|
||||
agent_cards: dict[str, AgentCard],
|
||||
) -> tuple[str, ...]:
|
||||
"""Extract all valid agent identifiers (endpoints and skill IDs) from agent cards.
|
||||
|
||||
Args:
|
||||
a2a_agents: List of A2A agent configurations
|
||||
agent_cards: Dictionary mapping endpoints to AgentCards
|
||||
|
||||
Returns:
|
||||
Tuple of all valid identifiers (endpoints + skill IDs)
|
||||
"""
|
||||
identifiers = set()
|
||||
|
||||
for config in a2a_agents:
|
||||
identifiers.add(config.endpoint)
|
||||
|
||||
for card in agent_cards.values():
|
||||
if card.skills:
|
||||
for skill in card.skills:
|
||||
identifiers.add(skill.id)
|
||||
|
||||
return tuple(sorted(identifiers))
|
||||
|
||||
|
||||
def resolve_agent_identifier(
|
||||
identifier: str,
|
||||
a2a_agents: list[A2AConfig],
|
||||
agent_cards: dict[str, AgentCard],
|
||||
) -> str:
|
||||
"""Resolve an agent identifier (endpoint or skill ID) to a canonical endpoint.
|
||||
|
||||
This function allows both endpoint URLs and skill IDs to be used as agent identifiers.
|
||||
If the identifier is already an endpoint, it's returned as-is. If it's a skill ID,
|
||||
it's resolved to the endpoint of the agent card that contains that skill.
|
||||
|
||||
Args:
|
||||
identifier: Either an endpoint URL or a skill ID
|
||||
a2a_agents: List of A2A agent configurations
|
||||
agent_cards: Dictionary mapping endpoints to AgentCards
|
||||
|
||||
Returns:
|
||||
The canonical endpoint URL
|
||||
|
||||
Raises:
|
||||
ValueError: If the identifier is unknown or ambiguous (matches multiple agents)
|
||||
|
||||
Examples:
|
||||
>>> # Endpoint passthrough
|
||||
>>> resolve_agent_identifier(
|
||||
... "http://localhost:10001/.well-known/agent-card.json",
|
||||
... a2a_agents,
|
||||
... agent_cards
|
||||
... )
|
||||
'http://localhost:10001/.well-known/agent-card.json'
|
||||
|
||||
>>> # Skill ID resolution
|
||||
>>> resolve_agent_identifier("Research", a2a_agents, agent_cards)
|
||||
'http://localhost:10001/.well-known/agent-card.json'
|
||||
"""
|
||||
endpoints = {config.endpoint for config in a2a_agents}
|
||||
if identifier in endpoints:
|
||||
return identifier
|
||||
|
||||
matching_endpoints: list[str] = []
|
||||
for endpoint, card in agent_cards.items():
|
||||
if card.skills:
|
||||
for skill in card.skills:
|
||||
if skill.id == identifier:
|
||||
matching_endpoints.append(endpoint)
|
||||
break
|
||||
|
||||
if len(matching_endpoints) == 0:
|
||||
available_endpoints = ", ".join(sorted(endpoints))
|
||||
available_skill_ids = []
|
||||
for card in agent_cards.values():
|
||||
if card.skills:
|
||||
available_skill_ids.extend([skill.id for skill in card.skills])
|
||||
available_skills = ", ".join(sorted(set(available_skill_ids))) if available_skill_ids else "none"
|
||||
raise ValueError(
|
||||
f"Unknown A2A agent identifier '{identifier}'. "
|
||||
f"Available endpoints: {available_endpoints}. "
|
||||
f"Available skill IDs: {available_skills}."
|
||||
)
|
||||
|
||||
if len(matching_endpoints) > 1:
|
||||
endpoints_list = ", ".join(sorted(matching_endpoints))
|
||||
raise ValueError(
|
||||
f"Ambiguous skill ID '{identifier}' found in multiple agents: {endpoints_list}. "
|
||||
f"Please use the specific endpoint URL to disambiguate."
|
||||
)
|
||||
|
||||
return matching_endpoints[0]
|
||||
|
||||
@@ -23,9 +23,12 @@ from crewai.a2a.templates import (
|
||||
)
|
||||
from crewai.a2a.types import AgentResponseProtocol
|
||||
from crewai.a2a.utils import (
|
||||
create_agent_response_model,
|
||||
execute_a2a_delegation,
|
||||
extract_agent_identifiers_from_cards,
|
||||
fetch_agent_card,
|
||||
get_a2a_agents_and_response_model,
|
||||
resolve_agent_identifier,
|
||||
)
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
@@ -190,6 +193,9 @@ def _execute_task_with_a2a(
|
||||
finally:
|
||||
task.description = original_description
|
||||
|
||||
agent_identifiers = extract_agent_identifiers_from_cards(a2a_agents, agent_cards)
|
||||
agent_response_model = create_agent_response_model(agent_identifiers)
|
||||
|
||||
task.description = _augment_prompt_with_a2a(
|
||||
a2a_agents=a2a_agents,
|
||||
task_description=original_description,
|
||||
@@ -301,6 +307,13 @@ def _augment_prompt_with_a2a(
|
||||
IMPORTANT: You have the ability to delegate this task to remote A2A agents.
|
||||
|
||||
{agents_text}
|
||||
|
||||
AGENT IDENTIFICATION: When setting a2a_ids, you may use either:
|
||||
1. The agent's endpoint URL (e.g., "http://localhost:10001/.well-known/agent-card.json")
|
||||
2. The exact skill.id from the agent's skills list (e.g., "Research")
|
||||
|
||||
Prefer using endpoint URLs when possible to avoid ambiguity. If a skill.id appears on multiple agents, you MUST use the endpoint URL to specify which agent you want.
|
||||
|
||||
{history_text}{turn_info}
|
||||
|
||||
|
||||
@@ -373,6 +386,9 @@ def _handle_agent_response_and_continue(
|
||||
if "agent_card" in a2a_result and agent_id not in agent_cards_dict:
|
||||
agent_cards_dict[agent_id] = a2a_result["agent_card"]
|
||||
|
||||
agent_identifiers = extract_agent_identifiers_from_cards(a2a_agents, agent_cards_dict)
|
||||
agent_response_model = create_agent_response_model(agent_identifiers)
|
||||
|
||||
task.description = _augment_prompt_with_a2a(
|
||||
a2a_agents=a2a_agents,
|
||||
task_description=original_task_description,
|
||||
@@ -445,16 +461,20 @@ def _delegate_to_a2a(
|
||||
ImportError: If a2a-sdk is not installed
|
||||
"""
|
||||
a2a_agents, agent_response_model = get_a2a_agents_and_response_model(self.a2a)
|
||||
agent_ids = tuple(config.endpoint for config in a2a_agents)
|
||||
current_request = str(agent_response.message)
|
||||
agent_id = agent_response.a2a_ids[0]
|
||||
agent_identifier = agent_response.a2a_ids[0]
|
||||
|
||||
if agent_id not in agent_ids:
|
||||
raise ValueError(
|
||||
f"Unknown A2A agent ID(s): {agent_response.a2a_ids} not in {agent_ids}"
|
||||
agent_cards_dict = agent_cards or {}
|
||||
try:
|
||||
agent_endpoint = resolve_agent_identifier(
|
||||
agent_identifier, a2a_agents, agent_cards_dict
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"Failed to resolve A2A agent identifier '{agent_identifier}': {e}"
|
||||
) from e
|
||||
|
||||
agent_config = next(filter(lambda x: x.endpoint == agent_id, a2a_agents))
|
||||
agent_config = next(filter(lambda x: x.endpoint == agent_endpoint, a2a_agents))
|
||||
task_config = task.config or {}
|
||||
context_id = task_config.get("context_id")
|
||||
task_id_config = task_config.get("task_id")
|
||||
@@ -488,7 +508,7 @@ def _delegate_to_a2a(
|
||||
metadata=metadata,
|
||||
extensions=extensions,
|
||||
conversation_history=conversation_history,
|
||||
agent_id=agent_id,
|
||||
agent_id=agent_endpoint,
|
||||
agent_role=Role.user,
|
||||
agent_branch=agent_branch,
|
||||
response_model=agent_config.response_model,
|
||||
@@ -501,7 +521,7 @@ def _delegate_to_a2a(
|
||||
final_result, next_request = _handle_agent_response_and_continue(
|
||||
self=self,
|
||||
a2a_result=a2a_result,
|
||||
agent_id=agent_id,
|
||||
agent_id=agent_endpoint,
|
||||
agent_cards=agent_cards,
|
||||
a2a_agents=a2a_agents,
|
||||
original_task_description=original_task_description,
|
||||
|
||||
245
lib/crewai/tests/a2a/test_resolve_agent_identifier.py
Normal file
245
lib/crewai/tests/a2a/test_resolve_agent_identifier.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""Test resolve_agent_identifier function for A2A skill ID resolution."""
|
||||
|
||||
import pytest
|
||||
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
|
||||
|
||||
from crewai.a2a.config import A2AConfig
|
||||
from crewai.a2a.utils import resolve_agent_identifier
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_agent_configs():
|
||||
"""Create sample A2A agent configurations."""
|
||||
return [
|
||||
A2AConfig(endpoint="http://localhost:10001/.well-known/agent-card.json"),
|
||||
A2AConfig(endpoint="http://localhost:10002/.well-known/agent-card.json"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_agent_cards():
|
||||
"""Create sample AgentCards with skills."""
|
||||
card1 = AgentCard(
|
||||
name="Research Agent",
|
||||
description="An expert research agent",
|
||||
url="http://localhost:10001",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Research",
|
||||
name="Research",
|
||||
description="Conduct comprehensive research",
|
||||
tags=["research", "analysis"],
|
||||
examples=["Research quantum computing"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
card2 = AgentCard(
|
||||
name="Writing Agent",
|
||||
description="An expert writing agent",
|
||||
url="http://localhost:10002",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Writing",
|
||||
name="Writing",
|
||||
description="Write high-quality content",
|
||||
tags=["writing", "content"],
|
||||
examples=["Write a blog post"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
return {
|
||||
"http://localhost:10001/.well-known/agent-card.json": card1,
|
||||
"http://localhost:10002/.well-known/agent-card.json": card2,
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_endpoint_passthrough(sample_agent_configs, sample_agent_cards):
|
||||
"""Test that endpoint URLs are returned as-is."""
|
||||
endpoint = "http://localhost:10001/.well-known/agent-card.json"
|
||||
result = resolve_agent_identifier(endpoint, sample_agent_configs, sample_agent_cards)
|
||||
assert result == endpoint
|
||||
|
||||
|
||||
def test_resolve_unique_skill_id(sample_agent_configs, sample_agent_cards):
|
||||
"""Test that a unique skill ID resolves to the correct endpoint."""
|
||||
result = resolve_agent_identifier("Research", sample_agent_configs, sample_agent_cards)
|
||||
assert result == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
result = resolve_agent_identifier("Writing", sample_agent_configs, sample_agent_cards)
|
||||
assert result == "http://localhost:10002/.well-known/agent-card.json"
|
||||
|
||||
|
||||
def test_resolve_unknown_identifier(sample_agent_configs, sample_agent_cards):
|
||||
"""Test that unknown identifiers raise a descriptive error."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve_agent_identifier("UnknownSkill", sample_agent_configs, sample_agent_cards)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Unknown A2A agent identifier 'UnknownSkill'" in error_msg
|
||||
assert "Available endpoints:" in error_msg
|
||||
assert "Available skill IDs:" in error_msg
|
||||
assert "Research" in error_msg
|
||||
assert "Writing" in error_msg
|
||||
|
||||
|
||||
def test_resolve_ambiguous_skill_id():
|
||||
"""Test that ambiguous skill IDs raise a descriptive error."""
|
||||
configs = [
|
||||
A2AConfig(endpoint="http://localhost:10001/.well-known/agent-card.json"),
|
||||
A2AConfig(endpoint="http://localhost:10002/.well-known/agent-card.json"),
|
||||
]
|
||||
|
||||
card1 = AgentCard(
|
||||
name="Research Agent 1",
|
||||
description="First research agent",
|
||||
url="http://localhost:10001",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Research",
|
||||
name="Research",
|
||||
description="Conduct research",
|
||||
tags=["research"],
|
||||
examples=["Research topic"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
card2 = AgentCard(
|
||||
name="Research Agent 2",
|
||||
description="Second research agent",
|
||||
url="http://localhost:10002",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Research",
|
||||
name="Research",
|
||||
description="Conduct research",
|
||||
tags=["research"],
|
||||
examples=["Research topic"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
cards = {
|
||||
"http://localhost:10001/.well-known/agent-card.json": card1,
|
||||
"http://localhost:10002/.well-known/agent-card.json": card2,
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve_agent_identifier("Research", configs, cards)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Ambiguous skill ID 'Research'" in error_msg
|
||||
assert "found in multiple agents" in error_msg
|
||||
assert "http://localhost:10001/.well-known/agent-card.json" in error_msg
|
||||
assert "http://localhost:10002/.well-known/agent-card.json" in error_msg
|
||||
assert "Please use the specific endpoint URL to disambiguate" in error_msg
|
||||
|
||||
|
||||
def test_resolve_with_no_skills():
|
||||
"""Test resolution when agent cards have no skills."""
|
||||
configs = [
|
||||
A2AConfig(endpoint="http://localhost:10001/.well-known/agent-card.json"),
|
||||
]
|
||||
|
||||
card = AgentCard(
|
||||
name="Agent Without Skills",
|
||||
description="An agent without skills",
|
||||
url="http://localhost:10001",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[],
|
||||
)
|
||||
|
||||
cards = {
|
||||
"http://localhost:10001/.well-known/agent-card.json": card,
|
||||
}
|
||||
|
||||
result = resolve_agent_identifier(
|
||||
"http://localhost:10001/.well-known/agent-card.json", configs, cards
|
||||
)
|
||||
assert result == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve_agent_identifier("SomeSkill", configs, cards)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Unknown A2A agent identifier 'SomeSkill'" in error_msg
|
||||
assert "Available skill IDs: none" in error_msg
|
||||
|
||||
|
||||
def test_resolve_with_multiple_skills_same_card(sample_agent_configs):
|
||||
"""Test resolution when a card has multiple skills."""
|
||||
card = AgentCard(
|
||||
name="Multi-Skill Agent",
|
||||
description="An agent with multiple skills",
|
||||
url="http://localhost:10001",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Research",
|
||||
name="Research",
|
||||
description="Conduct research",
|
||||
tags=["research"],
|
||||
examples=["Research topic"],
|
||||
),
|
||||
AgentSkill(
|
||||
id="Analysis",
|
||||
name="Analysis",
|
||||
description="Analyze data",
|
||||
tags=["analysis"],
|
||||
examples=["Analyze data"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
cards = {
|
||||
"http://localhost:10001/.well-known/agent-card.json": card,
|
||||
}
|
||||
|
||||
result1 = resolve_agent_identifier("Research", sample_agent_configs[:1], cards)
|
||||
assert result1 == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
result2 = resolve_agent_identifier("Analysis", sample_agent_configs[:1], cards)
|
||||
assert result2 == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
|
||||
def test_resolve_empty_agent_cards():
|
||||
"""Test resolution with empty agent cards dictionary."""
|
||||
configs = [
|
||||
A2AConfig(endpoint="http://localhost:10001/.well-known/agent-card.json"),
|
||||
]
|
||||
cards = {}
|
||||
|
||||
result = resolve_agent_identifier(
|
||||
"http://localhost:10001/.well-known/agent-card.json", configs, cards
|
||||
)
|
||||
assert result == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve_agent_identifier("SomeSkill", configs, cards)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Unknown A2A agent identifier 'SomeSkill'" in error_msg
|
||||
287
lib/crewai/tests/a2a/test_skill_id_integration.py
Normal file
287
lib/crewai/tests/a2a/test_skill_id_integration.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""Integration test for A2A skill ID resolution (issue #3897)."""
|
||||
|
||||
import pytest
|
||||
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.a2a.config import A2AConfig
|
||||
from crewai.a2a.utils import (
|
||||
create_agent_response_model,
|
||||
extract_agent_identifiers_from_cards,
|
||||
resolve_agent_identifier,
|
||||
)
|
||||
|
||||
|
||||
def test_skill_id_resolution_integration():
|
||||
"""Test the complete flow of skill ID resolution as described in issue #3897.
|
||||
|
||||
This test replicates the exact scenario from the bug report:
|
||||
1. User creates A2A config with endpoint URL
|
||||
2. Remote agent has AgentCard with skill.id="Research"
|
||||
3. LLM returns a2a_ids=["Research"] instead of the endpoint URL
|
||||
4. System should resolve "Research" to the endpoint and proceed successfully
|
||||
"""
|
||||
a2a_config = A2AConfig(
|
||||
endpoint="http://localhost:10001/.well-known/agent-card.json"
|
||||
)
|
||||
a2a_agents = [a2a_config]
|
||||
|
||||
agent_card = AgentCard(
|
||||
name="Research Agent",
|
||||
description="An expert research agent that can conduct thorough research",
|
||||
url="http://localhost:10001",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Research",
|
||||
name="Research",
|
||||
description="Conduct comprehensive research on any topic",
|
||||
tags=["research", "analysis", "information-gathering"],
|
||||
examples=[
|
||||
"Research the latest developments in quantum computing",
|
||||
"What are the current trends in renewable energy?",
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
agent_cards = {
|
||||
"http://localhost:10001/.well-known/agent-card.json": agent_card
|
||||
}
|
||||
|
||||
identifiers = extract_agent_identifiers_from_cards(a2a_agents, agent_cards)
|
||||
|
||||
assert "http://localhost:10001/.well-known/agent-card.json" in identifiers
|
||||
assert "Research" in identifiers
|
||||
|
||||
agent_response_model = create_agent_response_model(identifiers)
|
||||
|
||||
agent_response_data = {
|
||||
"a2a_ids": ["Research"], # LLM uses skill ID instead of endpoint
|
||||
"message": "Please research quantum computing developments",
|
||||
"is_a2a": True,
|
||||
}
|
||||
|
||||
agent_response = agent_response_model.model_validate(agent_response_data)
|
||||
assert agent_response.a2a_ids == ("Research",)
|
||||
assert agent_response.message == "Please research quantum computing developments"
|
||||
assert agent_response.is_a2a is True
|
||||
|
||||
resolved_endpoint = resolve_agent_identifier(
|
||||
"Research", a2a_agents, agent_cards
|
||||
)
|
||||
assert resolved_endpoint == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
resolved_endpoint_direct = resolve_agent_identifier(
|
||||
"http://localhost:10001/.well-known/agent-card.json",
|
||||
a2a_agents,
|
||||
agent_cards,
|
||||
)
|
||||
assert resolved_endpoint_direct == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
|
||||
def test_skill_id_validation_error_before_fix():
|
||||
"""Test that demonstrates the original bug (for documentation purposes).
|
||||
|
||||
Before the fix, creating an AgentResponse model with only endpoints
|
||||
would cause a validation error when the LLM returned a skill ID.
|
||||
"""
|
||||
endpoints_only = ("http://localhost:10001/.well-known/agent-card.json",)
|
||||
agent_response_model_old = create_agent_response_model(endpoints_only)
|
||||
|
||||
agent_response_data = {
|
||||
"a2a_ids": ["Research"],
|
||||
"message": "Please research quantum computing",
|
||||
"is_a2a": True,
|
||||
}
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
agent_response_model_old.model_validate(agent_response_data)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "validation error" in error_msg.lower() or "literal" in error_msg.lower()
|
||||
|
||||
|
||||
def test_multiple_agents_with_unique_skill_ids():
|
||||
"""Test that multiple agents with unique skill IDs work correctly."""
|
||||
a2a_agents = [
|
||||
A2AConfig(endpoint="http://localhost:10001/.well-known/agent-card.json"),
|
||||
A2AConfig(endpoint="http://localhost:10002/.well-known/agent-card.json"),
|
||||
]
|
||||
|
||||
card1 = AgentCard(
|
||||
name="Research Agent",
|
||||
description="Research agent",
|
||||
url="http://localhost:10001",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Research",
|
||||
name="Research",
|
||||
description="Conduct research",
|
||||
tags=["research"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
card2 = AgentCard(
|
||||
name="Writing Agent",
|
||||
description="Writing agent",
|
||||
url="http://localhost:10002",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Writing",
|
||||
name="Writing",
|
||||
description="Write content",
|
||||
tags=["writing"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
agent_cards = {
|
||||
"http://localhost:10001/.well-known/agent-card.json": card1,
|
||||
"http://localhost:10002/.well-known/agent-card.json": card2,
|
||||
}
|
||||
|
||||
identifiers = extract_agent_identifiers_from_cards(a2a_agents, agent_cards)
|
||||
|
||||
assert len(identifiers) == 4
|
||||
assert "http://localhost:10001/.well-known/agent-card.json" in identifiers
|
||||
assert "http://localhost:10002/.well-known/agent-card.json" in identifiers
|
||||
assert "Research" in identifiers
|
||||
assert "Writing" in identifiers
|
||||
|
||||
agent_response_model = create_agent_response_model(identifiers)
|
||||
|
||||
response1 = agent_response_model.model_validate({
|
||||
"a2a_ids": ["Research"],
|
||||
"message": "Do research",
|
||||
"is_a2a": True,
|
||||
})
|
||||
assert response1.a2a_ids == ("Research",)
|
||||
|
||||
response2 = agent_response_model.model_validate({
|
||||
"a2a_ids": ["Writing"],
|
||||
"message": "Write content",
|
||||
"is_a2a": True,
|
||||
})
|
||||
assert response2.a2a_ids == ("Writing",)
|
||||
|
||||
endpoint1 = resolve_agent_identifier("Research", a2a_agents, agent_cards)
|
||||
assert endpoint1 == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
endpoint2 = resolve_agent_identifier("Writing", a2a_agents, agent_cards)
|
||||
assert endpoint2 == "http://localhost:10002/.well-known/agent-card.json"
|
||||
|
||||
|
||||
def test_multi_turn_skill_id_resolution():
|
||||
"""Test that skill IDs work in multi-turn A2A conversations.
|
||||
|
||||
This test verifies the fix in _handle_agent_response_and_continue()
|
||||
that rebuilds the AgentResponse model with both endpoints and skill IDs
|
||||
for subsequent turns in multi-turn conversations.
|
||||
|
||||
Scenario:
|
||||
1. First turn: LLM returns skill ID "Research"
|
||||
2. A2A agent responds
|
||||
3. Second turn: LLM returns skill ID "Writing" (different agent)
|
||||
4. Both turns should accept skill IDs without validation errors
|
||||
"""
|
||||
a2a_agents = [
|
||||
A2AConfig(endpoint="http://localhost:10001/.well-known/agent-card.json"),
|
||||
A2AConfig(endpoint="http://localhost:10002/.well-known/agent-card.json"),
|
||||
]
|
||||
|
||||
card1 = AgentCard(
|
||||
name="Research Agent",
|
||||
description="Research agent",
|
||||
url="http://localhost:10001",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Research",
|
||||
name="Research",
|
||||
description="Conduct research",
|
||||
tags=["research"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
card2 = AgentCard(
|
||||
name="Writing Agent",
|
||||
description="Writing agent",
|
||||
url="http://localhost:10002",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(),
|
||||
default_input_modes=["text/plain"],
|
||||
default_output_modes=["text/plain"],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="Writing",
|
||||
name="Writing",
|
||||
description="Write content",
|
||||
tags=["writing"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
agent_cards_turn1 = {
|
||||
"http://localhost:10001/.well-known/agent-card.json": card1,
|
||||
}
|
||||
|
||||
identifiers_turn1 = extract_agent_identifiers_from_cards(a2a_agents, agent_cards_turn1)
|
||||
model_turn1 = create_agent_response_model(identifiers_turn1)
|
||||
|
||||
response_turn1 = model_turn1.model_validate({
|
||||
"a2a_ids": ["Research"],
|
||||
"message": "Please research quantum computing",
|
||||
"is_a2a": True,
|
||||
})
|
||||
assert response_turn1.a2a_ids == ("Research",)
|
||||
|
||||
endpoint_turn1 = resolve_agent_identifier("Research", a2a_agents, agent_cards_turn1)
|
||||
assert endpoint_turn1 == "http://localhost:10001/.well-known/agent-card.json"
|
||||
|
||||
agent_cards_turn2 = {
|
||||
"http://localhost:10001/.well-known/agent-card.json": card1,
|
||||
"http://localhost:10002/.well-known/agent-card.json": card2,
|
||||
}
|
||||
|
||||
identifiers_turn2 = extract_agent_identifiers_from_cards(a2a_agents, agent_cards_turn2)
|
||||
model_turn2 = create_agent_response_model(identifiers_turn2)
|
||||
|
||||
assert "Research" in identifiers_turn2
|
||||
assert "Writing" in identifiers_turn2
|
||||
|
||||
response_turn2 = model_turn2.model_validate({
|
||||
"a2a_ids": ["Writing"],
|
||||
"message": "Now write a report based on the research",
|
||||
"is_a2a": True,
|
||||
})
|
||||
assert response_turn2.a2a_ids == ("Writing",)
|
||||
|
||||
endpoint_turn2 = resolve_agent_identifier("Writing", a2a_agents, agent_cards_turn2)
|
||||
assert endpoint_turn2 == "http://localhost:10002/.well-known/agent-card.json"
|
||||
|
||||
response_turn3 = model_turn2.model_validate({
|
||||
"a2a_ids": ["Research"],
|
||||
"message": "Research more details",
|
||||
"is_a2a": True,
|
||||
})
|
||||
assert response_turn3.a2a_ids == ("Research",)
|
||||
|
||||
endpoint_turn3 = resolve_agent_identifier("Research", a2a_agents, agent_cards_turn2)
|
||||
assert endpoint_turn3 == "http://localhost:10001/.well-known/agent-card.json"
|
||||
Reference in New Issue
Block a user