refactor: extract a2a module into standalone workspace package

Move lib/crewai/src/crewai/a2a to lib/crewai-a2a/src/crewai_a2a as a
separate workspace member. crewai's `[a2a]` optional extra now pulls in
crewai-a2a instead of listing raw deps. All imports updated from
crewai.a2a.* to crewai_a2a.*. Handle PydanticUndefinedAnnotation in
crewai/__init__.py model_rebuild to break circular import at load time.
This commit is contained in:
Greyson LaLonde
2026-04-02 02:32:14 +08:00
parent 258f31d44c
commit 2ada20e9c6
55 changed files with 357 additions and 305 deletions

0
lib/crewai-a2a/README.md Normal file
View File

View File

@@ -0,0 +1,29 @@
[project]
name = "crewai-a2a"
dynamic = ["version"]
description = "Agent-to-Agent (A2A) protocol communication for CrewAI"
readme = "README.md"
authors = [
{ name = "Joao Moura", email = "joao@crewai.com" }
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai",
"a2a-sdk~=0.3.10",
"httpx-auth~=0.23.1",
"httpx-sse~=0.4.0",
"aiocache[redis,memcached]~=0.12.3",
"pydantic~=2.11.9",
"pyjwt>=2.9.0,<3",
"httpx~=0.28.1",
]
[tool.uv.sources]
crewai = { workspace = true }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/crewai_a2a/__init__.py"

View File

@@ -0,0 +1,12 @@
"""Agent-to-Agent (A2A) protocol communication for CrewAI."""
__version__ = "0.1.0"
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
__all__ = [
"A2AClientConfig",
"A2AConfig",
"A2AServerConfig",
]

View File

