mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-09 08:08:32 +00:00
This commit fixes issue #3897 where the LLM would return a skill.id (e.g., 'Research') instead of the full endpoint URL, causing a Pydantic validation error. Changes: - Added resolve_agent_identifier() function to map skill IDs to endpoints - Added extract_agent_identifiers_from_cards() to collect both endpoints and skill IDs - Modified _execute_task_with_a2a() to rebuild AgentResponse model after fetching AgentCards - Updated _delegate_to_a2a() to use resolver for identifier resolution - Updated _augment_prompt_with_a2a() to explicitly instruct LLM about both identifier types - Added comprehensive unit tests for resolve_agent_identifier() - Added integration tests replicating the exact issue from #3897 The fix allows the dynamic Pydantic model to accept both endpoint URLs and skill IDs in the Literal constraint, then resolves skill IDs to their canonical endpoints before delegation. This maintains backward compatibility while fixing the validation error. Fixes #3897 Co-Authored-By: João <joao@crewai.com>
246 lines
8.2 KiB
Python
246 lines
8.2 KiB
Python
"""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
|