solace-agent-mesh 1.3.3__py3-none-any.whl → 1.4.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/setup.py +183 -8
- solace_agent_mesh/agent/sac/app.py +337 -622
- solace_agent_mesh/agent/sac/component.py +47 -1
- solace_agent_mesh/agent/tools/dynamic_tool.py +36 -5
- solace_agent_mesh/agent/tools/tool_config_types.py +58 -0
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/42b3f8d8.508ae8db.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9a09e75d.92de8cf5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.e82b32e6.js → main.1de3da6a.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/{runtime~main.aad1f874.js → runtime~main.3188e049.js} +1 -1
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/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/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/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/quick-start/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +68 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1757991496554.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1757991496554.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/run_cmd.py +4 -7
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-CAX9u8a7.js → authCallback-j1LW-wlq.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{client-DXU9SPI5.js → client-B9p_nFNA.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-B9s_V9tJ.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-Dq4AJNvn.js +339 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{vendor-B0BEKoAR.js → vendor-CS5YMf8a.js} +74 -69
- 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/utils/pydantic_utils.py +56 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/registry_manager.py +6 -4
- solace_agent_mesh/gateway/base/app.py +58 -120
- solace_agent_mesh/gateway/http_sse/app.py +99 -150
- solace_agent_mesh/gateway/http_sse/component.py +57 -30
- solace_agent_mesh/gateway/http_sse/sse_event_buffer.py +87 -0
- solace_agent_mesh/gateway/http_sse/sse_manager.py +44 -23
- {solace_agent_mesh-1.3.3.dist-info → solace_agent_mesh-1.4.0.dist-info}/METADATA +1 -1
- {solace_agent_mesh-1.3.3.dist-info → solace_agent_mesh-1.4.0.dist-info}/RECORD +74 -71
- solace_agent_mesh/assets/docs/assets/js/42b3f8d8.3f34bf76.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/9a09e75d.5a319fd4.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1757873594308.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1757873594308.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-C03yrETa.css +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-DjoMeldu.js +0 -339
- /solace_agent_mesh/assets/docs/assets/js/{main.e82b32e6.js.LICENSE.txt → main.1de3da6a.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.3.3.dist-info → solace_agent_mesh-1.4.0.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.3.3.dist-info → solace_agent_mesh-1.4.0.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.3.3.dist-info → solace_agent_mesh-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,12 +17,14 @@ from fastapi import Request as FastAPIRequest
|
|
|
17
17
|
from solace_ai_connector.common.log import log
|
|
18
18
|
from solace_ai_connector.components.inputs_outputs.broker_input import BrokerInput
|
|
19
19
|
from solace_ai_connector.flow.app import App as SACApp
|
|
20
|
+
from solace_ai_connector.common.event import Event, EventType
|
|
20
21
|
|
|
21
22
|
from ...common.agent_registry import AgentRegistry
|
|
22
23
|
from ...core_a2a.service import CoreA2AService
|
|
23
24
|
from ...gateway.base.component import BaseGatewayComponent
|
|
24
25
|
from ...gateway.http_sse.session_manager import SessionManager
|
|
25
26
|
from ...gateway.http_sse.sse_manager import SSEManager
|
|
27
|
+
from .sse_event_buffer import SSEEventBuffer
|
|
26
28
|
from .components import VisualizationForwarderComponent
|
|
27
29
|
|
|
28
30
|
try:
|
|
@@ -118,8 +120,23 @@ class WebUIBackendComponent(BaseGatewayComponent):
|
|
|
118
120
|
raise ValueError(f"Configuration retrieval error: {e}") from e
|
|
119
121
|
|
|
120
122
|
sse_max_queue_size = self.get_config("sse_max_queue_size", 200)
|
|
123
|
+
sse_buffer_max_age_seconds = self.get_config("sse_buffer_max_age_seconds", 600)
|
|
121
124
|
|
|
122
|
-
self.
|
|
125
|
+
self.sse_event_buffer = SSEEventBuffer(
|
|
126
|
+
max_queue_size=sse_max_queue_size,
|
|
127
|
+
max_age_seconds=sse_buffer_max_age_seconds,
|
|
128
|
+
)
|
|
129
|
+
self.sse_manager = SSEManager(
|
|
130
|
+
max_queue_size=sse_max_queue_size, event_buffer=self.sse_event_buffer
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self._sse_cleanup_timer_id = f"sse_cleanup_{self.gateway_id}"
|
|
134
|
+
cleanup_interval_sec = self.get_config("sse_buffer_cleanup_interval_seconds", 300)
|
|
135
|
+
self.add_timer(
|
|
136
|
+
delay_ms=cleanup_interval_sec * 1000,
|
|
137
|
+
timer_id=self._sse_cleanup_timer_id,
|
|
138
|
+
interval_ms=cleanup_interval_sec * 1000,
|
|
139
|
+
)
|
|
123
140
|
|
|
124
141
|
session_config = self._resolve_session_config()
|
|
125
142
|
if session_config.get("type") == "sql":
|
|
@@ -167,6 +184,15 @@ class WebUIBackendComponent(BaseGatewayComponent):
|
|
|
167
184
|
|
|
168
185
|
log.info("%s Web UI Backend Component initialized.", self.log_identifier)
|
|
169
186
|
|
|
187
|
+
def process_event(self, event: Event):
|
|
188
|
+
if event.event_type == EventType.TIMER:
|
|
189
|
+
if event.data.get("timer_id") == self._sse_cleanup_timer_id:
|
|
190
|
+
log.debug("%s SSE buffer cleanup timer triggered.", self.log_identifier)
|
|
191
|
+
self.sse_event_buffer.cleanup_stale_buffers()
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
super().process_event(event)
|
|
195
|
+
|
|
170
196
|
def _get_visualization_lock(self) -> asyncio.Lock:
|
|
171
197
|
"""Get or create a visualization lock for the current event loop."""
|
|
172
198
|
try:
|
|
@@ -1025,6 +1051,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
|
|
|
1025
1051
|
def cleanup(self):
|
|
1026
1052
|
"""Gracefully shuts down the component and the FastAPI server."""
|
|
1027
1053
|
log.info("%s Cleaning up Web UI Backend Component...", self.log_identifier)
|
|
1054
|
+
self.cancel_timer(self._sse_cleanup_timer_id)
|
|
1028
1055
|
log.info("%s Cleaning up visualization resources...", self.log_identifier)
|
|
1029
1056
|
if self._visualization_message_queue:
|
|
1030
1057
|
self._visualization_message_queue.put(None)
|
|
@@ -1056,6 +1083,35 @@ class WebUIBackendComponent(BaseGatewayComponent):
|
|
|
1056
1083
|
self._cleanup_visualization_locks()
|
|
1057
1084
|
log.info("%s Visualization resources cleaned up.", self.log_identifier)
|
|
1058
1085
|
|
|
1086
|
+
super().cleanup()
|
|
1087
|
+
|
|
1088
|
+
if self.fastapi_thread and self.fastapi_thread.is_alive():
|
|
1089
|
+
log.info(
|
|
1090
|
+
"%s Waiting for FastAPI server thread to exit...", self.log_identifier
|
|
1091
|
+
)
|
|
1092
|
+
self.fastapi_thread.join(timeout=10)
|
|
1093
|
+
if self.fastapi_thread.is_alive():
|
|
1094
|
+
log.warning(
|
|
1095
|
+
"%s FastAPI server thread did not exit gracefully.",
|
|
1096
|
+
self.log_identifier,
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
if self.sse_manager:
|
|
1100
|
+
log.info(
|
|
1101
|
+
"%s Closing active SSE connections (best effort)...",
|
|
1102
|
+
self.log_identifier,
|
|
1103
|
+
)
|
|
1104
|
+
try:
|
|
1105
|
+
asyncio.run(self.sse_manager.close_all())
|
|
1106
|
+
except Exception as sse_close_err:
|
|
1107
|
+
log.error(
|
|
1108
|
+
"%s Error closing SSE connections during cleanup: %s",
|
|
1109
|
+
self.log_identifier,
|
|
1110
|
+
sse_close_err,
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
log.info("%s Web UI Backend Component cleanup finished.", self.log_identifier)
|
|
1114
|
+
|
|
1059
1115
|
def _infer_visualization_event_details(
|
|
1060
1116
|
self, topic: str, payload: dict[str, Any]
|
|
1061
1117
|
) -> dict[str, Any]:
|
|
@@ -1302,35 +1358,6 @@ class WebUIBackendComponent(BaseGatewayComponent):
|
|
|
1302
1358
|
)
|
|
1303
1359
|
return agents
|
|
1304
1360
|
|
|
1305
|
-
super().cleanup()
|
|
1306
|
-
|
|
1307
|
-
if self.fastapi_thread and self.fastapi_thread.is_alive():
|
|
1308
|
-
log.info(
|
|
1309
|
-
"%s Waiting for FastAPI server thread to exit...", self.log_identifier
|
|
1310
|
-
)
|
|
1311
|
-
self.fastapi_thread.join(timeout=10)
|
|
1312
|
-
if self.fastapi_thread.is_alive():
|
|
1313
|
-
log.warning(
|
|
1314
|
-
"%s FastAPI server thread did not exit gracefully.",
|
|
1315
|
-
self.log_identifier,
|
|
1316
|
-
)
|
|
1317
|
-
|
|
1318
|
-
if self.sse_manager:
|
|
1319
|
-
log.info(
|
|
1320
|
-
"%s Closing active SSE connections (best effort)...",
|
|
1321
|
-
self.log_identifier,
|
|
1322
|
-
)
|
|
1323
|
-
try:
|
|
1324
|
-
asyncio.run(self.sse_manager.close_all())
|
|
1325
|
-
except Exception as sse_close_err:
|
|
1326
|
-
log.error(
|
|
1327
|
-
"%s Error closing SSE connections during cleanup: %s",
|
|
1328
|
-
self.log_identifier,
|
|
1329
|
-
sse_close_err,
|
|
1330
|
-
)
|
|
1331
|
-
|
|
1332
|
-
log.info("%s Web UI Backend Component cleanup finished.", self.log_identifier)
|
|
1333
|
-
|
|
1334
1361
|
def get_agent_registry(self) -> AgentRegistry:
|
|
1335
1362
|
return self.agent_registry
|
|
1336
1363
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A thread-safe buffer for holding early SSE events before a client connects.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from solace_ai_connector.common.log import log
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SSEEventBuffer:
|
|
13
|
+
"""Manages buffering and cleanup of SSE events for tasks without active listeners."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, max_queue_size: int, max_age_seconds: int):
|
|
16
|
+
self._pending_events: Dict[
|
|
17
|
+
str, Tuple[datetime.datetime, List[Dict[str, Any]]]
|
|
18
|
+
] = {}
|
|
19
|
+
self._lock = threading.Lock()
|
|
20
|
+
self._max_queue_size = max_queue_size
|
|
21
|
+
self._max_age_seconds = max_age_seconds
|
|
22
|
+
self.log_identifier = "[SSEEventBuffer]"
|
|
23
|
+
log.debug(
|
|
24
|
+
"%s Initialized with max_age:%ds, max_size:%d",
|
|
25
|
+
self.log_identifier,
|
|
26
|
+
self._max_age_seconds,
|
|
27
|
+
self._max_queue_size,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def buffer_event(self, task_id: str, event: Dict[str, Any]):
|
|
31
|
+
"""Buffers an event for a given task ID."""
|
|
32
|
+
with self._lock:
|
|
33
|
+
if task_id not in self._pending_events:
|
|
34
|
+
self._pending_events[task_id] = (
|
|
35
|
+
datetime.datetime.now(datetime.timezone.utc),
|
|
36
|
+
[],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if len(self._pending_events[task_id][1]) < self._max_queue_size:
|
|
40
|
+
self._pending_events[task_id][1].append(event)
|
|
41
|
+
else:
|
|
42
|
+
log.warning(
|
|
43
|
+
"%s Buffer full for Task ID: %s. Event dropped.",
|
|
44
|
+
self.log_identifier,
|
|
45
|
+
task_id,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_and_remove_buffer(self, task_id: str) -> Optional[List[Dict[str, Any]]]:
|
|
49
|
+
"""Atomically retrieves and removes the event buffer for a task."""
|
|
50
|
+
with self._lock:
|
|
51
|
+
buffer_tuple = self._pending_events.pop(task_id, None)
|
|
52
|
+
if buffer_tuple:
|
|
53
|
+
log.debug(
|
|
54
|
+
"%s Flushing %d events for Task ID: %s",
|
|
55
|
+
self.log_identifier,
|
|
56
|
+
len(buffer_tuple[1]),
|
|
57
|
+
task_id,
|
|
58
|
+
)
|
|
59
|
+
return buffer_tuple[1]
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def remove_buffer(self, task_id: str):
|
|
63
|
+
"""Explicitly removes a buffer for a task, e.g., on finalization."""
|
|
64
|
+
with self._lock:
|
|
65
|
+
if self._pending_events.pop(task_id, None):
|
|
66
|
+
log.debug(
|
|
67
|
+
"%s Removed buffer for task %s.", self.log_identifier, task_id
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def cleanup_stale_buffers(self):
|
|
71
|
+
"""Removes all pending event buffers older than the max age."""
|
|
72
|
+
with self._lock:
|
|
73
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
74
|
+
stale_tasks = [
|
|
75
|
+
task_id
|
|
76
|
+
for task_id, (timestamp, _) in self._pending_events.items()
|
|
77
|
+
if (now - timestamp).total_seconds() > self._max_age_seconds
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
if stale_tasks:
|
|
81
|
+
log.debug(
|
|
82
|
+
"%s Cleaning up %d stale event buffers.",
|
|
83
|
+
self.log_identifier,
|
|
84
|
+
len(stale_tasks),
|
|
85
|
+
)
|
|
86
|
+
for task_id in stale_tasks:
|
|
87
|
+
del self._pending_events[task_id]
|
|
@@ -11,6 +11,8 @@ import math
|
|
|
11
11
|
|
|
12
12
|
from solace_ai_connector.common.log import log
|
|
13
13
|
|
|
14
|
+
from .sse_event_buffer import SSEEventBuffer
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class SSEManager:
|
|
16
18
|
"""
|
|
@@ -18,8 +20,9 @@ class SSEManager:
|
|
|
18
20
|
Uses asyncio Queues for buffering events per connection.
|
|
19
21
|
"""
|
|
20
22
|
|
|
21
|
-
def __init__(self, max_queue_size: int
|
|
23
|
+
def __init__(self, max_queue_size: int, event_buffer: SSEEventBuffer):
|
|
22
24
|
self._connections: Dict[str, List[asyncio.Queue]] = {}
|
|
25
|
+
self._event_buffer = event_buffer
|
|
23
26
|
self._locks: Dict[asyncio.AbstractEventLoop, asyncio.Lock] = {}
|
|
24
27
|
self._locks_lock = threading.Lock()
|
|
25
28
|
self.log_identifier = "[SSEManager]"
|
|
@@ -64,7 +67,6 @@ class SSEManager:
|
|
|
64
67
|
else:
|
|
65
68
|
return str(obj)
|
|
66
69
|
|
|
67
|
-
|
|
68
70
|
async def create_sse_connection(self, task_id: str) -> asyncio.Queue:
|
|
69
71
|
"""
|
|
70
72
|
Creates a new queue for an SSE connection subscribing to a task.
|
|
@@ -81,8 +83,15 @@ class SSEManager:
|
|
|
81
83
|
self._connections[task_id] = []
|
|
82
84
|
|
|
83
85
|
connection_queue = asyncio.Queue(maxsize=self._max_queue_size)
|
|
86
|
+
|
|
87
|
+
# Flush any pending events from the buffer to the new connection
|
|
88
|
+
buffered_events = self._event_buffer.get_and_remove_buffer(task_id)
|
|
89
|
+
if buffered_events:
|
|
90
|
+
for event in buffered_events:
|
|
91
|
+
await connection_queue.put(event)
|
|
92
|
+
|
|
84
93
|
self._connections[task_id].append(connection_queue)
|
|
85
|
-
log.
|
|
94
|
+
log.debug(
|
|
86
95
|
"%s Created SSE connection queue for Task ID: %s. Total queues for task: %d",
|
|
87
96
|
self.log_identifier,
|
|
88
97
|
task_id,
|
|
@@ -105,7 +114,7 @@ class SSEManager:
|
|
|
105
114
|
if task_id in self._connections:
|
|
106
115
|
try:
|
|
107
116
|
self._connections[task_id].remove(connection_queue)
|
|
108
|
-
log.
|
|
117
|
+
log.debug(
|
|
109
118
|
"%s Removed SSE connection queue for Task ID: %s. Remaining queues: %d",
|
|
110
119
|
self.log_identifier,
|
|
111
120
|
task_id,
|
|
@@ -113,7 +122,7 @@ class SSEManager:
|
|
|
113
122
|
)
|
|
114
123
|
if not self._connections[task_id]:
|
|
115
124
|
del self._connections[task_id]
|
|
116
|
-
log.
|
|
125
|
+
log.debug(
|
|
117
126
|
"%s Removed Task ID entry: %s as no connections remain.",
|
|
118
127
|
self.log_identifier,
|
|
119
128
|
task_id,
|
|
@@ -145,19 +154,11 @@ class SSEManager:
|
|
|
145
154
|
"""
|
|
146
155
|
lock = self._get_lock()
|
|
147
156
|
async with lock:
|
|
148
|
-
|
|
149
|
-
log.debug(
|
|
150
|
-
"%s No active SSE connections for Task ID: %s. Event not sent.",
|
|
151
|
-
self.log_identifier,
|
|
152
|
-
task_id,
|
|
153
|
-
)
|
|
154
|
-
return
|
|
157
|
+
queues = self._connections.get(task_id)
|
|
155
158
|
|
|
156
|
-
queues_to_remove = []
|
|
157
159
|
try:
|
|
158
160
|
serialized_data = json.dumps(
|
|
159
|
-
self._sanitize_json(event_data),
|
|
160
|
-
allow_nan=False
|
|
161
|
+
self._sanitize_json(event_data), allow_nan=False
|
|
161
162
|
)
|
|
162
163
|
except Exception as json_err:
|
|
163
164
|
log.error(
|
|
@@ -169,6 +170,16 @@ class SSEManager:
|
|
|
169
170
|
return
|
|
170
171
|
|
|
171
172
|
sse_payload = {"event": event_type, "data": serialized_data}
|
|
173
|
+
|
|
174
|
+
if not queues:
|
|
175
|
+
log.debug(
|
|
176
|
+
"%s No active SSE connections for Task ID: %s. Buffering event.",
|
|
177
|
+
self.log_identifier,
|
|
178
|
+
task_id,
|
|
179
|
+
)
|
|
180
|
+
self._event_buffer.buffer_event(task_id, sse_payload)
|
|
181
|
+
return
|
|
182
|
+
|
|
172
183
|
log.debug(
|
|
173
184
|
"%s Prepared SSE payload for Task ID %s: %s",
|
|
174
185
|
self.log_identifier,
|
|
@@ -176,6 +187,7 @@ class SSEManager:
|
|
|
176
187
|
sse_payload,
|
|
177
188
|
)
|
|
178
189
|
|
|
190
|
+
queues_to_remove = []
|
|
179
191
|
for connection_queue in list(self._connections.get(task_id, [])):
|
|
180
192
|
try:
|
|
181
193
|
await asyncio.wait_for(
|
|
@@ -224,7 +236,7 @@ class SSEManager:
|
|
|
224
236
|
|
|
225
237
|
if not current_queues:
|
|
226
238
|
del self._connections[task_id]
|
|
227
|
-
log.
|
|
239
|
+
log.debug(
|
|
228
240
|
"%s Removed Task ID entry: %s after cleaning queues.",
|
|
229
241
|
self.log_identifier,
|
|
230
242
|
task_id,
|
|
@@ -235,7 +247,7 @@ class SSEManager:
|
|
|
235
247
|
Signals a specific SSE connection queue to close by putting None.
|
|
236
248
|
Also removes the queue from the manager.
|
|
237
249
|
"""
|
|
238
|
-
log.
|
|
250
|
+
log.debug(
|
|
239
251
|
"%s Closing specific SSE connection queue for Task ID: %s",
|
|
240
252
|
self.log_identifier,
|
|
241
253
|
task_id,
|
|
@@ -267,13 +279,17 @@ class SSEManager:
|
|
|
267
279
|
async def close_all_for_task(self, task_id: str):
|
|
268
280
|
"""
|
|
269
281
|
Closes all SSE connections associated with a specific task.
|
|
282
|
+
If a connection existed, it also cleans up the event buffer.
|
|
283
|
+
If no connection ever existed, the buffer is left for a late-connecting client.
|
|
270
284
|
"""
|
|
271
285
|
lock = self._get_lock()
|
|
272
286
|
async with lock:
|
|
273
287
|
if task_id in self._connections:
|
|
288
|
+
# This is the "normal" case: a client is or was connected.
|
|
289
|
+
# It's safe to clean up everything.
|
|
274
290
|
queues_to_close = self._connections.pop(task_id)
|
|
275
|
-
log.
|
|
276
|
-
"%s Closing %d SSE connections for Task ID: %s",
|
|
291
|
+
log.debug(
|
|
292
|
+
"%s Closing %d SSE connections for Task ID: %s and cleaning up buffer.",
|
|
277
293
|
self.log_identifier,
|
|
278
294
|
len(queues_to_close),
|
|
279
295
|
task_id,
|
|
@@ -300,14 +316,19 @@ class SSEManager:
|
|
|
300
316
|
task_id,
|
|
301
317
|
e,
|
|
302
318
|
)
|
|
303
|
-
|
|
319
|
+
|
|
320
|
+
# Since a connection existed, the buffer is no longer needed.
|
|
321
|
+
self._event_buffer.remove_buffer(task_id)
|
|
322
|
+
log.debug(
|
|
304
323
|
"%s Removed Task ID entry: %s and signaled queues to close.",
|
|
305
324
|
self.log_identifier,
|
|
306
325
|
task_id,
|
|
307
326
|
)
|
|
308
327
|
else:
|
|
328
|
+
# This is the "race condition" case: no client has connected yet.
|
|
329
|
+
# We MUST leave the buffer intact for the late-connecting client.
|
|
309
330
|
log.debug(
|
|
310
|
-
"%s No connections found
|
|
331
|
+
"%s No active connections found for Task ID: %s. Leaving event buffer intact.",
|
|
311
332
|
self.log_identifier,
|
|
312
333
|
task_id,
|
|
313
334
|
)
|
|
@@ -329,7 +350,7 @@ class SSEManager:
|
|
|
329
350
|
self.cleanup_old_locks()
|
|
330
351
|
lock = self._get_lock()
|
|
331
352
|
async with lock:
|
|
332
|
-
log.
|
|
353
|
+
log.debug("%s Closing all active SSE connections...", self.log_identifier)
|
|
333
354
|
all_task_ids = list(self._connections.keys())
|
|
334
355
|
closed_count = 0
|
|
335
356
|
for task_id in all_task_ids:
|
|
@@ -341,7 +362,7 @@ class SSEManager:
|
|
|
341
362
|
await asyncio.wait_for(q.put(None), timeout=0.1)
|
|
342
363
|
except Exception:
|
|
343
364
|
pass
|
|
344
|
-
log.
|
|
365
|
+
log.debug(
|
|
345
366
|
"%s Closed %d connections for tasks: %s",
|
|
346
367
|
self.log_identifier,
|
|
347
368
|
closed_count,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solace-agent-mesh
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Solace Agent Mesh is an open-source framework for building event-driven, multi-agent AI systems where specialized agents collaborate on complex tasks.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SolaceLabs/solace-agent-mesh
|
|
6
6
|
Project-URL: Repository, https://github.com/SolaceLabs/solace-agent-mesh
|