@@ -1,6 +1,6 @@
"""A2A authentication schemas."""
from crewai.a2a.auth.client_schemes import (
from crewai_a2a.auth.client_schemes import (
APIKeyAuth,
AuthScheme,
BearerTokenAuth,
@@ -11,7 +11,7 @@ from crewai.a2a.auth.client_schemes import (
OAuth2ClientCredentials,
TLSConfig,
)
from crewai.a2a.auth.server_schemes import (
from crewai_a2a.auth.server_schemes import (
AuthenticatedUser,
EnterpriseTokenAuth,
OIDCAuth,

View File

@@ -0,0 +1,71 @@
"""Deprecated: Authentication schemes for A2A protocol agents.
This module is deprecated. Import from crewai_a2a.auth instead:
- crewai_a2a.auth.ClientAuthScheme (replaces AuthScheme)
- crewai_a2a.auth.BearerTokenAuth
- crewai_a2a.auth.HTTPBasicAuth
- crewai_a2a.auth.HTTPDigestAuth
- crewai_a2a.auth.APIKeyAuth
- crewai_a2a.auth.OAuth2ClientCredentials
- crewai_a2a.auth.OAuth2AuthorizationCode
"""
from __future__ import annotations
from typing_extensions import deprecated
from crewai_a2a.auth.client_schemes import (
APIKeyAuth as _APIKeyAuth,
BearerTokenAuth as _BearerTokenAuth,
ClientAuthScheme as _ClientAuthScheme,
HTTPBasicAuth as _HTTPBasicAuth,
HTTPDigestAuth as _HTTPDigestAuth,
OAuth2AuthorizationCode as _OAuth2AuthorizationCode,
OAuth2ClientCredentials as _OAuth2ClientCredentials,
)
@deprecated("Use ClientAuthScheme from crewai_a2a.auth instead", category=FutureWarning)
class AuthScheme(_ClientAuthScheme):
"""Deprecated: Use ClientAuthScheme from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class BearerTokenAuth(_BearerTokenAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class HTTPBasicAuth(_HTTPBasicAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class HTTPDigestAuth(_HTTPDigestAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class APIKeyAuth(_APIKeyAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class OAuth2ClientCredentials(_OAuth2ClientCredentials):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class OAuth2AuthorizationCode(_OAuth2AuthorizationCode):
"""Deprecated: Import from crewai_a2a.auth instead."""
__all__ = [
"APIKeyAuth",
"AuthScheme",
"BearerTokenAuth",
"HTTPBasicAuth",
"HTTPDigestAuth",
"OAuth2AuthorizationCode",
"OAuth2ClientCredentials",
]

View File

@@ -42,7 +42,10 @@ logger = logging.getLogger(__name__)
try:
from fastapi import HTTPException, status as http_status
from fastapi import ( # type: ignore[import-not-found]
HTTPException,
status as http_status,
)
HTTP_401_UNAUTHORIZED = http_status.HTTP_401_UNAUTHORIZED
HTTP_500_INTERNAL_SERVER_ERROR = http_status.HTTP_500_INTERNAL_SERVER_ERROR

View File

@@ -20,7 +20,7 @@ from a2a.types import (
)
from httpx import AsyncClient, Response
from crewai.a2a.auth.client_schemes import (
from crewai_a2a.auth.client_schemes import (
APIKeyAuth,
BearerTokenAuth,
ClientAuthScheme,

View File

@@ -20,10 +20,10 @@ from pydantic import (
)
from typing_extensions import Self, deprecated
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.a2a.auth.server_schemes import ServerAuthScheme
from crewai.a2a.extensions.base import ValidatedA2AExtension
from crewai.a2a.types import ProtocolVersion, TransportType, Url
from crewai_a2a.auth.client_schemes import ClientAuthScheme
from crewai_a2a.auth.server_schemes import ServerAuthScheme
from crewai_a2a.extensions.base import ValidatedA2AExtension
from crewai_a2a.types import ProtocolVersion, TransportType, Url
try:
@@ -36,8 +36,8 @@ try:
SecurityScheme,
)
from crewai.a2a.extensions.server import ServerExtension
from crewai.a2a.updates import UpdateConfig
from crewai_a2a.extensions.server import ServerExtension
from crewai_a2a.updates import UpdateConfig
except ImportError:
UpdateConfig: Any = Any # type: ignore[no-redef]
AgentCapabilities: Any = Any # type: ignore[no-redef]
@@ -50,7 +50,7 @@ except ImportError:
def _get_default_update_config() -> UpdateConfig:
from crewai.a2a.updates import StreamingConfig
from crewai_a2a.updates import StreamingConfig
return StreamingConfig()
@@ -360,8 +360,8 @@ class ClientTransportConfig(BaseModel):
@deprecated(
"""
`crewai.a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0,
use `crewai.a2a.config.A2AClientConfig` or `crewai.a2a.config.A2AServerConfig` instead.
`crewai_a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0,
use `crewai_a2a.config.A2AClientConfig` or `crewai_a2a.config.A2AServerConfig` instead.
""",
category=FutureWarning,
)

View File

@@ -13,13 +13,13 @@ via the X-A2A-Extensions header.
See: https://a2a-protocol.org/latest/topics/extensions/
"""
from crewai.a2a.extensions.base import (
from crewai_a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
ValidatedA2AExtension,
)
from crewai.a2a.extensions.server import (
from crewai_a2a.extensions.server import (
ExtensionContext,
ServerExtension,
ServerExtensionRegistry,

View File

@@ -19,7 +19,6 @@ from pydantic import BeforeValidator
if TYPE_CHECKING:
from a2a.types import Message
from crewai.agent.core import Agent

View File

@@ -18,8 +18,8 @@ from a2a.extensions.common import (
)
from a2a.types import AgentCard, AgentExtension
from crewai.a2a.config import A2AClientConfig, A2AConfig
from crewai.a2a.extensions.base import ExtensionRegistry
from crewai_a2a.config import A2AClientConfig, A2AConfig
from crewai_a2a.extensions.base import ExtensionRegistry
def get_extensions_from_config(

View File

@@ -18,13 +18,12 @@ from a2a.types import (
TaskStatusUpdateEvent,
TextPart,
)
from typing_extensions import NotRequired
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import NotRequired
if TYPE_CHECKING:

View File

@@ -16,7 +16,7 @@ from typing_extensions import NotRequired
try:
from crewai.a2a.updates import (
from crewai_a2a.updates import (
PollingConfig,
PollingHandler,
PushNotificationConfig,

View File

@@ -1,6 +1,6 @@
"""A2A update mechanism configuration types."""
from crewai.a2a.updates.base import (
from crewai_a2a.updates.base import (
BaseHandlerKwargs,
PollingHandlerKwargs,
PushNotificationHandlerKwargs,
@@ -8,12 +8,12 @@ from crewai.a2a.updates.base import (
StreamingHandlerKwargs,
UpdateHandler,
)
from crewai.a2a.updates.polling.config import PollingConfig
from crewai.a2a.updates.polling.handler import PollingHandler
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai.a2a.updates.streaming.config import StreamingConfig
from crewai.a2a.updates.streaming.handler import StreamingHandler
from crewai_a2a.updates.polling.config import PollingConfig
from crewai_a2a.updates.polling.handler import PollingHandler
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai_a2a.updates.streaming.config import StreamingConfig
from crewai_a2a.updates.streaming.handler import StreamingHandler
UpdateConfig = PollingConfig | StreamingConfig | PushNotificationConfig

View File

@@ -28,8 +28,8 @@ if TYPE_CHECKING:
from a2a.client import Client
from a2a.types import AgentCard, Message, Task
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
class BaseHandlerKwargs(TypedDict, total=False):

View File

@@ -18,17 +18,6 @@ from a2a.types import (
TaskState,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.errors import A2APollingTimeoutError
from crewai.a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai.a2a.updates.base import PollingHandlerKwargs
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
@@ -36,6 +25,17 @@ from crewai.events.types.a2a_events import (
A2APollingStatusEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.errors import A2APollingTimeoutError
from crewai_a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai_a2a.updates.base import PollingHandlerKwargs
if TYPE_CHECKING:

View File

@@ -7,8 +7,8 @@ from typing import Annotated
from a2a.types import PushNotificationAuthenticationInfo
from pydantic import AnyHttpUrl, BaseModel, BeforeValidator, Field
from crewai.a2a.updates.base import PushNotificationResultStore
from crewai.a2a.updates.push_notifications.signature import WebhookSignatureConfig
from crewai_a2a.updates.base import PushNotificationResultStore
from crewai_a2a.updates.push_notifications.signature import WebhookSignatureConfig
def _coerce_signature(

View File

@@ -16,19 +16,6 @@ from a2a.types import (
TaskState,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.task_helpers import (
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai.a2a.updates.base import (
CommonParams,
PushNotificationHandlerKwargs,
PushNotificationResultStore,
extract_common_params,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
@@ -36,6 +23,19 @@ from crewai.events.types.a2a_events import (
A2APushNotificationTimeoutEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.task_helpers import (
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai_a2a.updates.base import (
CommonParams,
PushNotificationHandlerKwargs,
PushNotificationResultStore,
extract_common_params,
)
if TYPE_CHECKING:

View File

@@ -22,18 +22,6 @@ from a2a.types import (
TaskStatusUpdateEvent,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
)
from crewai.a2a.updates.base import StreamingHandlerKwargs, extract_common_params
from crewai.a2a.updates.streaming.params import (
process_status_update,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AArtifactReceivedEvent,
@@ -42,6 +30,18 @@ from crewai.events.types.a2a_events import (
A2AStreamingChunkEvent,
A2AStreamingStartedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
)
from crewai_a2a.updates.base import StreamingHandlerKwargs, extract_common_params
from crewai_a2a.updates.streaming.params import (
process_status_update,
)
logger = logging.getLogger(__name__)

View File

@@ -16,15 +16,6 @@ from a2a.client.errors import A2AClientHTTPError
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from aiocache import cached # type: ignore[import-untyped]
from aiocache.serializers import PickleSerializer # type: ignore[import-untyped]
import httpx
from crewai.a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai.a2a.auth.utils import (
_auth_store,
configure_auth_client,
retry_on_401,
)
from crewai.a2a.config import A2AServerConfig
from crewai.crew import Crew
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
@@ -32,13 +23,23 @@ from crewai.events.types.a2a_events import (
A2AAuthenticationFailedEvent,
A2AConnectionErrorEvent,
)
import httpx
from crewai_a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai_a2a.auth.utils import (
_auth_store,
configure_auth_client,
retry_on_401,
)
from crewai_a2a.config import A2AServerConfig
if TYPE_CHECKING:
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.agent import Agent
from crewai.task import Task
from crewai_a2a.auth.client_schemes import ClientAuthScheme
def _get_tls_verify(auth: ClientAuthScheme | None) -> ssl.SSLContext | bool | str:
"""Get TLS verify parameter from auth scheme.
@@ -495,7 +496,7 @@ def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard:
Returns:
AgentCard describing the agent's capabilities.
"""
from crewai.a2a.utils.agent_card_signing import sign_agent_card
from crewai_a2a.utils.agent_card_signing import sign_agent_card
server_config = _get_server_config(agent) or A2AServerConfig()
@@ -529,7 +530,7 @@ def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard:
capabilities = server_config.capabilities
if server_config.server_extensions:
from crewai.a2a.extensions.server import ServerExtensionRegistry
from crewai_a2a.extensions.server import ServerExtensionRegistry
registry = ServerExtensionRegistry(server_config.server_extensions)
ext_list = registry.get_agent_extensions()

View File

@@ -5,7 +5,7 @@ JSON Web Signatures (JWS) as per RFC 7515. Signed agent cards allow clients
to verify the authenticity and integrity of agent card information.
Example:
>>> from crewai.a2a.utils.agent_card_signing import sign_agent_card
>>> from crewai_a2a.utils.agent_card_signing import sign_agent_card
>>> signature = sign_agent_card(agent_card, private_key_pem, key_id="key-1")
>>> card_with_sig = card.model_copy(update={"signatures": [signature]})
"""

View File

@@ -10,7 +10,6 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Final, Literal, cast
from a2a.types import Part
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2AContentTypeNegotiatedEvent

View File

@@ -23,48 +23,6 @@ from a2a.types import (
Role,
TextPart,
)
import httpx
from pydantic import BaseModel
from crewai.a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai.a2a.auth.utils import (
_auth_store,
configure_auth_client,
validate_auth_against_agent_card,
)
from crewai.a2a.config import ClientTransportConfig, GRPCClientConfig
from crewai.a2a.extensions.registry import (
ExtensionsMiddleware,
validate_required_extensions,
)
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.types import (
HANDLER_REGISTRY,
HandlerType,
PartsDict,
PartsMetadataDict,
TransportType,
)
from crewai.a2a.updates import (
PollingConfig,
PushNotificationConfig,
StreamingHandler,
UpdateConfig,
)
from crewai.a2a.utils.agent_card import (
_afetch_agent_card_cached,
_get_tls_verify,
_prepare_auth_headers,
)
from crewai.a2a.utils.content_type import (
DEFAULT_CLIENT_OUTPUT_MODES,
negotiate_content_types,
)
from crewai.a2a.utils.transport import (
NegotiatedTransport,
TransportNegotiationError,
negotiate_transport,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConversationStartedEvent,
@@ -72,6 +30,48 @@ from crewai.events.types.a2a_events import (
A2ADelegationStartedEvent,
A2AMessageSentEvent,
)
import httpx
from pydantic import BaseModel
from crewai_a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai_a2a.auth.utils import (
_auth_store,
configure_auth_client,
validate_auth_against_agent_card,
)
from crewai_a2a.config import ClientTransportConfig, GRPCClientConfig
from crewai_a2a.extensions.registry import (
ExtensionsMiddleware,
validate_required_extensions,
)
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.types import (
HANDLER_REGISTRY,
HandlerType,
PartsDict,
PartsMetadataDict,
TransportType,
)
from crewai_a2a.updates import (
PollingConfig,
PushNotificationConfig,
StreamingHandler,
UpdateConfig,
)
from crewai_a2a.utils.agent_card import (
_afetch_agent_card_cached,
_get_tls_verify,
_prepare_auth_headers,
)
from crewai_a2a.utils.content_type import (
DEFAULT_CLIENT_OUTPUT_MODES,
negotiate_content_types,
)
from crewai_a2a.utils.transport import (
NegotiatedTransport,
TransportNegotiationError,
negotiate_transport,
)
logger = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from a2a.types import Message
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai_a2a.auth.client_schemes import ClientAuthScheme
_DEFAULT_TRANSPORT: Final[TransportType] = "JSONRPC"
@@ -771,7 +771,7 @@ def _create_grpc_channel_factory(
auth_metadata: list[tuple[str, str]] = []
if auth is not None:
from crewai.a2a.auth.client_schemes import (
from crewai_a2a.auth.client_schemes import (
APIKeyAuth,
BearerTokenAuth,
HTTPBasicAuth,

View File

@@ -103,7 +103,7 @@ class LogContext:
_log_context.reset(self._token)
def configure_json_logging(logger_name: str = "crewai.a2a") -> None:
def configure_json_logging(logger_name: str = "crewai_a2a") -> None:
"""Configure JSON logging for the A2A module.
Args:

View File

@@ -4,10 +4,10 @@ from __future__ import annotations
from typing import TypeAlias
from crewai.types.utils import create_literals_from_strings
from pydantic import BaseModel, Field, create_model
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai.types.utils import create_literals_from_strings
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
A2AConfigTypes: TypeAlias = A2AConfig | A2AServerConfig | A2AClientConfig

View File

@@ -37,10 +37,6 @@ from a2a.utils import (
)
from a2a.utils.errors import ServerError
from aiocache import SimpleMemoryCache, caches # type: ignore[import-untyped]
from pydantic import BaseModel
from crewai.a2a.utils.agent_card import _get_server_config
from crewai.a2a.utils.content_type import validate_message_parts
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AServerTaskCanceledEvent,
@@ -50,12 +46,17 @@ from crewai.events.types.a2a_events import (
)
from crewai.task import Task
from crewai.utilities.pydantic_schema_utils import create_model_from_schema
from pydantic import BaseModel
from crewai_a2a.utils.agent_card import _get_server_config
from crewai_a2a.utils.content_type import validate_message_parts
if TYPE_CHECKING:
from crewai.a2a.extensions.server import ExtensionContext, ServerExtensionRegistry
from crewai.agent import Agent
from crewai_a2a.extensions.server import ExtensionContext, ServerExtensionRegistry
logger = logging.getLogger(__name__)

View File

@@ -11,7 +11,6 @@ from dataclasses import dataclass
from typing import Final, Literal
from a2a.types import AgentCard, AgentInterface
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2ATransportNegotiatedEvent

View File

@@ -15,33 +15,6 @@ from types import MethodType
from typing import TYPE_CHECKING, Any, NamedTuple
from a2a.types import Role, TaskState
from pydantic import BaseModel, ValidationError
from crewai.a2a.config import A2AClientConfig, A2AConfig
from crewai.a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
)
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.templates import (
AVAILABLE_AGENTS_TEMPLATE,
CONVERSATION_TURN_INFO_TEMPLATE,
PREVIOUS_A2A_CONVERSATION_TEMPLATE,
REMOTE_AGENT_RESPONSE_NOTICE,
UNAVAILABLE_AGENTS_NOTICE_TEMPLATE,
)
from crewai.a2a.types import AgentResponseProtocol
from crewai.a2a.utils.agent_card import (
afetch_agent_card,
fetch_agent_card,
inject_a2a_server_methods,
)
from crewai.a2a.utils.delegation import (
aexecute_a2a_delegation,
execute_a2a_delegation,
)
from crewai.a2a.utils.response_model import get_a2a_agents_and_response_model
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConversationCompletedEvent,
@@ -49,11 +22,37 @@ from crewai.events.types.a2a_events import (
)
from crewai.lite_agent_output import LiteAgentOutput
from crewai.task import Task
from pydantic import BaseModel, ValidationError
from crewai_a2a.config import A2AClientConfig, A2AConfig
from crewai_a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
)
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.templates import (
AVAILABLE_AGENTS_TEMPLATE,
CONVERSATION_TURN_INFO_TEMPLATE,
PREVIOUS_A2A_CONVERSATION_TEMPLATE,
REMOTE_AGENT_RESPONSE_NOTICE,
UNAVAILABLE_AGENTS_NOTICE_TEMPLATE,
)
from crewai_a2a.types import AgentResponseProtocol
from crewai_a2a.utils.agent_card import (
afetch_agent_card,
fetch_agent_card,
inject_a2a_server_methods,
)
from crewai_a2a.utils.delegation import (
aexecute_a2a_delegation,
execute_a2a_delegation,
)
from crewai_a2a.utils.response_model import get_a2a_agents_and_response_model
if TYPE_CHECKING:
from a2a.types import AgentCard, Message
from crewai.agent.core import Agent
from crewai.tools.base_tool import BaseTool

View File

@@ -98,10 +98,7 @@ anthropic = [
"anthropic~=0.73.0",
]
a2a = [
"a2a-sdk~=0.3.10",
"httpx-auth~=0.23.1",
"httpx-sse~=0.4.0",
"aiocache[redis,memcached]~=0.12.3",
"crewai-a2a",
]
file-processing = [
"crewai-files",
@@ -136,6 +133,7 @@ torchvision = [
{ index = "pytorch", marker = "python_version < '3.13'" },
]
crewai-files = { workspace = true }
crewai-a2a = { workspace = true }
[build-system]

View File

@@ -4,7 +4,7 @@ from typing import Any
import urllib.request
import warnings
from pydantic import PydanticUserError
from pydantic import PydanticUndefinedAnnotation, PydanticUserError
from crewai.agent.core import Agent
from crewai.agent.planning_config import PlanningConfig
@@ -119,11 +119,11 @@ try:
"ToolResult": _ToolResult,
},
)
except (ImportError, PydanticUserError):
except (ImportError, PydanticUserError, PydanticUndefinedAnnotation):
import logging as _logging
_logging.getLogger(__name__).warning(
"AgentExecutor.model_rebuild() failed; forward refs may be unresolved.",
_logging.getLogger(__name__).debug(
"AgentExecutor.model_rebuild() deferred; forward refs may be unresolved.",
exc_info=True,
)

View File

@@ -1,10 +0,0 @@
"""Agent-to-Agent (A2A) protocol communication module for CrewAI."""
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
__all__ = [
"A2AClientConfig",
"A2AConfig",
"A2AServerConfig",
]

View File

@@ -1,71 +0,0 @@
"""Deprecated: Authentication schemes for A2A protocol agents.
This module is deprecated. Import from crewai.a2a.auth instead:
- crewai.a2a.auth.ClientAuthScheme (replaces AuthScheme)
- crewai.a2a.auth.BearerTokenAuth
- crewai.a2a.auth.HTTPBasicAuth
- crewai.a2a.auth.HTTPDigestAuth
- crewai.a2a.auth.APIKeyAuth
- crewai.a2a.auth.OAuth2ClientCredentials
- crewai.a2a.auth.OAuth2AuthorizationCode
"""
from __future__ import annotations
from typing_extensions import deprecated
from crewai.a2a.auth.client_schemes import (
APIKeyAuth as _APIKeyAuth,
BearerTokenAuth as _BearerTokenAuth,
ClientAuthScheme as _ClientAuthScheme,
HTTPBasicAuth as _HTTPBasicAuth,
HTTPDigestAuth as _HTTPDigestAuth,
OAuth2AuthorizationCode as _OAuth2AuthorizationCode,
OAuth2ClientCredentials as _OAuth2ClientCredentials,
)
@deprecated("Use ClientAuthScheme from crewai.a2a.auth instead", category=FutureWarning)
class AuthScheme(_ClientAuthScheme):
"""Deprecated: Use ClientAuthScheme from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class BearerTokenAuth(_BearerTokenAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class HTTPBasicAuth(_HTTPBasicAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class HTTPDigestAuth(_HTTPDigestAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class APIKeyAuth(_APIKeyAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class OAuth2ClientCredentials(_OAuth2ClientCredentials):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class OAuth2AuthorizationCode(_OAuth2AuthorizationCode):
"""Deprecated: Import from crewai.a2a.auth instead."""
__all__ = [
"APIKeyAuth",
"AuthScheme",
"BearerTokenAuth",
"HTTPBasicAuth",
"HTTPDigestAuth",
"OAuth2AuthorizationCode",
"OAuth2ClientCredentials",
]

View File

@@ -102,16 +102,16 @@ from crewai.utilities.training_handler import CrewTrainingHandler
try:
from crewai.a2a.types import AgentResponseProtocol
from crewai_a2a.types import AgentResponseProtocol
except ImportError:
AgentResponseProtocol = None # type: ignore[assignment, misc]
if TYPE_CHECKING:
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai_files import FileInput
from crewai_tools import CodeInterpreterTool
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai.agents.agent_builder.base_agent import PlatformAppOrAction
from crewai.task import Task
from crewai.tools.base_tool import BaseTool
@@ -1790,7 +1790,7 @@ class Agent(BaseAgent):
try:
from crewai.a2a.config import (
from crewai_a2a.config import (
A2AClientConfig as _A2AClientConfig,
A2AConfig as _A2AConfig,
A2AServerConfig as _A2AServerConfig,

View File

@@ -58,10 +58,10 @@ class AgentMeta(ModelMetaclass):
a2a_value = getattr(self, "a2a", None)
if a2a_value is not None:
from crewai.a2a.extensions.registry import (
from crewai_a2a.extensions.registry import (
create_extension_registry_from_config,
)
from crewai.a2a.wrapper import wrap_agent_with_a2a_instance
from crewai_a2a.wrapper import wrap_agent_with_a2a_instance
extension_registry = create_extension_registry_from_config(
a2a_value

View File

@@ -30,10 +30,9 @@ from typing_extensions import Self
if TYPE_CHECKING:
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai_files import FileInput
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
from crewai.agents.cache.cache_handler import CacheHandler
@@ -119,8 +118,9 @@ def _kickoff_with_a2a_support(
Returns:
LiteAgentOutput from either local execution or A2A delegation.
"""
from crewai.a2a.utils.response_model import get_a2a_agents_and_response_model
from crewai.a2a.wrapper import _execute_task_with_a2a
from crewai_a2a.utils.response_model import get_a2a_agents_and_response_model
from crewai_a2a.wrapper import _execute_task_with_a2a
from crewai.task import Task
a2a_agents, agent_response_model = get_a2a_agents_and_response_model(agent.a2a)
@@ -318,11 +318,11 @@ class LiteAgent(FlowTrackable, BaseModel):
def setup_a2a_support(self) -> Self:
"""Setup A2A extensions and server methods if a2a config exists."""
if self.a2a:
from crewai.a2a.config import A2AClientConfig, A2AConfig
from crewai.a2a.extensions.registry import (
from crewai_a2a.config import A2AClientConfig, A2AConfig
from crewai_a2a.extensions.registry import (
create_extension_registry_from_config,
)
from crewai.a2a.utils.agent_card import inject_a2a_server_methods
from crewai_a2a.utils.agent_card import inject_a2a_server_methods
configs = self.a2a if isinstance(self.a2a, list) else [self.a2a]
client_configs = [
@@ -994,7 +994,7 @@ class LiteAgent(FlowTrackable, BaseModel):
try:
from crewai.a2a.config import (
from crewai_a2a.config import (
A2AClientConfig as _A2AClientConfig,
A2AConfig as _A2AConfig,
A2AServerConfig as _A2AServerConfig,

View File

@@ -9,8 +9,8 @@ import pytest_asyncio
from a2a.client import ClientFactory
from a2a.types import AgentCard, Message, Part, Role, TaskState, TextPart
from crewai.a2a.updates.polling.handler import PollingHandler
from crewai.a2a.updates.streaming.handler import StreamingHandler
from crewai_a2a.updates.polling.handler import PollingHandler
from crewai_a2a.updates.streaming.handler import StreamingHandler
A2A_TEST_ENDPOINT = os.getenv("A2A_TEST_ENDPOINT", "http://localhost:9999")
@@ -184,8 +184,8 @@ class TestA2APushNotificationHandler:
from a2a.types import Task, TaskStatus
from pydantic import AnyHttpUrl
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.updates.push_notifications.handler import PushNotificationHandler
completed_task = Task(
id="task-123",
@@ -248,8 +248,8 @@ class TestA2APushNotificationHandler:
from a2a.types import Task, TaskStatus
from pydantic import AnyHttpUrl
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.updates.push_notifications.handler import PushNotificationHandler
mock_store = MagicMock()
mock_store.wait_for_result = AsyncMock(return_value=None)
@@ -303,7 +303,7 @@ class TestA2APushNotificationHandler:
"""Test that push handler fails gracefully without config."""
from unittest.mock import MagicMock
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai_a2a.updates.push_notifications.handler import PushNotificationHandler
mock_client = MagicMock()

View File

@@ -5,8 +5,8 @@ from __future__ import annotations
from a2a.types import AgentCard, AgentSkill
from crewai import Agent
from crewai.a2a.config import A2AClientConfig, A2AServerConfig
from crewai.a2a.utils.agent_card import inject_a2a_server_methods
from crewai_a2a.config import A2AClientConfig, A2AServerConfig
from crewai_a2a.utils.agent_card import inject_a2a_server_methods
class TestInjectA2AServerMethods:

View File

@@ -12,7 +12,7 @@ from a2a.server.agent_execution import RequestContext
from a2a.server.events import EventQueue
from a2a.types import Message, Task as A2ATask, TaskState, TaskStatus
from crewai.a2a.utils.task import cancel, cancellable, execute
from crewai_a2a.utils.task import cancel, cancellable, execute
@pytest.fixture
@@ -156,8 +156,8 @@ class TestExecute:
) -> None:
"""Execute completes successfully and enqueues completed task."""
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -175,8 +175,8 @@ class TestExecute:
) -> None:
"""Execute emits A2AServerTaskStartedEvent."""
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -197,8 +197,8 @@ class TestExecute:
) -> None:
"""Execute emits A2AServerTaskCompletedEvent on success."""
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -221,8 +221,8 @@ class TestExecute:
mock_agent.aexecute_task = AsyncMock(side_effect=ValueError("Test error"))
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
with pytest.raises(Exception):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -245,8 +245,8 @@ class TestExecute:
mock_agent.aexecute_task = AsyncMock(side_effect=asyncio.CancelledError())
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
with pytest.raises(asyncio.CancelledError):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -361,8 +361,8 @@ class TestExecuteAndCancelIntegration:
mock_agent.aexecute_task = slow_task
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus"),
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus"),
):
execute_task = asyncio.create_task(
execute(mock_agent, mock_context, mock_event_queue)

View File

@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
import pytest
from crewai.a2a.config import A2AConfig
from crewai_a2a.config import A2AConfig
try:
from a2a.types import Message, Role
@@ -27,8 +27,8 @@ def _create_mock_agent_card(name: str = "Test", url: str = "http://test-endpoint
@pytest.mark.skipif(not A2A_SDK_INSTALLED, reason="Requires a2a-sdk to be installed")
def test_trust_remote_completion_status_true_returns_directly():
"""When trust_remote_completion_status=True and A2A returns completed, return result directly."""
from crewai.a2a.wrapper import _delegate_to_a2a
from crewai.a2a.types import AgentResponseProtocol
from crewai_a2a.wrapper import _delegate_to_a2a
from crewai_a2a.types import AgentResponseProtocol
from crewai import Agent, Task
a2a_config = A2AConfig(
@@ -51,8 +51,8 @@ def test_trust_remote_completion_status_true_returns_directly():
a2a_ids = ["http://test-endpoint.com/"]
with (
patch("crewai.a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai.a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
patch("crewai_a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai_a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
):
mock_card = _create_mock_agent_card()
mock_fetch.return_value = ({"http://test-endpoint.com/": mock_card}, {})
@@ -83,7 +83,7 @@ def test_trust_remote_completion_status_true_returns_directly():
@pytest.mark.skipif(not A2A_SDK_INSTALLED, reason="Requires a2a-sdk to be installed")
def test_trust_remote_completion_status_false_continues_conversation():
"""When trust_remote_completion_status=False and A2A returns completed, ask server agent."""
from crewai.a2a.wrapper import _delegate_to_a2a
from crewai_a2a.wrapper import _delegate_to_a2a
from crewai import Agent, Task
a2a_config = A2AConfig(
@@ -116,8 +116,8 @@ def test_trust_remote_completion_status_false_continues_conversation():
return "unexpected"
with (
patch("crewai.a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai.a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
patch("crewai_a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai_a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
):
mock_card = _create_mock_agent_card()
mock_fetch.return_value = ({"http://test-endpoint.com/": mock_card}, {})

View File

@@ -7,7 +7,7 @@ import os
import pytest
from crewai import Agent
from crewai.a2a.config import A2AClientConfig
from crewai_a2a.config import A2AClientConfig
A2A_TEST_ENDPOINT = os.getenv(

View File

@@ -5,7 +5,7 @@ from unittest.mock import patch
import pytest
from crewai import Agent
from crewai.a2a.config import A2AConfig
from crewai_a2a.config import A2AConfig
try:
import a2a # noqa: F401