solace-agent-mesh 1.6.0__py3-none-any.whl → 1.6.2__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/app_llm_agent.py +26 -0
- solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +1 -1
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +135 -31
- solace_agent_mesh/agent/adk/models/lite_llm.py +5 -0
- solace_agent_mesh/agent/adk/runner.py +10 -12
- solace_agent_mesh/agent/adk/services.py +50 -14
- solace_agent_mesh/agent/adk/setup.py +66 -38
- solace_agent_mesh/agent/protocol/event_handlers.py +416 -152
- solace_agent_mesh/agent/proxies/a2a/app.py +3 -2
- solace_agent_mesh/agent/proxies/base/app.py +3 -2
- solace_agent_mesh/agent/proxies/base/component.py +35 -4
- solace_agent_mesh/agent/sac/app.py +97 -9
- solace_agent_mesh/agent/sac/component.py +284 -145
- solace_agent_mesh/agent/sac/task_execution_context.py +79 -2
- solace_agent_mesh/agent/tools/tool_config_types.py +3 -0
- solace_agent_mesh/agent/utils/artifact_helpers.py +1 -1
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/240a0364.c39f8388.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/631738c7.7c4594c9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/66d4869e.830d443f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/71da7b71.ddbdfbe2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{e3d9abda.2b916f9e.js → e3d9abda.6b9493d0.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/e92d0134.4f395c6b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.720d2ef2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.d1643f0b.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.97f920d4.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +4 -25
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +76 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +5 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +3 -3
- 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 +3 -6
- 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 +3 -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-1761663789856.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1761663789856.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/client/webui/frontend/static/assets/{authCallback-BTf6dqwp.js → authCallback-D4_RMYRh.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{client-CaY59VuC.js → client-UZ3qU6Bq.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main--3yJYl7S.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-DojKHS49.js +342 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BEmvJSYz.js → vendor-DSqhjwq_.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/events.py +2 -1
- solace_agent_mesh/common/a2a/protocol.py +78 -0
- solace_agent_mesh/common/sac/sam_component_base.py +406 -21
- solace_agent_mesh/common/utils/pydantic_utils.py +90 -3
- solace_agent_mesh/gateway/base/app.py +15 -0
- solace_agent_mesh/gateway/base/component.py +116 -46
- solace_agent_mesh/gateway/http_sse/app.py +7 -0
- solace_agent_mesh/gateway/http_sse/component.py +18 -10
- solace_agent_mesh/gateway/http_sse/dependencies.py +83 -59
- solace_agent_mesh/gateway/http_sse/main.py +5 -4
- solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/auth.py +103 -6
- solace_agent_mesh/gateway/http_sse/routers/config.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/sse.py +15 -5
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +3 -3
- solace_agent_mesh/gateway/http_sse/routers/users.py +47 -1
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +90 -8
- solace_agent_mesh/gateway/http_sse/services/session_service.py +1 -1
- solace_agent_mesh/gateway/http_sse/session_manager.py +15 -15
- solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +16 -1
- solace_agent_mesh/gateway/http_sse/sse_manager.py +15 -6
- solace_agent_mesh/templates/logging_config_template.ini +2 -2
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/METADATA +2 -2
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/RECORD +116 -114
- solace_agent_mesh/assets/docs/assets/js/240a0364.7eac6021.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/631738c7.a8b1ef8b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e92d0134.cf6d6522.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/main.20feee82.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.0d198646.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1761165361160.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1761165361160.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-BGTaW0uv.js +0 -342
- solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +0 -1
- /solace_agent_mesh/assets/docs/assets/js/{main.20feee82.js.LICENSE.txt → main.d1643f0b.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,8 @@ import logging
|
|
|
6
6
|
import abc
|
|
7
7
|
import asyncio
|
|
8
8
|
import threading
|
|
9
|
-
|
|
9
|
+
import functools
|
|
10
|
+
from typing import Any, Optional
|
|
10
11
|
|
|
11
12
|
from solace_ai_connector.components.component_base import ComponentBase
|
|
12
13
|
|
|
@@ -14,6 +15,8 @@ from ..exceptions import MessageSizeExceededError
|
|
|
14
15
|
from ..utils.message_utils import validate_message_size
|
|
15
16
|
|
|
16
17
|
log = logging.getLogger(__name__)
|
|
18
|
+
trace_logger = logging.getLogger("sam_trace")
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class SamComponentBase(ComponentBase, abc.ABC):
|
|
19
22
|
"""
|
|
@@ -54,7 +57,293 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
54
57
|
|
|
55
58
|
self._async_loop: asyncio.AbstractEventLoop | None = None
|
|
56
59
|
self._async_thread: threading.Thread | None = None
|
|
57
|
-
|
|
60
|
+
|
|
61
|
+
# Timer callback registry
|
|
62
|
+
self._timer_callbacks: dict[str, Any] = {}
|
|
63
|
+
self._timer_callbacks_lock = threading.Lock()
|
|
64
|
+
|
|
65
|
+
# Trust Manager integration (enterprise feature) - initialized as part of _late_init
|
|
66
|
+
self.trust_manager: Optional[Any] = None
|
|
67
|
+
|
|
68
|
+
log.info("%s Initialized SamComponentBase", self.log_identifier)
|
|
69
|
+
|
|
70
|
+
def add_timer(
|
|
71
|
+
self,
|
|
72
|
+
delay_ms: int,
|
|
73
|
+
timer_id: str,
|
|
74
|
+
interval_ms: int = 0,
|
|
75
|
+
callback: Optional[Any] = None,
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Add a timer with optional callback.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
delay_ms: Initial delay in milliseconds
|
|
82
|
+
timer_id: Unique timer identifier
|
|
83
|
+
interval_ms: Repeat interval in milliseconds (0 for one-shot)
|
|
84
|
+
callback: Optional callback function to invoke when timer fires.
|
|
85
|
+
If provided, callback will be invoked when timer event occurs.
|
|
86
|
+
Callback receives timer_data dict as argument.
|
|
87
|
+
Callback should be thread-safe or schedule work appropriately.
|
|
88
|
+
"""
|
|
89
|
+
# Register callback if provided
|
|
90
|
+
if callback:
|
|
91
|
+
with self._timer_callbacks_lock:
|
|
92
|
+
if timer_id in self._timer_callbacks:
|
|
93
|
+
log.warning(
|
|
94
|
+
"%s Timer ID '%s' already has a registered callback. Overwriting.",
|
|
95
|
+
self.log_identifier,
|
|
96
|
+
timer_id,
|
|
97
|
+
)
|
|
98
|
+
self._timer_callbacks[timer_id] = callback
|
|
99
|
+
log.debug(
|
|
100
|
+
"%s Registered callback for timer: %s",
|
|
101
|
+
self.log_identifier,
|
|
102
|
+
timer_id,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Call parent implementation to actually create the timer
|
|
106
|
+
super().add_timer(delay_ms=delay_ms, timer_id=timer_id, interval_ms=interval_ms)
|
|
107
|
+
|
|
108
|
+
def cancel_timer(self, timer_id: str):
|
|
109
|
+
"""
|
|
110
|
+
Cancel a timer and remove its callback if registered.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
timer_id: Timer identifier to cancel
|
|
114
|
+
"""
|
|
115
|
+
# Remove callback registration
|
|
116
|
+
with self._timer_callbacks_lock:
|
|
117
|
+
if timer_id in self._timer_callbacks:
|
|
118
|
+
del self._timer_callbacks[timer_id]
|
|
119
|
+
log.debug(
|
|
120
|
+
"%s Unregistered callback for timer: %s",
|
|
121
|
+
self.log_identifier,
|
|
122
|
+
timer_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Call parent implementation to actually cancel the timer
|
|
126
|
+
super().cancel_timer(timer_id)
|
|
127
|
+
|
|
128
|
+
def process_event(self, event):
|
|
129
|
+
"""
|
|
130
|
+
Process incoming events by routing to appropriate handlers.
|
|
131
|
+
|
|
132
|
+
This base implementation handles MESSAGE and TIMER events:
|
|
133
|
+
- MESSAGE events are routed to _handle_message() abstract method
|
|
134
|
+
- TIMER events are routed to registered callbacks
|
|
135
|
+
- Other events are passed to parent class
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
event: Event object from SAC framework
|
|
139
|
+
"""
|
|
140
|
+
from solace_ai_connector.common.event import Event, EventType
|
|
141
|
+
from solace_ai_connector.common.message import Message as SolaceMessage
|
|
142
|
+
|
|
143
|
+
if event.event_type == EventType.MESSAGE:
|
|
144
|
+
message: SolaceMessage = event.data
|
|
145
|
+
topic = message.get_topic()
|
|
146
|
+
|
|
147
|
+
if not topic:
|
|
148
|
+
log.warning(
|
|
149
|
+
"%s Received message without topic. Ignoring.",
|
|
150
|
+
self.log_identifier,
|
|
151
|
+
)
|
|
152
|
+
try:
|
|
153
|
+
message.call_negative_acknowledgements()
|
|
154
|
+
except Exception as nack_e:
|
|
155
|
+
log.error(
|
|
156
|
+
"%s Failed to NACK message without topic: %s",
|
|
157
|
+
self.log_identifier,
|
|
158
|
+
nack_e,
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# Delegate to abstract method implemented by subclass
|
|
164
|
+
self._handle_message(message, topic)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
log.error(
|
|
167
|
+
"%s Error in _handle_message for topic %s: %s",
|
|
168
|
+
self.log_identifier,
|
|
169
|
+
topic,
|
|
170
|
+
e,
|
|
171
|
+
exc_info=True,
|
|
172
|
+
)
|
|
173
|
+
try:
|
|
174
|
+
message.call_negative_acknowledgements()
|
|
175
|
+
except Exception as nack_e:
|
|
176
|
+
log.error(
|
|
177
|
+
"%s Failed to NACK message after error: %s",
|
|
178
|
+
self.log_identifier,
|
|
179
|
+
nack_e,
|
|
180
|
+
)
|
|
181
|
+
self.handle_error(e, event)
|
|
182
|
+
|
|
183
|
+
elif event.event_type == EventType.TIMER:
|
|
184
|
+
# Handle timer events via callback registry
|
|
185
|
+
timer_data = event.data
|
|
186
|
+
timer_id = timer_data.get("timer_id")
|
|
187
|
+
|
|
188
|
+
if not timer_id:
|
|
189
|
+
log.warning(
|
|
190
|
+
"%s Timer event missing timer_id: %s",
|
|
191
|
+
self.log_identifier,
|
|
192
|
+
timer_data,
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# Look up registered callback
|
|
197
|
+
with self._timer_callbacks_lock:
|
|
198
|
+
callback = self._timer_callbacks.get(timer_id)
|
|
199
|
+
|
|
200
|
+
if callback:
|
|
201
|
+
try:
|
|
202
|
+
log.debug(
|
|
203
|
+
"%s Invoking registered callback for timer: %s",
|
|
204
|
+
self.log_identifier,
|
|
205
|
+
timer_id,
|
|
206
|
+
)
|
|
207
|
+
callback(timer_data)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
log.error(
|
|
210
|
+
"%s Error in timer callback for %s: %s",
|
|
211
|
+
self.log_identifier,
|
|
212
|
+
timer_id,
|
|
213
|
+
e,
|
|
214
|
+
exc_info=True,
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
log.warning(
|
|
218
|
+
"%s No callback registered for timer: %s. Timer event ignored.",
|
|
219
|
+
self.log_identifier,
|
|
220
|
+
timer_id,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
# Pass other event types to parent class
|
|
224
|
+
super().process_event(event)
|
|
225
|
+
|
|
226
|
+
def _handle_message(self, message, topic: str) -> None:
|
|
227
|
+
"""
|
|
228
|
+
Handle an incoming message by routing to async handler.
|
|
229
|
+
|
|
230
|
+
This base implementation schedules async processing on the component's
|
|
231
|
+
event loop. Subclasses can override this for custom sync handling,
|
|
232
|
+
or implement _handle_message_async() for async handling.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
message: The Solace message (SolaceMessage instance)
|
|
236
|
+
topic: The topic the message was received on
|
|
237
|
+
"""
|
|
238
|
+
loop = self.get_async_loop()
|
|
239
|
+
if loop and loop.is_running():
|
|
240
|
+
# Schedule async processing
|
|
241
|
+
coro = self._handle_message_async(message, topic)
|
|
242
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
243
|
+
future.add_done_callback(
|
|
244
|
+
functools.partial(self._handle_async_message_completion, topic=topic)
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
log.error(
|
|
248
|
+
"%s Async loop not available. Cannot process message on topic: %s",
|
|
249
|
+
self.log_identifier,
|
|
250
|
+
topic,
|
|
251
|
+
)
|
|
252
|
+
raise RuntimeError("Async loop not available for message processing")
|
|
253
|
+
|
|
254
|
+
def _handle_async_message_completion(self, future: asyncio.Future, topic: str):
|
|
255
|
+
"""Callback to handle completion of async message processing."""
|
|
256
|
+
try:
|
|
257
|
+
if future.cancelled():
|
|
258
|
+
log.warning(
|
|
259
|
+
"%s Message processing for topic %s was cancelled.",
|
|
260
|
+
self.log_identifier,
|
|
261
|
+
topic,
|
|
262
|
+
)
|
|
263
|
+
elif future.done():
|
|
264
|
+
exception = future.exception()
|
|
265
|
+
if exception is not None:
|
|
266
|
+
log.error(
|
|
267
|
+
"%s Message processing for topic %s failed: %s",
|
|
268
|
+
self.log_identifier,
|
|
269
|
+
topic,
|
|
270
|
+
exception,
|
|
271
|
+
exc_info=exception,
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
# Handle successful completion
|
|
275
|
+
try:
|
|
276
|
+
_ = future.result()
|
|
277
|
+
log.debug(
|
|
278
|
+
"%s Message processing for topic %s completed successfully.",
|
|
279
|
+
self.log_identifier,
|
|
280
|
+
topic,
|
|
281
|
+
)
|
|
282
|
+
# Optional: Process the result if needed
|
|
283
|
+
# self._process_successful_result(result, topic)
|
|
284
|
+
except Exception as result_exception:
|
|
285
|
+
# This catches exceptions that might occur when getting the result
|
|
286
|
+
log.error(
|
|
287
|
+
"%s Error retrieving result for topic %s: %s",
|
|
288
|
+
self.log_identifier,
|
|
289
|
+
topic,
|
|
290
|
+
result_exception,
|
|
291
|
+
exc_info=result_exception,
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
# This case shouldn't normally occur in a completion callback,
|
|
295
|
+
# but it's good defensive programming
|
|
296
|
+
log.warning(
|
|
297
|
+
"%s Future for topic %s is not done in completion handler.",
|
|
298
|
+
self.log_identifier,
|
|
299
|
+
topic,
|
|
300
|
+
)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
log.error(
|
|
303
|
+
"%s Error in async message completion handler for topic %s: %s",
|
|
304
|
+
self.log_identifier,
|
|
305
|
+
topic,
|
|
306
|
+
e,
|
|
307
|
+
exc_info=True,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@abc.abstractmethod
|
|
311
|
+
async def _handle_message_async(self, message, topic: str) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Async handler for incoming messages.
|
|
314
|
+
|
|
315
|
+
Subclasses must implement this to process messages asynchronously.
|
|
316
|
+
This runs on the component's dedicated async event loop.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
message: The Solace message (SolaceMessage instance)
|
|
320
|
+
topic: The topic the message was received on
|
|
321
|
+
"""
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
def _late_init(self):
|
|
325
|
+
"""Late initialization hook called after the component is fully set up."""
|
|
326
|
+
|
|
327
|
+
# Setup the Trust Manager if present (enterprise feature)
|
|
328
|
+
# NOTE: The Trust Manager should use component.get_broker_username() to retrieve
|
|
329
|
+
# the actual broker client-username for trust card topic construction. This is
|
|
330
|
+
# critical because trust cards MUST be published on topics that match the actual
|
|
331
|
+
# authentication identity (client-username) used to connect to the broker.
|
|
332
|
+
try:
|
|
333
|
+
from solace_agent_mesh_enterprise.common.trust import (
|
|
334
|
+
initialize_trust_manager,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
trust_config = self.get_config("trust_manager")
|
|
338
|
+
if trust_config and trust_config.get("enabled", False):
|
|
339
|
+
self.trust_manager = initialize_trust_manager(self)
|
|
340
|
+
log.info("%s Enterprise Trust Manager initialized", self.log_identifier)
|
|
341
|
+
except ImportError:
|
|
342
|
+
log.debug("%s Enterprise Trust Manager not available", self.log_identifier)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
log.error(
|
|
345
|
+
"%s Failed to initialize Trust Manager: %s", self.log_identifier, e
|
|
346
|
+
)
|
|
58
347
|
|
|
59
348
|
def publish_a2a_message(
|
|
60
349
|
self, payload: dict, topic: str, user_properties: dict | None = None
|
|
@@ -62,7 +351,10 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
62
351
|
"""Helper to publish A2A messages via the SAC App with size validation."""
|
|
63
352
|
try:
|
|
64
353
|
log.debug(
|
|
65
|
-
|
|
354
|
+
"%s [publish_a2a_message] Starting - topic: %s, payload keys: %s",
|
|
355
|
+
self.log_identifier,
|
|
356
|
+
topic,
|
|
357
|
+
list(payload.keys()) if isinstance(payload, dict) else "not_dict"
|
|
66
358
|
)
|
|
67
359
|
|
|
68
360
|
# Validate message size
|
|
@@ -75,14 +367,14 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
75
367
|
f"Message size validation failed: payload size ({actual_size} bytes) "
|
|
76
368
|
f"exceeds maximum allowed size ({self.max_message_size_bytes} bytes)"
|
|
77
369
|
)
|
|
78
|
-
log.error("%s %s", self.log_identifier, error_msg)
|
|
370
|
+
log.error("%s [publish_a2a_message] %s", self.log_identifier, error_msg)
|
|
79
371
|
raise MessageSizeExceededError(
|
|
80
372
|
actual_size, self.max_message_size_bytes, error_msg
|
|
81
373
|
)
|
|
82
374
|
|
|
83
375
|
# Debug logging to show message size when publishing
|
|
84
376
|
log.debug(
|
|
85
|
-
"%s Publishing message to topic %s (size: %d bytes)",
|
|
377
|
+
"%s [publish_a2a_message] Publishing message to topic %s (size: %d bytes)",
|
|
86
378
|
self.log_identifier,
|
|
87
379
|
topic,
|
|
88
380
|
actual_size,
|
|
@@ -91,7 +383,8 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
91
383
|
app = self.get_app()
|
|
92
384
|
if app:
|
|
93
385
|
log.debug(
|
|
94
|
-
|
|
386
|
+
"%s [publish_a2a_message] Got app instance, about to call app.send_message",
|
|
387
|
+
self.log_identifier
|
|
95
388
|
)
|
|
96
389
|
|
|
97
390
|
# Conditionally log to invocation monitor if it exists (i.e., on an agent)
|
|
@@ -103,19 +396,24 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
103
396
|
component_identifier=self.log_identifier,
|
|
104
397
|
)
|
|
105
398
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
399
|
+
if trace_logger.isEnabledFor(logging.DEBUG):
|
|
400
|
+
trace_logger.debug(
|
|
401
|
+
"%s [publish_a2a_message] About to call app.send_message on topic '%s'\nwith payload: %s\nwith user_properties: %s",
|
|
402
|
+
self.log_identifier, topic, payload, user_properties
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
log.debug(
|
|
406
|
+
"%s [publish_a2a_message] About to call app.send_message on topic '%s' (for more details, enable TRACE logging)",
|
|
407
|
+
self.log_identifier, topic
|
|
408
|
+
)
|
|
112
409
|
|
|
113
410
|
app.send_message(
|
|
114
411
|
payload=payload, topic=topic, user_properties=user_properties
|
|
115
412
|
)
|
|
116
413
|
|
|
117
414
|
log.debug(
|
|
118
|
-
|
|
415
|
+
"%s [publish_a2a_message] Successfully called app.send_message on topic '%s'",
|
|
416
|
+
self.log_identifier, topic
|
|
119
417
|
)
|
|
120
418
|
else:
|
|
121
419
|
log.error(
|
|
@@ -146,15 +444,11 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
146
444
|
main_task = None
|
|
147
445
|
try:
|
|
148
446
|
log.info(
|
|
149
|
-
"%s Starting _async_setup_and_run as an asyncio task.",
|
|
447
|
+
"%s Starting _async_setup_and_run as an asyncio task. Will run event loop forever (or until stop_signal).",
|
|
150
448
|
self.log_identifier,
|
|
151
449
|
)
|
|
152
450
|
main_task = self._async_loop.create_task(self._async_setup_and_run())
|
|
153
451
|
|
|
154
|
-
log.info(
|
|
155
|
-
"%s Running asyncio event loop forever (or until stop_signal).",
|
|
156
|
-
self.log_identifier,
|
|
157
|
-
)
|
|
158
452
|
self._async_loop.run_forever()
|
|
159
453
|
|
|
160
454
|
except Exception as e:
|
|
@@ -197,6 +491,10 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
197
491
|
def run(self):
|
|
198
492
|
"""Starts the component's dedicated async thread."""
|
|
199
493
|
log.info("%s Starting SamComponentBase run method.", self.log_identifier)
|
|
494
|
+
|
|
495
|
+
# Do all initialization that needs to be done after we are fully setup
|
|
496
|
+
self._late_init()
|
|
497
|
+
|
|
200
498
|
if not self._async_thread or not self._async_thread.is_alive():
|
|
201
499
|
self._async_thread = threading.Thread(
|
|
202
500
|
target=self._run_async_operations,
|
|
@@ -256,14 +554,101 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
256
554
|
"""Returns the dedicated asyncio event loop for this component's async tasks."""
|
|
257
555
|
return self._async_loop
|
|
258
556
|
|
|
557
|
+
def get_broker_username(self) -> Optional[str]:
|
|
558
|
+
"""
|
|
559
|
+
Returns the broker username (client-username) that this component uses
|
|
560
|
+
to authenticate with the Solace broker.
|
|
561
|
+
|
|
562
|
+
This is critical for trust card publishing and verification, as the
|
|
563
|
+
trust card topic must match the actual authentication identity.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
The broker username if available, None otherwise.
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
app = self.get_app()
|
|
570
|
+
if app and hasattr(app, "app_info"):
|
|
571
|
+
broker_config = app.app_info.get("broker", {})
|
|
572
|
+
broker_username = broker_config.get("broker_username")
|
|
573
|
+
if broker_username:
|
|
574
|
+
log.debug(
|
|
575
|
+
"%s Retrieved broker username: %s",
|
|
576
|
+
self.log_identifier,
|
|
577
|
+
broker_username,
|
|
578
|
+
)
|
|
579
|
+
return broker_username
|
|
580
|
+
else:
|
|
581
|
+
log.warning(
|
|
582
|
+
"%s Broker username not found in broker configuration",
|
|
583
|
+
self.log_identifier,
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
log.warning(
|
|
587
|
+
"%s Unable to access app or app_info to retrieve broker username",
|
|
588
|
+
self.log_identifier,
|
|
589
|
+
)
|
|
590
|
+
except Exception as e:
|
|
591
|
+
log.error(
|
|
592
|
+
"%s Error retrieving broker username: %s",
|
|
593
|
+
self.log_identifier,
|
|
594
|
+
e,
|
|
595
|
+
exc_info=True,
|
|
596
|
+
)
|
|
597
|
+
return None
|
|
598
|
+
|
|
259
599
|
@abc.abstractmethod
|
|
260
|
-
|
|
600
|
+
def _get_component_id(self) -> str:
|
|
261
601
|
"""
|
|
262
|
-
|
|
263
|
-
|
|
602
|
+
Returns unique identifier for this component instance.
|
|
603
|
+
Must be implemented by subclasses.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Unique component identifier (e.g., agent_name, gateway_id)
|
|
264
607
|
"""
|
|
265
608
|
pass
|
|
266
609
|
|
|
610
|
+
@abc.abstractmethod
|
|
611
|
+
def _get_component_type(self) -> str:
|
|
612
|
+
"""
|
|
613
|
+
Returns component type string.
|
|
614
|
+
Must be implemented by subclasses.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
Component type ("gateway", "agent", etc.)
|
|
618
|
+
"""
|
|
619
|
+
pass
|
|
620
|
+
|
|
621
|
+
async def _async_setup_and_run(self) -> None:
|
|
622
|
+
"""
|
|
623
|
+
Base async setup that initializes Trust Manager if present.
|
|
624
|
+
Subclasses should override and call super() first, then add their logic.
|
|
625
|
+
"""
|
|
626
|
+
# Initialize Trust Manager if present (ENTERPRISE FEATURE)
|
|
627
|
+
if self.trust_manager:
|
|
628
|
+
try:
|
|
629
|
+
log.info(
|
|
630
|
+
"%s Initializing Trust Manager with periodic publishing...",
|
|
631
|
+
self.log_identifier,
|
|
632
|
+
)
|
|
633
|
+
# Pass event loop and add_timer method to Trust Manager
|
|
634
|
+
await self.trust_manager.initialize(
|
|
635
|
+
add_timer_callback=self.add_timer,
|
|
636
|
+
event_loop=self.get_async_loop(),
|
|
637
|
+
)
|
|
638
|
+
log.info(
|
|
639
|
+
"%s Initialized Trust Manager", self.log_identifier
|
|
640
|
+
)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
log.error(
|
|
643
|
+
"%s Failed to initialize Trust Manager: %s",
|
|
644
|
+
self.log_identifier,
|
|
645
|
+
e,
|
|
646
|
+
exc_info=True,
|
|
647
|
+
)
|
|
648
|
+
# Trust Manager failure should not prevent component startup
|
|
649
|
+
# Set to None to disable trust manager for this session
|
|
650
|
+
self.trust_manager = None
|
|
651
|
+
|
|
267
652
|
@abc.abstractmethod
|
|
268
653
|
def _pre_async_cleanup(self) -> None:
|
|
269
654
|
"""
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Provides a Pydantic BaseModel for SAM configuration with dict-like access."""
|
|
2
|
-
from pydantic import BaseModel
|
|
3
|
-
from typing import Any,
|
|
2
|
+
from pydantic import BaseModel, ValidationError
|
|
3
|
+
from typing import Any, TypeVar, Union, get_args, get_origin
|
|
4
|
+
from types import UnionType
|
|
4
5
|
|
|
5
6
|
T = TypeVar("T", bound="SamConfigBase")
|
|
6
7
|
|
|
@@ -13,7 +14,7 @@ class SamConfigBase(BaseModel):
|
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
@classmethod
|
|
16
|
-
def model_validate_and_clean(cls:
|
|
17
|
+
def model_validate_and_clean(cls: type[T], obj: Any) -> T:
|
|
17
18
|
"""
|
|
18
19
|
Validates a dictionary, first removing any keys with None values.
|
|
19
20
|
This allows Pydantic's default values to be applied correctly when
|
|
@@ -24,6 +25,92 @@ class SamConfigBase(BaseModel):
|
|
|
24
25
|
return cls.model_validate(cleaned_obj)
|
|
25
26
|
return cls.model_validate(obj)
|
|
26
27
|
|
|
28
|
+
@classmethod
|
|
29
|
+
def format_validation_error_message(cls: type[T], error: ValidationError, app_name: str | None, agent_name: str | None = None) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Formats Pydantic validation error messages into a clear, actionable format.
|
|
32
|
+
|
|
33
|
+
Example output:
|
|
34
|
+
---- Configuration validation failed for 'my-agent-app' ----
|
|
35
|
+
|
|
36
|
+
Agent Name: AgentConfig
|
|
37
|
+
|
|
38
|
+
ERROR 1:
|
|
39
|
+
Missing required field: 'namespace'
|
|
40
|
+
Location: app_config.namespace
|
|
41
|
+
Description: Absolute topic prefix for A2A communication (e.g., 'myorg/dev')
|
|
42
|
+
|
|
43
|
+
---- Please update your YAML configuration ----
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
error_lines = [
|
|
47
|
+
f"\n---- Configuration validation failed for {app_name or 'UNKNOWN'} ----",
|
|
48
|
+
""
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
if agent_name:
|
|
52
|
+
error_lines.append(f" Agent Name: {agent_name}\n")
|
|
53
|
+
|
|
54
|
+
def get_nested_field_description(model_class: type[BaseModel], path: list[str | int]) -> str | None:
|
|
55
|
+
"""Recursively get field description from nested models"""
|
|
56
|
+
if not path:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
current_field = path[0]
|
|
60
|
+
if str(current_field) not in model_class.model_fields:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
field_info = model_class.model_fields[str(current_field)]
|
|
64
|
+
|
|
65
|
+
if len(path) == 1:
|
|
66
|
+
return field_info.description
|
|
67
|
+
|
|
68
|
+
annotation = field_info.annotation
|
|
69
|
+
|
|
70
|
+
# Handle Optional/Union types
|
|
71
|
+
if annotation is not None:
|
|
72
|
+
origin = get_origin(annotation)
|
|
73
|
+
if origin is Union or origin is UnionType:
|
|
74
|
+
types = get_args(annotation)
|
|
75
|
+
annotation = next((t for t in types if t is not type(None)), None)
|
|
76
|
+
elif origin is list:
|
|
77
|
+
inner_type = get_args(annotation)[0]
|
|
78
|
+
if len(path) > 1 and isinstance(path[1], int):
|
|
79
|
+
if isinstance(inner_type, type) and issubclass(inner_type, BaseModel):
|
|
80
|
+
return get_nested_field_description(inner_type, path[2:])
|
|
81
|
+
return None
|
|
82
|
+
annotation = inner_type
|
|
83
|
+
|
|
84
|
+
if annotation is not None and isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
85
|
+
return get_nested_field_description(annotation, path[1:])
|
|
86
|
+
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
for index, err in enumerate(error.errors()):
|
|
91
|
+
error_type = err.get('type')
|
|
92
|
+
loc = err['loc']
|
|
93
|
+
msg = err['msg']
|
|
94
|
+
|
|
95
|
+
error_lines.append(f"ERROR {index + 1}:")
|
|
96
|
+
|
|
97
|
+
absolute_path = '.'.join(str(item) for item in loc)
|
|
98
|
+
description = get_nested_field_description(cls, list(loc))
|
|
99
|
+
if error_type == 'missing':
|
|
100
|
+
error_lines.extend([
|
|
101
|
+
f" Missing required field: '{loc[-1]}'",
|
|
102
|
+
])
|
|
103
|
+
else:
|
|
104
|
+
error_lines.extend([
|
|
105
|
+
f" Error: {msg}",
|
|
106
|
+
])
|
|
107
|
+
error_lines.append(f" Location: app_config.{absolute_path}")
|
|
108
|
+
error_lines.append(f" Description: {description or 'UNKNOWN'}")
|
|
109
|
+
error_lines.append("")
|
|
110
|
+
|
|
111
|
+
error_lines.append('---- Please update your YAML configuration ----')
|
|
112
|
+
return '\n'.join(error_lines) + "\n"
|
|
113
|
+
|
|
27
114
|
def get(self, key: str, default: Any = None) -> Any:
|
|
28
115
|
"""Provides dict-like .get() method."""
|
|
29
116
|
return getattr(self, key, default)
|
|
@@ -19,6 +19,7 @@ from ...common.a2a import (
|
|
|
19
19
|
|
|
20
20
|
log = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
class BaseGatewayComponent(ComponentBase):
|
|
23
24
|
pass
|
|
24
25
|
|
|
@@ -244,6 +245,20 @@ class BaseGatewayApp(App):
|
|
|
244
245
|
)
|
|
245
246
|
},
|
|
246
247
|
]
|
|
248
|
+
|
|
249
|
+
# Add trust card subscription if trust manager is enabled
|
|
250
|
+
trust_config = resolved_app_config_block.get("trust_manager")
|
|
251
|
+
if trust_config and trust_config.get("enabled", False):
|
|
252
|
+
from ...common.a2a.protocol import get_trust_card_subscription_topic
|
|
253
|
+
|
|
254
|
+
trust_card_topic = get_trust_card_subscription_topic(self.namespace)
|
|
255
|
+
subscriptions.append({"topic": trust_card_topic})
|
|
256
|
+
log.info(
|
|
257
|
+
"Trust Manager enabled for gateway '%s', added trust card subscription: %s",
|
|
258
|
+
self.gateway_id,
|
|
259
|
+
trust_card_topic,
|
|
260
|
+
)
|
|
261
|
+
|
|
247
262
|
log.info(
|
|
248
263
|
"Generated Solace subscriptions for gateway '%s': %s",
|
|
249
264
|
self.gateway_id,
|