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.

Files changed (180) hide show
  1. solace_agent_mesh/agent/adk/callbacks.py +0 -5
  2. solace_agent_mesh/agent/adk/models/lite_llm.py +123 -8
  3. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +245 -0
  4. solace_agent_mesh/agent/protocol/event_handlers.py +40 -1
  5. solace_agent_mesh/agent/proxies/__init__.py +0 -0
  6. solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
  7. solace_agent_mesh/agent/proxies/a2a/app.py +55 -0
  8. solace_agent_mesh/agent/proxies/a2a/component.py +1115 -0
  9. solace_agent_mesh/agent/proxies/a2a/config.py +140 -0
  10. solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
  11. solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
  12. solace_agent_mesh/agent/proxies/base/app.py +99 -0
  13. solace_agent_mesh/agent/proxies/base/component.py +619 -0
  14. solace_agent_mesh/agent/proxies/base/config.py +85 -0
  15. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +17 -0
  16. solace_agent_mesh/agent/sac/app.py +9 -3
  17. solace_agent_mesh/agent/sac/component.py +160 -8
  18. solace_agent_mesh/agent/tools/audio_tools.py +125 -8
  19. solace_agent_mesh/agent/tools/web_tools.py +10 -5
  20. solace_agent_mesh/agent/utils/artifact_helpers.py +141 -3
  21. solace_agent_mesh/assets/docs/404.html +3 -3
  22. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +1 -0
  23. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/{ad71b5ed.60668e9e.js → ad71b5ed.af3ecfd1.js} +1 -1
  29. solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
  30. solace_agent_mesh/assets/docs/assets/js/{da0b5bad.9d369087.js → da0b5bad.d08a9466.js} +1 -1
  31. solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
  32. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +1 -0
  35. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js → main.20feee82.js} +2 -2
  37. solace_agent_mesh/assets/docs/assets/js/runtime~main.0d198646.js +1 -0
  38. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +15 -4
  39. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
  40. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +262 -0
  50. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
  51. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +31 -3
  52. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
  53. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
  54. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
  55. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +5 -5
  56. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
  59. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +6 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
  61. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +5 -5
  64. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
  65. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
  66. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
  67. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
  68. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
  69. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
  70. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
  71. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
  72. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
  73. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
  74. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
  75. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
  76. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  77. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
  78. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +6 -5
  79. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
  80. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
  81. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +100 -3
  82. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
  83. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  84. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
  85. solace_agent_mesh/assets/docs/lunr-index-1761165361160.json +1 -0
  86. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  87. solace_agent_mesh/assets/docs/search-doc-1761165361160.json +1 -0
  88. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  89. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  90. solace_agent_mesh/cli/__init__.py +1 -1
  91. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +2 -69
  92. solace_agent_mesh/cli/commands/eval_cmd.py +11 -49
  93. solace_agent_mesh/cli/commands/init_cmd/__init__.py +0 -5
  94. solace_agent_mesh/cli/commands/init_cmd/env_step.py +10 -12
  95. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +9 -61
  96. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +9 -49
  97. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +1 -2
  98. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DwrxZE0E.js → authCallback-BTf6dqwp.js} +1 -1
  99. solace_agent_mesh/client/webui/frontend/static/assets/{client-DarGQzyw.js → client-CaY59VuC.js} +1 -1
  100. solace_agent_mesh/client/webui/frontend/static/assets/main-BGTaW0uv.js +342 -0
  101. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +1 -0
  102. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BKIeiHj_.js → vendor-BEmvJSYz.js} +1 -1
  103. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  104. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  105. solace_agent_mesh/common/a2a/__init__.py +24 -0
  106. solace_agent_mesh/common/a2a/artifact.py +39 -0
  107. solace_agent_mesh/common/a2a/events.py +29 -0
  108. solace_agent_mesh/common/a2a/message.py +68 -0
  109. solace_agent_mesh/common/a2a/protocol.py +73 -1
  110. solace_agent_mesh/common/agent_registry.py +83 -3
  111. solace_agent_mesh/common/constants.py +3 -1
  112. solace_agent_mesh/common/utils/pydantic_utils.py +12 -0
  113. solace_agent_mesh/config_portal/backend/common.py +1 -1
  114. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +98 -0
  115. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-44d62be6.js → manifest-61038fc6.js} +1 -1
  116. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  117. solace_agent_mesh/evaluation/evaluator.py +128 -104
  118. solace_agent_mesh/evaluation/message_organizer.py +116 -110
  119. solace_agent_mesh/evaluation/report_data_processor.py +84 -86
  120. solace_agent_mesh/evaluation/report_generator.py +73 -79
  121. solace_agent_mesh/evaluation/run.py +421 -235
  122. solace_agent_mesh/evaluation/shared/__init__.py +92 -0
  123. solace_agent_mesh/evaluation/shared/constants.py +47 -0
  124. solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
  125. solace_agent_mesh/evaluation/shared/helpers.py +35 -0
  126. solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
  127. solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
  128. solace_agent_mesh/evaluation/subscriber.py +111 -232
  129. solace_agent_mesh/evaluation/summary_builder.py +227 -117
  130. solace_agent_mesh/gateway/base/app.py +1 -1
  131. solace_agent_mesh/gateway/base/component.py +8 -1
  132. solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
  133. solace_agent_mesh/gateway/http_sse/component.py +98 -2
  134. solace_agent_mesh/gateway/http_sse/dependencies.py +4 -4
  135. solace_agent_mesh/gateway/http_sse/main.py +2 -1
  136. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +12 -13
  137. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +15 -18
  138. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +25 -18
  139. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +30 -26
  140. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +35 -44
  141. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +4 -3
  142. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +95 -203
  143. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +4 -3
  144. solace_agent_mesh/gateway/http_sse/routers/sessions.py +2 -2
  145. solace_agent_mesh/gateway/http_sse/routers/tasks.py +33 -41
  146. solace_agent_mesh/gateway/http_sse/routers/visualization.py +17 -11
  147. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +4 -4
  148. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +51 -43
  149. solace_agent_mesh/gateway/http_sse/services/session_service.py +20 -20
  150. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +8 -8
  151. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +45 -71
  152. solace_agent_mesh/gateway/http_sse/shared/types.py +0 -18
  153. solace_agent_mesh/templates/gateway_config_template.yaml +0 -5
  154. solace_agent_mesh/templates/logging_config_template.ini +10 -6
  155. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +0 -3
  156. solace_agent_mesh/templates/shared_config.yaml +40 -0
  157. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/METADATA +47 -21
  158. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/RECORD +162 -141
  159. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.e49689dd.js +0 -1
  160. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.39d5851d.js +0 -1
  161. solace_agent_mesh/assets/docs/assets/js/71da7b71.804d6567.js +0 -1
  162. solace_agent_mesh/assets/docs/assets/js/77cf947d.64c9bd6c.js +0 -1
  163. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.dd810042.js +0 -1
  164. solace_agent_mesh/assets/docs/assets/js/db924877.cbc66f02.js +0 -1
  165. solace_agent_mesh/assets/docs/assets/js/de915948.139b4b9c.js +0 -1
  166. solace_agent_mesh/assets/docs/assets/js/e6f9706b.582a78ca.js +0 -1
  167. solace_agent_mesh/assets/docs/assets/js/f284c35a.5766a13d.js +0 -1
  168. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.9c0297a6.js +0 -1
  169. solace_agent_mesh/assets/docs/assets/js/runtime~main.18dc45dd.js +0 -1
  170. solace_agent_mesh/assets/docs/lunr-index-1760121512891.json +0 -1
  171. solace_agent_mesh/assets/docs/search-doc-1760121512891.json +0 -1
  172. solace_agent_mesh/client/webui/frontend/static/assets/main-2nd1gbaH.js +0 -339
  173. solace_agent_mesh/client/webui/frontend/static/assets/main-DoKXctCM.css +0 -1
  174. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-BNuqpWDc.js +0 -98
  175. solace_agent_mesh/evaluation/config_loader.py +0 -657
  176. solace_agent_mesh/evaluation/test_case_loader.py +0 -714
  177. /solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js.LICENSE.txt → main.20feee82.js.LICENSE.txt} +0 -0
  178. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/WHEEL +0 -0
  179. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/entry_points.txt +0 -0
  180. {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