solace-agent-mesh 1.5.1__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/adk/callbacks.py +0 -5
- solace_agent_mesh/agent/adk/models/lite_llm.py +123 -8
- solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +245 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +40 -1
- solace_agent_mesh/agent/proxies/__init__.py +0 -0
- solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
- solace_agent_mesh/agent/proxies/a2a/app.py +55 -0
- solace_agent_mesh/agent/proxies/a2a/component.py +1115 -0
- solace_agent_mesh/agent/proxies/a2a/config.py +140 -0
- solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
- solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
- solace_agent_mesh/agent/proxies/base/app.py +99 -0
- solace_agent_mesh/agent/proxies/base/component.py +619 -0
- solace_agent_mesh/agent/proxies/base/config.py +85 -0
- solace_agent_mesh/agent/proxies/base/proxy_task_context.py +17 -0
- solace_agent_mesh/agent/sac/app.py +9 -3
- solace_agent_mesh/agent/sac/component.py +160 -8
- solace_agent_mesh/agent/tools/audio_tools.py +125 -8
- solace_agent_mesh/agent/tools/web_tools.py +10 -5
- solace_agent_mesh/agent/utils/artifact_helpers.py +141 -3
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{ad71b5ed.60668e9e.js → ad71b5ed.af3ecfd1.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{da0b5bad.9d369087.js → da0b5bad.d08a9466.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js → main.20feee82.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.0d198646.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +15 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +262 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +31 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +6 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +6 -5
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +100 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1761165361160.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1761165361160.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +2 -69
- solace_agent_mesh/cli/commands/eval_cmd.py +11 -49
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +0 -5
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +10 -12
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +9 -61
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +9 -49
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +1 -2
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DwrxZE0E.js → authCallback-BTf6dqwp.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{client-DarGQzyw.js → client-CaY59VuC.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-BGTaW0uv.js +342 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BKIeiHj_.js → vendor-BEmvJSYz.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
- solace_agent_mesh/common/a2a/__init__.py +24 -0
- solace_agent_mesh/common/a2a/artifact.py +39 -0
- solace_agent_mesh/common/a2a/events.py +29 -0
- solace_agent_mesh/common/a2a/message.py +68 -0
- solace_agent_mesh/common/a2a/protocol.py +73 -1
- solace_agent_mesh/common/agent_registry.py +83 -3
- solace_agent_mesh/common/constants.py +3 -1
- solace_agent_mesh/common/utils/pydantic_utils.py +12 -0
- solace_agent_mesh/config_portal/backend/common.py +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +98 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-44d62be6.js → manifest-61038fc6.js} +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
- solace_agent_mesh/evaluation/evaluator.py +128 -104
- solace_agent_mesh/evaluation/message_organizer.py +116 -110
- solace_agent_mesh/evaluation/report_data_processor.py +84 -86
- solace_agent_mesh/evaluation/report_generator.py +73 -79
- solace_agent_mesh/evaluation/run.py +421 -235
- solace_agent_mesh/evaluation/shared/__init__.py +92 -0
- solace_agent_mesh/evaluation/shared/constants.py +47 -0
- solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
- solace_agent_mesh/evaluation/shared/helpers.py +35 -0
- solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
- solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
- solace_agent_mesh/evaluation/subscriber.py +111 -232
- solace_agent_mesh/evaluation/summary_builder.py +227 -117
- solace_agent_mesh/gateway/base/app.py +1 -1
- solace_agent_mesh/gateway/base/component.py +8 -1
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
- solace_agent_mesh/gateway/http_sse/component.py +98 -2
- solace_agent_mesh/gateway/http_sse/dependencies.py +4 -4
- solace_agent_mesh/gateway/http_sse/main.py +2 -1
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +12 -13
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +15 -18
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +25 -18
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +30 -26
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +35 -44
- solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +4 -3
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +95 -203
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +4 -3
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +2 -2
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +33 -41
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +17 -11
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +4 -4
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +51 -43
- solace_agent_mesh/gateway/http_sse/services/session_service.py +20 -20
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +8 -8
- solace_agent_mesh/gateway/http_sse/shared/base_repository.py +45 -71
- solace_agent_mesh/gateway/http_sse/shared/types.py +0 -18
- solace_agent_mesh/templates/gateway_config_template.yaml +0 -5
- solace_agent_mesh/templates/logging_config_template.ini +10 -6
- solace_agent_mesh/templates/plugin_gateway_config_template.yaml +0 -3
- solace_agent_mesh/templates/shared_config.yaml +40 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/METADATA +47 -21
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/RECORD +162 -141
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.e49689dd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.39d5851d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/71da7b71.804d6567.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/77cf947d.64c9bd6c.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/9e9d0a82.dd810042.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/db924877.cbc66f02.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/de915948.139b4b9c.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.582a78ca.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.5766a13d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.9c0297a6.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/runtime~main.18dc45dd.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1760121512891.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1760121512891.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-2nd1gbaH.js +0 -339
- solace_agent_mesh/client/webui/frontend/static/assets/main-DoKXctCM.css +0 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-BNuqpWDc.js +0 -98
- solace_agent_mesh/evaluation/config_loader.py +0 -657
- solace_agent_mesh/evaluation/test_case_loader.py +0 -714
- /solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js.LICENSE.txt → main.20feee82.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base class for proxy components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import concurrent.futures
|
|
9
|
+
import threading
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from solace_ai_connector.common.event import Event, EventType
|
|
16
|
+
from solace_ai_connector.common.log import log
|
|
17
|
+
from solace_ai_connector.common.message import Message as SolaceMessage
|
|
18
|
+
from solace_ai_connector.components.component_base import ComponentBase
|
|
19
|
+
|
|
20
|
+
from ....common.agent_registry import AgentRegistry
|
|
21
|
+
from pydantic import TypeAdapter, ValidationError
|
|
22
|
+
|
|
23
|
+
from ....common import a2a
|
|
24
|
+
from ....common.a2a.message import get_file_parts_from_message, update_message_parts
|
|
25
|
+
from ....common.a2a.protocol import (
|
|
26
|
+
create_error_response,
|
|
27
|
+
create_success_response,
|
|
28
|
+
get_message_from_send_request,
|
|
29
|
+
get_request_id,
|
|
30
|
+
get_task_id_from_cancel_request,
|
|
31
|
+
)
|
|
32
|
+
from a2a.types import (
|
|
33
|
+
A2ARequest,
|
|
34
|
+
AgentCard,
|
|
35
|
+
CancelTaskRequest,
|
|
36
|
+
FilePart,
|
|
37
|
+
InternalError,
|
|
38
|
+
InvalidRequestError,
|
|
39
|
+
SendMessageRequest,
|
|
40
|
+
SendStreamingMessageRequest,
|
|
41
|
+
Task,
|
|
42
|
+
TaskArtifactUpdateEvent,
|
|
43
|
+
TaskStatusUpdateEvent,
|
|
44
|
+
)
|
|
45
|
+
from ...adk.services import initialize_artifact_service
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from google.adk.artifacts import BaseArtifactService
|
|
49
|
+
|
|
50
|
+
from .proxy_task_context import ProxyTaskContext
|
|
51
|
+
|
|
52
|
+
info = {
|
|
53
|
+
"class_name": "BaseProxyComponent",
|
|
54
|
+
"description": (
|
|
55
|
+
"Abstract base class for proxy components. Handles Solace interaction, "
|
|
56
|
+
"discovery, and task lifecycle management."
|
|
57
|
+
),
|
|
58
|
+
"config_parameters": [],
|
|
59
|
+
"input_schema": {},
|
|
60
|
+
"output_schema": {},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BaseProxyComponent(ComponentBase, ABC):
|
|
65
|
+
"""
|
|
66
|
+
Abstract base class for proxy components.
|
|
67
|
+
|
|
68
|
+
Initializes shared services and manages the core lifecycle for proxying
|
|
69
|
+
requests between the Solace event mesh and a downstream agent protocol.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, **kwargs: Any):
|
|
73
|
+
super().__init__(info, **kwargs)
|
|
74
|
+
self.namespace = self.get_config("namespace")
|
|
75
|
+
self.proxied_agents_config = self.get_config("proxied_agents", [])
|
|
76
|
+
self.artifact_service_config = self.get_config(
|
|
77
|
+
"artifact_service", {"type": "memory"}
|
|
78
|
+
)
|
|
79
|
+
self.discovery_interval_sec = self.get_config("discovery_interval_seconds", 60)
|
|
80
|
+
|
|
81
|
+
self.agent_registry = AgentRegistry()
|
|
82
|
+
self.artifact_service: Optional[BaseArtifactService] = None
|
|
83
|
+
self.active_tasks: Dict[str, ProxyTaskContext] = {}
|
|
84
|
+
self.active_tasks_lock = threading.Lock()
|
|
85
|
+
|
|
86
|
+
self._async_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
87
|
+
self._async_thread: Optional[threading.Thread] = None
|
|
88
|
+
self._async_init_future: Optional[concurrent.futures.Future] = None
|
|
89
|
+
self._discovery_timer_id = f"proxy_discovery_{self.name}"
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Initialize synchronous services first
|
|
93
|
+
self.artifact_service = initialize_artifact_service(self)
|
|
94
|
+
log.info("%s Artifact service initialized.", self.log_identifier)
|
|
95
|
+
|
|
96
|
+
# Start the dedicated asyncio event loop
|
|
97
|
+
self._async_loop = asyncio.new_event_loop()
|
|
98
|
+
self._async_init_future = concurrent.futures.Future()
|
|
99
|
+
self._async_thread = threading.Thread(
|
|
100
|
+
target=self._start_async_loop, daemon=True
|
|
101
|
+
)
|
|
102
|
+
self._async_thread.start()
|
|
103
|
+
|
|
104
|
+
# Schedule async initialization and wait for it to complete
|
|
105
|
+
init_coro_future = asyncio.run_coroutine_threadsafe(
|
|
106
|
+
self._perform_async_init(), self._async_loop
|
|
107
|
+
)
|
|
108
|
+
init_coro_future.result(timeout=60)
|
|
109
|
+
self._async_init_future.result(timeout=1)
|
|
110
|
+
log.info("%s Async initialization completed.", self.log_identifier)
|
|
111
|
+
|
|
112
|
+
# Perform initial synchronous discovery to populate the registry
|
|
113
|
+
self._initial_discovery_sync()
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
log.exception("%s Initialization failed: %s", self.log_identifier, e)
|
|
117
|
+
self.cleanup()
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
def invoke(self, message: SolaceMessage, data: dict) -> dict:
|
|
121
|
+
"""Placeholder invoke method. Primary logic resides in process_event."""
|
|
122
|
+
log.warning(
|
|
123
|
+
"%s 'invoke' method called, but primary logic resides in 'process_event'. This should not happen in normal operation.",
|
|
124
|
+
self.log_identifier,
|
|
125
|
+
)
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def process_event(self, event: Event):
|
|
129
|
+
"""Processes incoming events by routing them to the async loop."""
|
|
130
|
+
if not self._async_loop or not self._async_loop.is_running():
|
|
131
|
+
log.error(
|
|
132
|
+
"%s Async loop not available. Cannot process event: %s",
|
|
133
|
+
self.log_identifier,
|
|
134
|
+
event.event_type,
|
|
135
|
+
)
|
|
136
|
+
if event.event_type == EventType.MESSAGE:
|
|
137
|
+
event.data.call_negative_acknowledgements()
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
141
|
+
self._process_event_async(event), self._async_loop
|
|
142
|
+
)
|
|
143
|
+
future.add_done_callback(self._handle_scheduled_task_completion)
|
|
144
|
+
|
|
145
|
+
async def _process_event_async(self, event: Event):
|
|
146
|
+
"""Asynchronous event processing logic."""
|
|
147
|
+
if event.event_type == EventType.MESSAGE:
|
|
148
|
+
await self._handle_a2a_request(event.data)
|
|
149
|
+
elif event.event_type == EventType.TIMER:
|
|
150
|
+
if event.data.get("timer_id") == self._discovery_timer_id:
|
|
151
|
+
await self._discover_and_publish_agents()
|
|
152
|
+
else:
|
|
153
|
+
log.debug(
|
|
154
|
+
"%s Ignoring unhandled event type: %s",
|
|
155
|
+
self.log_identifier,
|
|
156
|
+
event.event_type,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def _handle_a2a_request(self, message: SolaceMessage):
|
|
160
|
+
"""Handles an incoming A2A request message from Solace."""
|
|
161
|
+
jsonrpc_request_id = None
|
|
162
|
+
logical_task_id = None
|
|
163
|
+
try:
|
|
164
|
+
payload = message.get_payload()
|
|
165
|
+
if not isinstance(payload, dict):
|
|
166
|
+
raise ValueError("Payload is not a dictionary.")
|
|
167
|
+
|
|
168
|
+
# Directly validate the payload against the modern A2A spec
|
|
169
|
+
adapter = TypeAdapter(A2ARequest)
|
|
170
|
+
a2a_request = adapter.validate_python(payload)
|
|
171
|
+
|
|
172
|
+
jsonrpc_request_id = get_request_id(a2a_request)
|
|
173
|
+
|
|
174
|
+
# Get agent name from topic
|
|
175
|
+
topic = message.get_topic()
|
|
176
|
+
if not topic:
|
|
177
|
+
raise ValueError("Message has no topic.")
|
|
178
|
+
target_agent_name = topic.split("/")[-1]
|
|
179
|
+
|
|
180
|
+
if isinstance(
|
|
181
|
+
a2a_request.root, (SendMessageRequest, SendStreamingMessageRequest)
|
|
182
|
+
):
|
|
183
|
+
from .proxy_task_context import ProxyTaskContext
|
|
184
|
+
|
|
185
|
+
logical_task_id = jsonrpc_request_id
|
|
186
|
+
|
|
187
|
+
# Resolve inbound artifacts before forwarding
|
|
188
|
+
resolved_message = await self._resolve_inbound_artifacts(a2a_request)
|
|
189
|
+
a2a_request.root.params.message = resolved_message
|
|
190
|
+
|
|
191
|
+
a2a_context = {
|
|
192
|
+
"jsonrpc_request_id": jsonrpc_request_id,
|
|
193
|
+
"logical_task_id": logical_task_id,
|
|
194
|
+
"session_id": a2a.get_context_id(resolved_message),
|
|
195
|
+
"user_id": message.get_user_properties().get(
|
|
196
|
+
"userId", "default_user"
|
|
197
|
+
),
|
|
198
|
+
"status_topic": message.get_user_properties().get("a2aStatusTopic"),
|
|
199
|
+
"reply_to_topic": message.get_user_properties().get("replyTo"),
|
|
200
|
+
"is_streaming": isinstance(a2a_request.root, SendStreamingMessageRequest),
|
|
201
|
+
}
|
|
202
|
+
task_context = ProxyTaskContext(
|
|
203
|
+
task_id=logical_task_id, a2a_context=a2a_context
|
|
204
|
+
)
|
|
205
|
+
with self.active_tasks_lock:
|
|
206
|
+
self.active_tasks[logical_task_id] = task_context
|
|
207
|
+
|
|
208
|
+
log.info(
|
|
209
|
+
"%s Forwarding request for task %s to agent %s.",
|
|
210
|
+
self.log_identifier,
|
|
211
|
+
logical_task_id,
|
|
212
|
+
target_agent_name,
|
|
213
|
+
)
|
|
214
|
+
await self._forward_request(
|
|
215
|
+
task_context, a2a_request.root, target_agent_name
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
elif isinstance(a2a_request.root, CancelTaskRequest):
|
|
219
|
+
logical_task_id = get_task_id_from_cancel_request(a2a_request)
|
|
220
|
+
|
|
221
|
+
# Get the agent name from the topic (same as for send_message)
|
|
222
|
+
target_agent_name = topic.split("/")[-1]
|
|
223
|
+
|
|
224
|
+
with self.active_tasks_lock:
|
|
225
|
+
task_context = self.active_tasks.get(logical_task_id)
|
|
226
|
+
|
|
227
|
+
if task_context:
|
|
228
|
+
log.info(
|
|
229
|
+
"%s Forwarding cancellation request for task %s to agent %s.",
|
|
230
|
+
self.log_identifier,
|
|
231
|
+
logical_task_id,
|
|
232
|
+
target_agent_name,
|
|
233
|
+
)
|
|
234
|
+
# Forward the cancel request to the downstream agent
|
|
235
|
+
await self._forward_request(
|
|
236
|
+
task_context, a2a_request.root, target_agent_name
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
# Task not found in active tasks
|
|
240
|
+
log.warning(
|
|
241
|
+
"%s Received cancel request for unknown task %s.",
|
|
242
|
+
self.log_identifier,
|
|
243
|
+
logical_task_id,
|
|
244
|
+
)
|
|
245
|
+
from a2a.types import TaskNotFoundError
|
|
246
|
+
error = TaskNotFoundError(data={"taskId": logical_task_id})
|
|
247
|
+
await self._publish_error_response(jsonrpc_request_id, error, message)
|
|
248
|
+
else:
|
|
249
|
+
log.warning(
|
|
250
|
+
"%s Received unhandled A2A request type: %s",
|
|
251
|
+
self.log_identifier,
|
|
252
|
+
type(a2a_request.root).__name__,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
message.call_acknowledgements()
|
|
256
|
+
|
|
257
|
+
except (ValueError, TypeError, ValidationError) as e:
|
|
258
|
+
log.error(
|
|
259
|
+
"%s Failed to parse or validate A2A request: %s",
|
|
260
|
+
self.log_identifier,
|
|
261
|
+
e,
|
|
262
|
+
)
|
|
263
|
+
error_data = {"taskId": logical_task_id} if logical_task_id else None
|
|
264
|
+
error = InvalidRequestError(message=str(e), data=error_data)
|
|
265
|
+
await self._publish_error_response(jsonrpc_request_id, error, message)
|
|
266
|
+
message.call_negative_acknowledgements()
|
|
267
|
+
except Exception as e:
|
|
268
|
+
log.exception(
|
|
269
|
+
"%s Unexpected error handling A2A request: %s",
|
|
270
|
+
self.log_identifier,
|
|
271
|
+
e,
|
|
272
|
+
)
|
|
273
|
+
error = InternalError(
|
|
274
|
+
message=f"Unexpected proxy error: {e}",
|
|
275
|
+
data={"taskId": logical_task_id},
|
|
276
|
+
)
|
|
277
|
+
await self._publish_error_response(jsonrpc_request_id, error, message)
|
|
278
|
+
message.call_negative_acknowledgements()
|
|
279
|
+
|
|
280
|
+
async def _resolve_inbound_artifacts(self, request: A2ARequest) -> a2a.Message:
|
|
281
|
+
"""
|
|
282
|
+
Resolves artifact URIs in an incoming message into byte content.
|
|
283
|
+
This is necessary before forwarding to a downstream agent that may not
|
|
284
|
+
share the same artifact store.
|
|
285
|
+
"""
|
|
286
|
+
original_message = get_message_from_send_request(request)
|
|
287
|
+
if not original_message:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
file_parts = get_file_parts_from_message(original_message)
|
|
291
|
+
if not file_parts:
|
|
292
|
+
return original_message # No files to resolve
|
|
293
|
+
|
|
294
|
+
log_id = f"{self.log_identifier}[ResolveInbound:{get_request_id(request)}]"
|
|
295
|
+
log.info("%s Found %d file parts to resolve.", log_id, len(file_parts))
|
|
296
|
+
|
|
297
|
+
resolved_parts = []
|
|
298
|
+
all_parts = a2a.get_parts_from_message(original_message)
|
|
299
|
+
|
|
300
|
+
for part in all_parts:
|
|
301
|
+
if isinstance(part, FilePart):
|
|
302
|
+
resolved_part = await a2a.resolve_file_part_uri(
|
|
303
|
+
part, self.artifact_service, log_id
|
|
304
|
+
)
|
|
305
|
+
resolved_parts.append(resolved_part)
|
|
306
|
+
else:
|
|
307
|
+
resolved_parts.append(part)
|
|
308
|
+
|
|
309
|
+
return update_message_parts(original_message, resolved_parts)
|
|
310
|
+
|
|
311
|
+
def _initial_discovery_sync(self):
|
|
312
|
+
"""
|
|
313
|
+
Synchronously fetches agent cards to populate the registry at startup.
|
|
314
|
+
This method does NOT publish the cards to the mesh.
|
|
315
|
+
"""
|
|
316
|
+
log.info(
|
|
317
|
+
"%s Performing initial synchronous agent discovery...", self.log_identifier
|
|
318
|
+
)
|
|
319
|
+
with httpx.Client() as client:
|
|
320
|
+
for agent_config in self.proxied_agents_config:
|
|
321
|
+
agent_alias = agent_config["name"]
|
|
322
|
+
agent_url = agent_config.get("url")
|
|
323
|
+
if not agent_url:
|
|
324
|
+
log.error(
|
|
325
|
+
"%s Skipping agent '%s' in initial discovery: no URL configured.",
|
|
326
|
+
self.log_identifier,
|
|
327
|
+
agent_alias,
|
|
328
|
+
)
|
|
329
|
+
continue
|
|
330
|
+
try:
|
|
331
|
+
# Use a synchronous client for this initial blocking call
|
|
332
|
+
response = client.get(f"{agent_url}/.well-known/agent.json")
|
|
333
|
+
response.raise_for_status()
|
|
334
|
+
agent_card = AgentCard.model_validate(response.json())
|
|
335
|
+
|
|
336
|
+
card_for_proxy = agent_card.model_copy(deep=True)
|
|
337
|
+
card_for_proxy.name = agent_alias
|
|
338
|
+
self.agent_registry.add_or_update_agent(card_for_proxy)
|
|
339
|
+
log.info(
|
|
340
|
+
"%s Initial discovery successful for alias '%s' (actual name: '%s').",
|
|
341
|
+
self.log_identifier,
|
|
342
|
+
agent_alias,
|
|
343
|
+
agent_card.name,
|
|
344
|
+
)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
log.error(
|
|
347
|
+
"%s Failed initial discovery for agent '%s' at URL '%s': %s",
|
|
348
|
+
self.log_identifier,
|
|
349
|
+
agent_alias,
|
|
350
|
+
agent_url,
|
|
351
|
+
e,
|
|
352
|
+
)
|
|
353
|
+
log.info("%s Initial synchronous discovery complete.", self.log_identifier)
|
|
354
|
+
|
|
355
|
+
async def _discover_and_publish_agents(self):
|
|
356
|
+
"""
|
|
357
|
+
Asynchronously fetches agent cards, updates the registry, and publishes them.
|
|
358
|
+
This is intended for the recurring timer.
|
|
359
|
+
"""
|
|
360
|
+
log.info("%s Starting recurring agent discovery cycle...", self.log_identifier)
|
|
361
|
+
for agent_config in self.proxied_agents_config:
|
|
362
|
+
try:
|
|
363
|
+
modern_card = await self._fetch_agent_card(agent_config)
|
|
364
|
+
if not modern_card:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
agent_alias = agent_config["name"]
|
|
368
|
+
card_for_proxy = modern_card.model_copy(deep=True)
|
|
369
|
+
card_for_proxy.name = agent_alias
|
|
370
|
+
self.agent_registry.add_or_update_agent(card_for_proxy)
|
|
371
|
+
|
|
372
|
+
# Publish the modern card directly after updating its URL
|
|
373
|
+
card_to_publish = card_for_proxy.model_copy(deep=True)
|
|
374
|
+
card_to_publish.url = (
|
|
375
|
+
f"solace:{a2a.get_agent_request_topic(self.namespace, agent_alias)}"
|
|
376
|
+
)
|
|
377
|
+
discovery_topic = a2a.get_discovery_topic(self.namespace)
|
|
378
|
+
self._publish_a2a_message(
|
|
379
|
+
card_to_publish.model_dump(exclude_none=True), discovery_topic
|
|
380
|
+
)
|
|
381
|
+
log.info(
|
|
382
|
+
"%s Refreshed and published card for agent '%s'.",
|
|
383
|
+
self.log_identifier,
|
|
384
|
+
agent_alias,
|
|
385
|
+
)
|
|
386
|
+
except Exception as e:
|
|
387
|
+
log.error(
|
|
388
|
+
"%s Failed to discover or publish card for agent '%s' in recurring cycle: %s",
|
|
389
|
+
self.log_identifier,
|
|
390
|
+
agent_config.get("name", "unknown"),
|
|
391
|
+
e,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
async def _publish_status_update(
|
|
395
|
+
self, event: TaskStatusUpdateEvent, a2a_context: Dict
|
|
396
|
+
):
|
|
397
|
+
"""Publishes a TaskStatusUpdateEvent to the appropriate Solace topic."""
|
|
398
|
+
target_topic = a2a_context.get("status_topic")
|
|
399
|
+
if not target_topic:
|
|
400
|
+
log.warning(
|
|
401
|
+
"%s No statusTopic in context for task %s. Cannot publish status update.",
|
|
402
|
+
self.log_identifier,
|
|
403
|
+
event.task_id,
|
|
404
|
+
)
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
response = create_success_response(
|
|
408
|
+
result=event, request_id=a2a_context.get("jsonrpc_request_id")
|
|
409
|
+
)
|
|
410
|
+
self._publish_a2a_message(response.model_dump(exclude_none=True), target_topic)
|
|
411
|
+
|
|
412
|
+
async def _publish_final_response(self, task: Task, a2a_context: Dict):
|
|
413
|
+
"""Publishes the final Task object to the appropriate Solace topic."""
|
|
414
|
+
target_topic = a2a_context.get("reply_to_topic")
|
|
415
|
+
if not target_topic:
|
|
416
|
+
log.warning(
|
|
417
|
+
"%s No replyToTopic in context for task %s. Cannot publish final response.",
|
|
418
|
+
self.log_identifier,
|
|
419
|
+
task.id,
|
|
420
|
+
)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
response = create_success_response(
|
|
424
|
+
result=task, request_id=a2a_context.get("jsonrpc_request_id")
|
|
425
|
+
)
|
|
426
|
+
self._publish_a2a_message(response.model_dump(exclude_none=True), target_topic)
|
|
427
|
+
|
|
428
|
+
async def _publish_artifact_update(
|
|
429
|
+
self, event: TaskArtifactUpdateEvent, a2a_context: Dict
|
|
430
|
+
):
|
|
431
|
+
"""Publishes a TaskArtifactUpdateEvent to the appropriate Solace topic."""
|
|
432
|
+
target_topic = a2a_context.get("status_topic")
|
|
433
|
+
if not target_topic:
|
|
434
|
+
log.warning(
|
|
435
|
+
"%s No statusTopic in context for task %s. Cannot publish artifact update.",
|
|
436
|
+
self.log_identifier,
|
|
437
|
+
event.task_id,
|
|
438
|
+
)
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
response = create_success_response(
|
|
442
|
+
result=event, request_id=a2a_context.get("jsonrpc_request_id")
|
|
443
|
+
)
|
|
444
|
+
self._publish_a2a_message(response.model_dump(exclude_none=True), target_topic)
|
|
445
|
+
|
|
446
|
+
async def _publish_error_response(
|
|
447
|
+
self,
|
|
448
|
+
request_id: str,
|
|
449
|
+
error: InternalError | InvalidRequestError,
|
|
450
|
+
message: SolaceMessage,
|
|
451
|
+
):
|
|
452
|
+
"""Publishes a JSON-RPC error response."""
|
|
453
|
+
target_topic = message.get_user_properties().get("replyTo")
|
|
454
|
+
if not target_topic:
|
|
455
|
+
log.warning(
|
|
456
|
+
"%s No replyToTopic in message. Cannot publish error response.",
|
|
457
|
+
self.log_identifier,
|
|
458
|
+
)
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
response = create_error_response(error=error, request_id=request_id)
|
|
462
|
+
self._publish_a2a_message(response.model_dump(exclude_none=True), target_topic)
|
|
463
|
+
|
|
464
|
+
def _publish_a2a_message(
|
|
465
|
+
self, payload: Dict, topic: str, user_properties: Optional[Dict] = None
|
|
466
|
+
):
|
|
467
|
+
"""Helper to publish A2A messages via the SAC App."""
|
|
468
|
+
app = self.get_app()
|
|
469
|
+
if app:
|
|
470
|
+
app.send_message(
|
|
471
|
+
payload=payload, topic=topic, user_properties=user_properties
|
|
472
|
+
)
|
|
473
|
+
else:
|
|
474
|
+
log.error(
|
|
475
|
+
"%s Cannot publish message: Not running within a SAC App context.",
|
|
476
|
+
self.log_identifier,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _start_async_loop(self):
|
|
480
|
+
"""Target method for the dedicated async thread."""
|
|
481
|
+
log.info("%s Dedicated async thread started.", self.log_identifier)
|
|
482
|
+
try:
|
|
483
|
+
asyncio.set_event_loop(self._async_loop)
|
|
484
|
+
self._async_loop.run_forever()
|
|
485
|
+
except Exception as e:
|
|
486
|
+
log.exception(
|
|
487
|
+
"%s Exception in dedicated async thread loop: %s",
|
|
488
|
+
self.log_identifier,
|
|
489
|
+
e,
|
|
490
|
+
)
|
|
491
|
+
if self._async_init_future and not self._async_init_future.done():
|
|
492
|
+
self._async_init_future.set_exception(e)
|
|
493
|
+
finally:
|
|
494
|
+
log.info("%s Dedicated async thread loop finishing.", self.log_identifier)
|
|
495
|
+
if self._async_loop.is_running():
|
|
496
|
+
self._async_loop.call_soon_threadsafe(self._async_loop.stop)
|
|
497
|
+
|
|
498
|
+
async def _perform_async_init(self):
|
|
499
|
+
"""Coroutine to perform async initialization."""
|
|
500
|
+
try:
|
|
501
|
+
log.info("%s Performing async initialization...", self.log_identifier)
|
|
502
|
+
# Placeholder for any future async init steps
|
|
503
|
+
if self._async_init_future and not self._async_init_future.done():
|
|
504
|
+
self._async_loop.call_soon_threadsafe(
|
|
505
|
+
self._async_init_future.set_result, True
|
|
506
|
+
)
|
|
507
|
+
except Exception as e:
|
|
508
|
+
if self._async_init_future and not self._async_init_future.done():
|
|
509
|
+
self._async_loop.call_soon_threadsafe(
|
|
510
|
+
self._async_init_future.set_exception, e
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
def _handle_scheduled_task_completion(self, future: concurrent.futures.Future):
|
|
514
|
+
"""Callback to log exceptions from tasks scheduled on the async loop."""
|
|
515
|
+
if future.done() and future.exception():
|
|
516
|
+
log.error(
|
|
517
|
+
"%s Coroutine scheduled on async loop failed: %s",
|
|
518
|
+
self.log_identifier,
|
|
519
|
+
future.exception(),
|
|
520
|
+
exc_info=future.exception(),
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def _publish_discovered_cards(self):
|
|
524
|
+
"""Publishes all agent cards currently in the registry."""
|
|
525
|
+
log.info(
|
|
526
|
+
"%s Publishing initially discovered agent cards...", self.log_identifier
|
|
527
|
+
)
|
|
528
|
+
for agent_alias in self.agent_registry.get_agent_names():
|
|
529
|
+
card_to_publish = self.agent_registry.get_agent(agent_alias)
|
|
530
|
+
if not card_to_publish:
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
card_to_publish.url = (
|
|
534
|
+
f"solace:{a2a.get_agent_request_topic(self.namespace, agent_alias)}"
|
|
535
|
+
)
|
|
536
|
+
discovery_topic = a2a.get_discovery_topic(self.namespace)
|
|
537
|
+
self._publish_a2a_message(
|
|
538
|
+
card_to_publish.model_dump(exclude_none=True), discovery_topic
|
|
539
|
+
)
|
|
540
|
+
log.info(
|
|
541
|
+
"%s Published initially discovered card for agent '%s'.",
|
|
542
|
+
self.log_identifier,
|
|
543
|
+
agent_alias,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def run(self):
|
|
547
|
+
"""
|
|
548
|
+
Called by the framework to start the component's background tasks.
|
|
549
|
+
This is the component's main entry point for active operations.
|
|
550
|
+
"""
|
|
551
|
+
log.info(
|
|
552
|
+
"%s Component is ready. Starting active operations.", self.log_identifier
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Publish the cards that were discovered synchronously during init
|
|
556
|
+
self._publish_discovered_cards()
|
|
557
|
+
|
|
558
|
+
# Schedule the recurring discovery timer
|
|
559
|
+
if self.discovery_interval_sec > 0:
|
|
560
|
+
self.add_timer(
|
|
561
|
+
delay_ms=self.discovery_interval_sec * 1000,
|
|
562
|
+
timer_id=self._discovery_timer_id,
|
|
563
|
+
interval_ms=self.discovery_interval_sec * 1000,
|
|
564
|
+
)
|
|
565
|
+
log.info(
|
|
566
|
+
"%s Scheduled recurring agent discovery every %d seconds.",
|
|
567
|
+
self.log_identifier,
|
|
568
|
+
self.discovery_interval_sec,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
super().run()
|
|
572
|
+
|
|
573
|
+
def clear_client_cache(self):
|
|
574
|
+
"""
|
|
575
|
+
Clears all cached clients. Useful for testing when authentication
|
|
576
|
+
configuration changes between tests.
|
|
577
|
+
"""
|
|
578
|
+
# This method is intentionally empty in the base class.
|
|
579
|
+
# Concrete implementations should override it if they cache clients.
|
|
580
|
+
pass
|
|
581
|
+
|
|
582
|
+
def cleanup(self):
|
|
583
|
+
"""Cleans up resources on component shutdown."""
|
|
584
|
+
log.info("%s Cleaning up proxy component.", self.log_identifier)
|
|
585
|
+
self.cancel_timer(self._discovery_timer_id)
|
|
586
|
+
|
|
587
|
+
# Clear active tasks (no need to signal cancellation - downstream agents own their tasks)
|
|
588
|
+
with self.active_tasks_lock:
|
|
589
|
+
self.active_tasks.clear()
|
|
590
|
+
|
|
591
|
+
if self._async_loop and self._async_loop.is_running():
|
|
592
|
+
self._async_loop.call_soon_threadsafe(self._async_loop.stop)
|
|
593
|
+
if self._async_thread and self._async_thread.is_alive():
|
|
594
|
+
self._async_thread.join(timeout=5)
|
|
595
|
+
if self._async_thread.is_alive():
|
|
596
|
+
log.warning(
|
|
597
|
+
"%s Async thread did not exit cleanly.", self.log_identifier
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
super().cleanup()
|
|
601
|
+
log.info("%s Component cleanup finished.", self.log_identifier)
|
|
602
|
+
|
|
603
|
+
@abstractmethod
|
|
604
|
+
async def _fetch_agent_card(self, agent_config: dict) -> Optional[AgentCard]:
|
|
605
|
+
"""
|
|
606
|
+
Fetches the AgentCard from a single downstream agent.
|
|
607
|
+
To be implemented by concrete proxy classes.
|
|
608
|
+
"""
|
|
609
|
+
raise NotImplementedError
|
|
610
|
+
|
|
611
|
+
@abstractmethod
|
|
612
|
+
async def _forward_request(
|
|
613
|
+
self, task_context: "ProxyTaskContext", request: A2ARequest, agent_name: str
|
|
614
|
+
):
|
|
615
|
+
"""
|
|
616
|
+
Forwards a request to the downstream agent using its specific protocol.
|
|
617
|
+
To be implemented by concrete proxy classes.
|
|
618
|
+
"""
|
|
619
|
+
raise NotImplementedError
|