solace-agent-mesh 1.0.9__py3-none-any.whl → 1.3.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 (220) hide show
  1. solace_agent_mesh/agent/adk/adk_llm.txt +182 -42
  2. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +171 -0
  3. solace_agent_mesh/agent/adk/callbacks.py +165 -104
  4. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +0 -18
  5. solace_agent_mesh/agent/adk/models/models_llm.txt +104 -55
  6. solace_agent_mesh/agent/adk/runner.py +25 -17
  7. solace_agent_mesh/agent/adk/services.py +3 -3
  8. solace_agent_mesh/agent/adk/setup.py +11 -0
  9. solace_agent_mesh/agent/adk/stream_parser.py +8 -1
  10. solace_agent_mesh/agent/adk/tool_wrapper.py +10 -3
  11. solace_agent_mesh/agent/agent_llm.txt +355 -18
  12. solace_agent_mesh/agent/protocol/event_handlers.py +460 -317
  13. solace_agent_mesh/agent/protocol/protocol_llm.txt +54 -7
  14. solace_agent_mesh/agent/sac/app.py +2 -2
  15. solace_agent_mesh/agent/sac/component.py +211 -517
  16. solace_agent_mesh/agent/sac/sac_llm.txt +133 -63
  17. solace_agent_mesh/agent/testing/testing_llm.txt +25 -58
  18. solace_agent_mesh/agent/tools/peer_agent_tool.py +15 -11
  19. solace_agent_mesh/agent/tools/tools_llm.txt +234 -69
  20. solace_agent_mesh/agent/utils/artifact_helpers.py +35 -1
  21. solace_agent_mesh/agent/utils/utils_llm.txt +90 -105
  22. solace_agent_mesh/assets/docs/404.html +3 -3
  23. solace_agent_mesh/assets/docs/assets/js/6e0db977.39a79ca9.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/{75384d09.ccd480c4.js → 75384d09.bf78fbdb.js} +1 -1
  25. solace_agent_mesh/assets/docs/assets/js/90dd9cf6.88f385ea.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/f284c35a.fb68323a.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/main.08d30374.js +2 -0
  28. solace_agent_mesh/assets/docs/assets/js/runtime~main.458efb1d.js +1 -0
  29. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +4 -4
  30. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +4 -4
  31. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +4 -4
  32. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +4 -4
  33. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +4 -4
  34. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +4 -4
  35. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
  36. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +4 -4
  37. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
  38. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +4 -4
  39. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +4 -4
  40. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/migration-guides/a2a-upgrade-to-0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +105 -0
  44. solace_agent_mesh/assets/docs/docs/documentation/migration-guides/a2a-upgrade-to-0.3.0/a2a-technical-migration-map/index.html +53 -0
  45. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +4 -4
  51. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +4 -4
  52. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +4 -4
  53. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  54. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +4 -4
  55. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +4 -4
  56. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +4 -4
  59. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +4 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
  61. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +4 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
  64. solace_agent_mesh/assets/docs/lunr-index-1757433031159.json +1 -0
  65. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  66. solace_agent_mesh/assets/docs/search-doc-1757433031159.json +1 -0
  67. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  68. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  69. solace_agent_mesh/cli/__init__.py +1 -1
  70. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +125 -48
  71. solace_agent_mesh/cli/commands/eval_cmd.py +14 -0
  72. solace_agent_mesh/cli/commands/init_cmd/__init__.py +53 -31
  73. solace_agent_mesh/cli/commands/init_cmd/database_step.py +91 -0
  74. solace_agent_mesh/cli/commands/init_cmd/env_step.py +19 -8
  75. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +80 -25
  76. solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +32 -10
  77. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +74 -15
  78. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +0 -2
  79. solace_agent_mesh/cli/commands/run_cmd.py +5 -3
  80. solace_agent_mesh/cli/utils.py +68 -12
  81. solace_agent_mesh/client/webui/frontend/static/assets/authCallback-vY5eu2lI.js +1 -0
  82. solace_agent_mesh/client/webui/frontend/static/assets/client-BeBkzgWW.js +25 -0
  83. solace_agent_mesh/client/webui/frontend/static/assets/main-Bjys1KQs.js +339 -0
  84. solace_agent_mesh/client/webui/frontend/static/assets/main-C03yrETa.css +1 -0
  85. solace_agent_mesh/client/webui/frontend/static/assets/vendor-CE0AeXyK.js +395 -0
  86. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -2
  87. solace_agent_mesh/client/webui/frontend/static/index.html +4 -3
  88. solace_agent_mesh/common/a2a/__init__.py +213 -0
  89. solace_agent_mesh/common/a2a/a2a_llm.txt +182 -0
  90. solace_agent_mesh/common/a2a/artifact.py +328 -0
  91. solace_agent_mesh/common/a2a/events.py +183 -0
  92. solace_agent_mesh/common/a2a/message.py +307 -0
  93. solace_agent_mesh/common/a2a/protocol.py +513 -0
  94. solace_agent_mesh/common/a2a/task.py +127 -0
  95. solace_agent_mesh/common/a2a/translation.py +653 -0
  96. solace_agent_mesh/common/a2a/types.py +54 -0
  97. solace_agent_mesh/common/a2a_spec/a2a.json +2576 -0
  98. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +407 -0
  99. solace_agent_mesh/common/a2a_spec/schemas/agent_progress_update.json +18 -0
  100. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +31 -0
  101. solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +18 -0
  102. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +235 -0
  103. solace_agent_mesh/common/a2a_spec/schemas/tool_invocation_start.json +26 -0
  104. solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +25 -0
  105. solace_agent_mesh/common/agent_registry.py +1 -1
  106. solace_agent_mesh/common/common_llm.txt +192 -70
  107. solace_agent_mesh/common/data_parts.py +99 -0
  108. solace_agent_mesh/common/middleware/middleware_llm.txt +17 -17
  109. solace_agent_mesh/common/sac/__init__.py +0 -0
  110. solace_agent_mesh/common/sac/sac_llm.txt +71 -0
  111. solace_agent_mesh/common/sac/sam_component_base.py +252 -0
  112. solace_agent_mesh/common/services/providers/providers_llm.txt +51 -84
  113. solace_agent_mesh/common/services/services_llm.txt +206 -26
  114. solace_agent_mesh/common/utils/artifact_utils.py +29 -0
  115. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +176 -80
  116. solace_agent_mesh/common/utils/embeds/resolver.py +1 -0
  117. solace_agent_mesh/common/utils/utils_llm.txt +323 -42
  118. solace_agent_mesh/config_portal/backend/common.py +2 -2
  119. solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +1 -1
  120. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-bFMKlzKf.js +98 -0
  121. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-d845808d.js → manifest-89db7c30.js} +1 -1
  122. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  123. solace_agent_mesh/core_a2a/core_a2a_llm.txt +10 -8
  124. solace_agent_mesh/core_a2a/service.py +20 -44
  125. solace_agent_mesh/evaluation/message_organizer.py +35 -56
  126. solace_agent_mesh/evaluation/run.py +26 -5
  127. solace_agent_mesh/evaluation/subscriber.py +35 -10
  128. solace_agent_mesh/evaluation/summary_builder.py +27 -34
  129. solace_agent_mesh/gateway/base/app.py +27 -1
  130. solace_agent_mesh/gateway/base/base_llm.txt +177 -72
  131. solace_agent_mesh/gateway/base/component.py +294 -523
  132. solace_agent_mesh/gateway/gateway_llm.txt +299 -58
  133. solace_agent_mesh/gateway/http_sse/ARCHITECTURE_GUIDE.md +676 -0
  134. solace_agent_mesh/gateway/http_sse/alembic/env.py +85 -0
  135. solace_agent_mesh/gateway/http_sse/alembic/script.py.mako +28 -0
  136. solace_agent_mesh/gateway/http_sse/alembic/versions/b1c2d3e4f5g6_add_database_indexes.py +83 -0
  137. solace_agent_mesh/gateway/http_sse/alembic/versions/d5b3f8f2e9a0_create_initial_database.py +58 -0
  138. solace_agent_mesh/gateway/http_sse/alembic.ini +147 -0
  139. solace_agent_mesh/gateway/http_sse/api/__init__.py +11 -0
  140. solace_agent_mesh/gateway/http_sse/api/controllers/__init__.py +9 -0
  141. solace_agent_mesh/gateway/http_sse/api/controllers/session_controller.py +355 -0
  142. solace_agent_mesh/gateway/http_sse/api/controllers/task_controller.py +279 -0
  143. solace_agent_mesh/gateway/http_sse/api/controllers/user_controller.py +35 -0
  144. solace_agent_mesh/gateway/http_sse/api/dto/__init__.py +10 -0
  145. solace_agent_mesh/gateway/http_sse/api/dto/requests/__init__.py +37 -0
  146. solace_agent_mesh/gateway/http_sse/api/dto/requests/session_requests.py +49 -0
  147. solace_agent_mesh/gateway/http_sse/api/dto/requests/task_requests.py +66 -0
  148. solace_agent_mesh/gateway/http_sse/api/dto/responses/__init__.py +43 -0
  149. solace_agent_mesh/gateway/http_sse/api/dto/responses/session_responses.py +68 -0
  150. solace_agent_mesh/gateway/http_sse/api/dto/responses/task_responses.py +74 -0
  151. solace_agent_mesh/gateway/http_sse/app.py +31 -1
  152. solace_agent_mesh/gateway/http_sse/application/__init__.py +3 -0
  153. solace_agent_mesh/gateway/http_sse/application/services/__init__.py +3 -0
  154. solace_agent_mesh/gateway/http_sse/application/services/session_service.py +135 -0
  155. solace_agent_mesh/gateway/http_sse/component.py +371 -236
  156. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +29 -29
  157. solace_agent_mesh/gateway/http_sse/dependencies.py +142 -39
  158. solace_agent_mesh/gateway/http_sse/domain/entities/__init__.py +3 -0
  159. solace_agent_mesh/gateway/http_sse/domain/entities/session.py +90 -0
  160. solace_agent_mesh/gateway/http_sse/domain/repositories/__init__.py +3 -0
  161. solace_agent_mesh/gateway/http_sse/domain/repositories/session_repository.py +54 -0
  162. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +272 -36
  163. solace_agent_mesh/gateway/http_sse/infrastructure/__init__.py +4 -0
  164. solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/__init__.py +3 -0
  165. solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/container.py +123 -0
  166. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/__init__.py +4 -0
  167. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_persistence_service.py +16 -0
  168. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_service.py +119 -0
  169. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/models.py +31 -0
  170. solace_agent_mesh/gateway/http_sse/infrastructure/persistence_service.py +12 -0
  171. solace_agent_mesh/gateway/http_sse/infrastructure/repositories/__init__.py +3 -0
  172. solace_agent_mesh/gateway/http_sse/infrastructure/repositories/session_repository.py +174 -0
  173. solace_agent_mesh/gateway/http_sse/main.py +293 -91
  174. solace_agent_mesh/gateway/http_sse/routers/agents.py +1 -1
  175. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +137 -56
  176. solace_agent_mesh/gateway/http_sse/routers/config.py +3 -1
  177. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +231 -5
  178. solace_agent_mesh/gateway/http_sse/routers/tasks.py +199 -171
  179. solace_agent_mesh/gateway/http_sse/routers/visualization.py +7 -7
  180. solace_agent_mesh/gateway/http_sse/services/agent_service.py +1 -1
  181. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +89 -135
  182. solace_agent_mesh/gateway/http_sse/services/task_service.py +2 -5
  183. solace_agent_mesh/gateway/http_sse/session_manager.py +64 -30
  184. solace_agent_mesh/gateway/http_sse/shared/__init__.py +9 -0
  185. solace_agent_mesh/gateway/http_sse/shared/auth_utils.py +29 -0
  186. solace_agent_mesh/gateway/http_sse/shared/enums.py +45 -0
  187. solace_agent_mesh/gateway/http_sse/shared/types.py +45 -0
  188. solace_agent_mesh/solace_agent_mesh_llm.txt +362 -0
  189. solace_agent_mesh/templates/gateway_component_template.py +149 -98
  190. solace_agent_mesh/templates/shared_config.yaml +4 -5
  191. solace_agent_mesh/templates/webui.yaml +8 -10
  192. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/METADATA +9 -6
  193. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/RECORD +197 -141
  194. solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +0 -1
  195. solace_agent_mesh/assets/docs/assets/js/main.3d0e7879.js +0 -2
  196. solace_agent_mesh/assets/docs/assets/js/runtime~main.05d19492.js +0 -1
  197. solace_agent_mesh/assets/docs/lunr-index-1757091012487.json +0 -1
  198. solace_agent_mesh/assets/docs/search-doc-1757091012487.json +0 -1
  199. solace_agent_mesh/client/webui/frontend/static/assets/authCallback-BmF2l6vg.js +0 -1
  200. solace_agent_mesh/client/webui/frontend/static/assets/client-D881Dttc.js +0 -49
  201. solace_agent_mesh/client/webui/frontend/static/assets/main-D0FnP_W4.css +0 -1
  202. solace_agent_mesh/client/webui/frontend/static/assets/main-Do32sFPX.js +0 -708
  203. solace_agent_mesh/common/a2a_protocol.py +0 -564
  204. solace_agent_mesh/common/client/__init__.py +0 -4
  205. solace_agent_mesh/common/client/card_resolver.py +0 -21
  206. solace_agent_mesh/common/client/client.py +0 -85
  207. solace_agent_mesh/common/client/client_llm.txt +0 -133
  208. solace_agent_mesh/common/server/__init__.py +0 -4
  209. solace_agent_mesh/common/server/server.py +0 -122
  210. solace_agent_mesh/common/server/server_llm.txt +0 -169
  211. solace_agent_mesh/common/server/task_manager.py +0 -291
  212. solace_agent_mesh/common/server/utils.py +0 -28
  213. solace_agent_mesh/common/types.py +0 -411
  214. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-Bym6YkMd.js +0 -98
  215. solace_agent_mesh/gateway/http_sse/routers/sessions.py +0 -80
  216. solace_agent_mesh/gateway/http_sse/routers/users.py +0 -59
  217. /solace_agent_mesh/assets/docs/assets/js/{main.3d0e7879.js.LICENSE.txt → main.08d30374.js.LICENSE.txt} +0 -0
  218. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/WHEEL +0 -0
  219. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/entry_points.txt +0 -0
  220. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,52 +3,55 @@ Custom Solace AI Connector Component to host the FastAPI backend for the Web UI.
3
3
  """
4
4
 
5
5
  import asyncio
6
- import queue
7
- import uuid
8
6
  import json
7
+ import queue
9
8
  import re
10
9
  import threading
11
- from typing import Any, Dict, Optional, List, Tuple, Union, Set
10
+ import uuid
12
11
  from datetime import datetime, timezone
13
- from fastapi import UploadFile, Request as FastAPIRequest
12
+ from typing import Any
14
13
 
15
14
  import uvicorn
16
- from fastapi import FastAPI
17
-
15
+ from fastapi import FastAPI, UploadFile
16
+ from fastapi import Request as FastAPIRequest
18
17
  from solace_ai_connector.common.log import log
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.components.inputs_outputs.broker_input import (
21
- BrokerInput,
22
- )
23
-
24
- from ...gateway.http_sse.sse_manager import SSEManager
25
20
 
26
- from .components import VisualizationForwarderComponent
27
- from ...gateway.http_sse.session_manager import SessionManager
28
- from ...gateway.base.component import BaseGatewayComponent
29
21
  from ...common.agent_registry import AgentRegistry
30
22
  from ...core_a2a.service import CoreA2AService
31
- from google.adk.artifacts import BaseArtifactService
23
+ from ...gateway.base.component import BaseGatewayComponent
24
+ from ...gateway.http_sse.session_manager import SessionManager
25
+ from ...gateway.http_sse.sse_manager import SSEManager
26
+ from .components import VisualizationForwarderComponent
27
+ from .infrastructure.persistence_service import PersistenceService
28
+
29
+ try:
30
+ from google.adk.artifacts import BaseArtifactService
31
+ except ImportError:
32
+
33
+ class BaseArtifactService:
34
+ pass
32
35
 
33
- from ...common.types import (
36
+
37
+ from a2a.types import (
38
+ A2ARequest,
34
39
  AgentCard,
35
- Part as A2APart,
36
- Task,
37
- TaskStatusUpdateEvent,
38
- TaskArtifactUpdateEvent,
39
40
  JSONRPCError,
40
41
  JSONRPCResponse,
41
- TextPart,
42
- FilePart,
43
- FileContent,
44
- )
45
- from ...common.a2a_protocol import (
46
- _topic_matches_subscription,
42
+ Task,
43
+ TaskArtifactUpdateEvent,
44
+ TaskStatusUpdateEvent,
47
45
  )
48
46
 
49
- from ...agent.utils.artifact_helpers import save_artifact_with_metadata
47
+ from ...common import a2a
48
+ from ...common.a2a.types import ContentPart
50
49
  from ...common.middleware.config_resolver import ConfigResolver
51
-
50
+ from ...common.utils.embeds import (
51
+ EARLY_EMBED_TYPES,
52
+ evaluate_embed,
53
+ resolve_embeds_in_string,
54
+ )
52
55
 
53
56
  info = {
54
57
  "class_name": "WebUIBackendComponent",
@@ -82,7 +85,11 @@ class WebUIBackendComponent(BaseGatewayComponent):
82
85
  """
83
86
  Initializes the WebUIBackendComponent, inheriting from BaseGatewayComponent.
84
87
  """
85
- super().__init__(**kwargs)
88
+ component_config = kwargs.get("component_config", {})
89
+ app_config = component_config.get("app_config", {})
90
+ resolve_uris = app_config.get("resolve_artifact_uris_in_gateway", True)
91
+
92
+ super().__init__(resolve_artifact_uris_in_gateway=resolve_uris, **kwargs)
86
93
  log.info("%s Initializing Web UI Backend Component...", self.log_identifier)
87
94
 
88
95
  try:
@@ -97,9 +104,6 @@ class WebUIBackendComponent(BaseGatewayComponent):
97
104
  self.fastapi_https_port = self.get_config("fastapi_https_port", 8443)
98
105
  self.session_secret_key = self.get_config("session_secret_key")
99
106
  self.cors_allowed_origins = self.get_config("cors_allowed_origins", ["*"])
100
- self.resolve_artifact_uris_in_gateway = self.get_config(
101
- "resolve_artifact_uris_in_gateway", True
102
- )
103
107
  self.ssl_keyfile = self.get_config("ssl_keyfile", "")
104
108
  self.ssl_certfile = self.get_config("ssl_certfile", "")
105
109
  self.ssl_keyfile_password = self.get_config("ssl_keyfile_password", "")
@@ -118,27 +122,42 @@ class WebUIBackendComponent(BaseGatewayComponent):
118
122
 
119
123
  self.sse_manager = SSEManager(max_queue_size=sse_max_queue_size)
120
124
 
125
+ session_config = self._resolve_session_config()
126
+ if session_config.get("type") == "sql":
127
+ # SQL type explicitly configured - database_url is required
128
+ database_url = session_config.get("database_url")
129
+ if not database_url:
130
+ raise ValueError(
131
+ f"{self.log_identifier} Session service type is 'sql' but no database_url provided. "
132
+ "Please provide a database_url in the session_service configuration or use type 'memory'."
133
+ )
134
+ self.persistence_service = PersistenceService(database_url)
135
+ else:
136
+ # Memory storage or no explicit configuration - no persistence service needed
137
+ self.persistence_service = None
138
+
121
139
  component_config = self.get_config("component_config", {})
122
140
  app_config = component_config.get("app_config", {})
123
141
 
124
142
  self.session_manager = SessionManager(
125
143
  secret_key=self.session_secret_key,
126
144
  app_config=app_config,
145
+ persistence_service=self.persistence_service,
127
146
  )
128
147
 
129
- self.fastapi_app: Optional[FastAPI] = None
130
- self.uvicorn_server: Optional[uvicorn.Server] = None
131
- self.fastapi_thread: Optional[threading.Thread] = None
132
- self.fastapi_event_loop: Optional[asyncio.AbstractEventLoop] = None
148
+ self.fastapi_app: FastAPI | None = None
149
+ self.uvicorn_server: uvicorn.Server | None = None
150
+ self.fastapi_thread: threading.Thread | None = None
151
+ self.fastapi_event_loop: asyncio.AbstractEventLoop | None = None
133
152
 
134
- self._visualization_internal_app: Optional[SACApp] = None
135
- self._visualization_broker_input: Optional[BrokerInput] = None
153
+ self._visualization_internal_app: SACApp | None = None
154
+ self._visualization_broker_input: BrokerInput | None = None
136
155
  self._visualization_message_queue: queue.Queue = queue.Queue(maxsize=200)
137
- self._active_visualization_streams: Dict[str, Dict[str, Any]] = {}
138
- self._visualization_locks: Dict[asyncio.AbstractEventLoop, asyncio.Lock] = {}
156
+ self._active_visualization_streams: dict[str, dict[str, Any]] = {}
157
+ self._visualization_locks: dict[asyncio.AbstractEventLoop, asyncio.Lock] = {}
139
158
  self._visualization_locks_lock = threading.Lock()
140
- self._global_visualization_subscriptions: Dict[str, int] = {}
141
- self._visualization_processor_task: Optional[asyncio.Task] = None
159
+ self._global_visualization_subscriptions: dict[str, int] = {}
160
+ self._visualization_processor_task: asyncio.Task | None = None
142
161
 
143
162
  log.info("%s Web UI Backend Component initialized.", self.log_identifier)
144
163
 
@@ -301,6 +320,37 @@ class WebUIBackendComponent(BaseGatewayComponent):
301
320
  self._visualization_broker_input = None
302
321
  raise
303
322
 
323
+ def _resolve_session_config(self) -> dict:
324
+ """
325
+ Resolve session service configuration with backward compatibility.
326
+
327
+ Priority order:
328
+ 1. Component-specific session_service config (new approach)
329
+ 2. Shared default_session_service config (deprecated, with warning)
330
+ 3. Hardcoded default (SQLite for Web UI)
331
+ """
332
+ # Check component-specific session_service config first
333
+ component_session_config = self.get_config("session_service")
334
+ if component_session_config:
335
+ log.debug("Using component-specific session_service configuration")
336
+ return component_session_config
337
+
338
+ # Backward compatibility: check shared config
339
+ shared_session_config = self.get_config("default_session_service")
340
+ if shared_session_config:
341
+ log.warning(
342
+ "Using session_service from shared config is deprecated. "
343
+ "Move to component-specific configuration in app_config.session_service"
344
+ )
345
+ return shared_session_config
346
+
347
+ # Default configuration for Web UI (backward compatibility)
348
+ default_config = {"type": "memory", "default_behavior": "PERSISTENT"}
349
+ log.info(
350
+ "Using default memory session configuration for Web UI (backward compatibility)"
351
+ )
352
+ return default_config
353
+
304
354
  async def _visualization_message_processor_loop(self) -> None:
305
355
  """
306
356
  Asynchronously consumes messages from the _visualization_message_queue,
@@ -427,7 +477,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
427
477
  "solace_topics", set()
428
478
  )
429
479
  if any(
430
- _topic_matches_subscription(topic, pattern)
480
+ a2a.topic_matches_subscription(topic, pattern)
431
481
  for pattern in subscribed_topics_for_stream
432
482
  ):
433
483
  is_permitted = True
@@ -449,6 +499,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
449
499
  "task_id": event_details["task_id"],
450
500
  "payload_summary": event_details["payload_summary"],
451
501
  "full_payload": payload_dict,
502
+ "debug_type": event_details["debug_type"],
452
503
  }
453
504
 
454
505
  try:
@@ -567,7 +618,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
567
618
  if not hasattr(
568
619
  self._visualization_broker_input, "add_subscription"
569
620
  ) or not callable(
570
- getattr(self._visualization_broker_input, "add_subscription")
621
+ self._visualization_broker_input.add_subscription
571
622
  ):
572
623
  log.error(
573
624
  "%s Visualization BrokerInput does not support dynamic 'add_subscription'. "
@@ -682,9 +733,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
682
733
  try:
683
734
  if not hasattr(
684
735
  self._visualization_broker_input, "remove_subscription"
685
- ) or not callable(
686
- getattr(self._visualization_broker_input, "remove_subscription")
687
- ):
736
+ ) or not callable(self._visualization_broker_input.remove_subscription):
688
737
  log.error(
689
738
  "%s Visualization BrokerInput does not support dynamic 'remove_subscription'. "
690
739
  "Please upgrade the 'solace-ai-connector' module. Cannot remove subscription '%s'.",
@@ -771,7 +820,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
771
820
 
772
821
  async def _extract_initial_claims(
773
822
  self, external_event_data: Any
774
- ) -> Optional[Dict[str, Any]]:
823
+ ) -> dict[str, Any] | None:
775
824
  """
776
825
  Extracts initial identity claims from the incoming external event.
777
826
  For the WebUI, this means inspecting the FastAPIRequest.
@@ -827,18 +876,18 @@ class WebUIBackendComponent(BaseGatewayComponent):
827
876
  return
828
877
 
829
878
  try:
830
- from ...gateway.http_sse.main import (
831
- app as fastapi_app_instance,
832
- )
833
- from ...gateway.http_sse.main import (
834
- setup_dependencies,
835
- )
879
+ from ...gateway.http_sse.main import app as fastapi_app_instance
880
+ from ...gateway.http_sse.main import setup_dependencies
836
881
 
837
882
  self.fastapi_app = fastapi_app_instance
838
883
 
839
- setup_dependencies(self)
884
+ setup_dependencies(self, self.persistence_service)
840
885
 
841
- port = self.fastapi_https_port if self.ssl_keyfile and self.ssl_certfile else self.fastapi_port
886
+ port = (
887
+ self.fastapi_https_port
888
+ if self.ssl_keyfile and self.ssl_certfile
889
+ else self.fastapi_port
890
+ )
842
891
 
843
892
  config = uvicorn.Config(
844
893
  app=self.fastapi_app,
@@ -935,7 +984,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
935
984
  raise
936
985
 
937
986
  def publish_a2a(
938
- self, topic: str, payload: Dict, user_properties: Optional[Dict] = None
987
+ self, topic: str, payload: dict, user_properties: dict | None = None
939
988
  ):
940
989
  """
941
990
  Publishes an A2A message using the SAC App's send_message method.
@@ -993,15 +1042,17 @@ class WebUIBackendComponent(BaseGatewayComponent):
993
1042
  log.info("%s Visualization resources cleaned up.", self.log_identifier)
994
1043
 
995
1044
  def _infer_visualization_event_details(
996
- self, topic: str, payload: Dict[str, Any]
997
- ) -> Dict[str, Any]:
1045
+ self, topic: str, payload: dict[str, Any]
1046
+ ) -> dict[str, Any]:
998
1047
  """
999
1048
  Infers details for the visualization SSE payload from the Solace topic and A2A message.
1049
+ This version is updated to parse the official A2A SDK message formats.
1000
1050
  """
1001
1051
  details = {
1002
1052
  "direction": "unknown",
1003
1053
  "source_entity": "unknown",
1004
1054
  "target_entity": "unknown",
1055
+ "debug_type": "unknown",
1005
1056
  "message_id": payload.get("id"),
1006
1057
  "task_id": None,
1007
1058
  "payload_summary": {
@@ -1010,135 +1061,141 @@ class WebUIBackendComponent(BaseGatewayComponent):
1010
1061
  },
1011
1062
  }
1012
1063
 
1013
- topic_parts = topic.split("/")
1014
-
1064
+ # --- Phase 1: Parse the payload to extract core info ---
1015
1065
  try:
1016
- a2a_base_index = topic_parts.index("a2a")
1017
- domain_index = a2a_base_index + 2
1018
- action_type_index = a2a_base_index + 3
1019
- entity_name_index = a2a_base_index + 4
1020
- task_id_from_topic_index = a2a_base_index + 5
1021
-
1022
- domain = (
1023
- topic_parts[domain_index] if len(topic_parts) > domain_index else None
1024
- )
1025
- action_type = (
1026
- topic_parts[action_type_index]
1027
- if len(topic_parts) > action_type_index
1028
- else None
1029
- )
1030
- entity_name = (
1031
- topic_parts[entity_name_index]
1032
- if len(topic_parts) > entity_name_index
1033
- else None
1034
- )
1035
-
1036
- if domain == "agent":
1037
- if action_type == "request":
1038
- details["direction"] = "request"
1039
- details["target_entity"] = entity_name
1040
- user_props = (
1041
- payload.get("params", {})
1042
- .get("metadata", {})
1043
- .get("solaceUserProperties", {})
1066
+ # Try to parse as a JSON-RPC response first
1067
+ if "result" in payload or "error" in payload:
1068
+ rpc_response = JSONRPCResponse.model_validate(payload)
1069
+ result = a2a.get_response_result(rpc_response)
1070
+ error = a2a.get_response_error(rpc_response)
1071
+ details["message_id"] = a2a.get_response_id(rpc_response)
1072
+
1073
+ if result:
1074
+ kind = getattr(result, "kind", None)
1075
+ details["direction"] = kind or "response"
1076
+ details["task_id"] = getattr(result, "task_id", None) or getattr(
1077
+ result, "id", None
1044
1078
  )
1045
- details["source_entity"] = (
1046
- user_props.get("clientId")
1047
- or user_props.get("delegating_agent_name")
1048
- or self.gateway_id
1049
- )
1050
- elif action_type == "response":
1051
- details["direction"] = "response"
1052
- details["source_entity"] = entity_name
1053
- details["target_entity"] = (
1054
- payload.get("result", {}).get("metadata", {}).get("clientId")
1055
- )
1056
- elif action_type == "status":
1057
- details["direction"] = "status_update"
1058
- details["source_entity"] = entity_name
1059
- details["target_entity"] = (
1060
- payload.get("result", {}).get("metadata", {}).get("clientId")
1061
- )
1062
- elif domain == "gateway":
1063
- if action_type == "response":
1064
- details["direction"] = "response"
1065
- details["source_entity"] = (
1066
- payload.get("result", {})
1067
- .get("status", {})
1068
- .get("message", {})
1069
- .get("metadata", {})
1070
- .get("agent_name", "unknown_agent")
1079
+
1080
+ if isinstance(result, TaskStatusUpdateEvent):
1081
+ details["source_entity"] = (
1082
+ result.metadata.get("agent_name")
1083
+ if result.metadata
1084
+ else None
1085
+ )
1086
+ message = a2a.get_message_from_status_update(result)
1087
+ if message:
1088
+ if not details["source_entity"]:
1089
+ details["source_entity"] = (
1090
+ message.metadata.get("agent_name")
1091
+ if message.metadata
1092
+ else None
1093
+ )
1094
+ data_parts = a2a.get_data_parts_from_message(message)
1095
+ if data_parts:
1096
+ details["debug_type"] = data_parts[0].data.get(
1097
+ "type", "unknown"
1098
+ )
1099
+ elif a2a.get_text_from_message(message):
1100
+ details["debug_type"] = "streaming_text"
1101
+ elif isinstance(result, Task):
1102
+ details["source_entity"] = (
1103
+ result.metadata.get("agent_name")
1104
+ if result.metadata
1105
+ else None
1106
+ )
1107
+ elif isinstance(result, TaskArtifactUpdateEvent):
1108
+ artifact = a2a.get_artifact_from_artifact_update(result)
1109
+ if artifact:
1110
+ details["source_entity"] = (
1111
+ artifact.metadata.get("agent_name")
1112
+ if artifact.metadata
1113
+ else None
1114
+ )
1115
+ elif error:
1116
+ details["direction"] = "error_response"
1117
+ details["task_id"] = (
1118
+ error.data.get("taskId")
1119
+ if isinstance(error.data, dict)
1120
+ else None
1071
1121
  )
1072
- details["target_entity"] = entity_name
1073
- elif action_type == "status":
1074
- details["direction"] = "status_update"
1075
- details["source_entity"] = (
1076
- payload.get("result", {})
1077
- .get("status", {})
1078
- .get("message", {})
1079
- .get("metadata", {})
1080
- .get("agent_name", "unknown_agent")
1122
+ details["debug_type"] = "error"
1123
+
1124
+ # Try to parse as a JSON-RPC request
1125
+ elif "method" in payload:
1126
+ rpc_request = A2ARequest.model_validate(payload)
1127
+ method = a2a.get_request_method(rpc_request)
1128
+ details["direction"] = "request"
1129
+ details["payload_summary"]["method"] = method
1130
+ details["message_id"] = a2a.get_request_id(rpc_request)
1131
+
1132
+ if method in ["message/send", "message/stream"]:
1133
+ details["debug_type"] = method
1134
+ message = a2a.get_message_from_send_request(rpc_request)
1135
+ details["task_id"] = a2a.get_request_id(rpc_request)
1136
+ if message:
1137
+ details["target_entity"] = (
1138
+ message.metadata.get("agent_name")
1139
+ if message.metadata
1140
+ else None
1141
+ )
1142
+ elif method == "tasks/cancel":
1143
+ details["task_id"] = a2a.get_task_id_from_cancel_request(
1144
+ rpc_request
1081
1145
  )
1082
- details["target_entity"] = entity_name
1083
- elif domain == "discovery" and action_type == "agentcards":
1146
+
1147
+ # Handle Discovery messages (which are not JSON-RPC)
1148
+ elif "/a2a/v1/discovery/" in topic:
1149
+ agent_card = AgentCard.model_validate(payload)
1084
1150
  details["direction"] = "discovery"
1085
- details["source_entity"] = payload.get("name", "unknown_agent")
1151
+ details["source_entity"] = agent_card.name
1086
1152
  details["target_entity"] = "broadcast"
1153
+ details["message_id"] = None # Discovery has no ID
1087
1154
 
1088
- if payload.get("method") in [
1089
- "tasks/send",
1090
- "tasks/sendSubscribe",
1091
- "tasks/cancel",
1092
- ]:
1093
- details["task_id"] = payload.get("params", {}).get("id")
1094
- elif "result" in payload and isinstance(payload["result"], dict):
1095
- details["task_id"] = payload["result"].get("id")
1096
- elif len(topic_parts) > task_id_from_topic_index and (
1097
- action_type == "status" or action_type == "response"
1098
- ):
1099
- details["task_id"] = topic_parts[task_id_from_topic_index]
1100
-
1101
- except (ValueError, IndexError):
1102
- log.debug(
1103
- "%s Could not parse A2A structure from topic: %s",
1155
+ except Exception as e:
1156
+ log.warning(
1157
+ "[%s] Failed to parse A2A payload for visualization details: %s",
1104
1158
  self.log_identifier,
1105
- topic,
1159
+ e,
1106
1160
  )
1161
+
1162
+ # --- Phase 2: Refine details using topic information as a fallback ---
1163
+ if details["direction"] == "unknown":
1107
1164
  if "request" in topic:
1108
1165
  details["direction"] = "request"
1109
1166
  elif "response" in topic:
1110
1167
  details["direction"] = "response"
1111
1168
  elif "status" in topic:
1112
1169
  details["direction"] = "status_update"
1113
- elif "discovery" in topic:
1114
- details["direction"] = "discovery"
1170
+ # TEMP - add debug_type based on the type in the data
1171
+ details["debug_type"] = "unknown"
1115
1172
 
1116
- if "params" in payload:
1117
- params_str = json.dumps(payload["params"])
1118
- details["payload_summary"]["params_preview"] = (
1119
- (params_str[:100] + "...") if len(params_str) > 100 else params_str
1173
+ # --- Phase 3: Create a payload summary ---
1174
+ try:
1175
+ summary_source = (
1176
+ payload.get("result")
1177
+ or payload.get("params")
1178
+ or payload.get("error")
1179
+ or payload
1120
1180
  )
1121
- elif "result" in payload:
1122
- result_str = json.dumps(payload["result"])
1181
+ summary_str = json.dumps(summary_source)
1123
1182
  details["payload_summary"]["params_preview"] = (
1124
- (result_str[:100] + "...") if len(result_str) > 100 else result_str
1183
+ (summary_str[:100] + "...") if len(summary_str) > 100 else summary_str
1125
1184
  )
1126
- elif "error" in payload:
1127
- details["payload_summary"]["method"] = "JSONRPCError"
1128
- error_str = json.dumps(payload["error"])
1185
+ except Exception:
1129
1186
  details["payload_summary"]["params_preview"] = (
1130
- (error_str[:100] + "...") if len(error_str) > 100 else error_str
1187
+ "[Could not serialize payload]"
1131
1188
  )
1132
1189
 
1133
1190
  return details
1134
1191
 
1135
1192
  def _extract_involved_agents_for_viz(
1136
- self, topic: str, payload_dict: Dict[str, Any]
1137
- ) -> Set[str]:
1193
+ self, topic: str, payload_dict: dict[str, Any]
1194
+ ) -> set[str]:
1138
1195
  """
1139
1196
  Extracts agent names involved in a message from its topic and payload.
1140
1197
  """
1141
- agents: Set[str] = set()
1198
+ agents: set[str] = set()
1142
1199
  log_id_prefix = f"{self.log_identifier}[ExtractAgentsViz]"
1143
1200
 
1144
1201
  topic_agent_match = re.match(
@@ -1265,13 +1322,13 @@ class WebUIBackendComponent(BaseGatewayComponent):
1265
1322
  """Returns the unique identifier for this gateway instance."""
1266
1323
  return self.gateway_id
1267
1324
 
1268
- def get_cors_origins(self) -> List[str]:
1325
+ def get_cors_origins(self) -> list[str]:
1269
1326
  return self.cors_allowed_origins
1270
1327
 
1271
- def get_shared_artifact_service(self) -> Optional[BaseArtifactService]:
1328
+ def get_shared_artifact_service(self) -> BaseArtifactService | None:
1272
1329
  return self.shared_artifact_service
1273
1330
 
1274
- def get_embed_config(self) -> Dict[str, Any]:
1331
+ def get_embed_config(self) -> dict[str, Any]:
1275
1332
  """Returns embed-related configuration needed by dependencies."""
1276
1333
  return {
1277
1334
  "enable_embed_resolution": self.enable_embed_resolution,
@@ -1287,6 +1344,52 @@ class WebUIBackendComponent(BaseGatewayComponent):
1287
1344
  """Returns the instance of the ConfigResolver."""
1288
1345
  return self._config_resolver
1289
1346
 
1347
+ async def _resolve_embeds_for_persistence(
1348
+ self, message_content: str, session_id: str, user_id: str, log_identifier: str
1349
+ ) -> str:
1350
+ """
1351
+ Resolves embeds in a message for database storage.
1352
+ Returns the resolved text.
1353
+
1354
+ Args:
1355
+ message_content: The message text that may contain embeds
1356
+ session_id: The A2A session ID
1357
+ user_id: The user ID
1358
+ log_identifier: Logging identifier
1359
+
1360
+ Returns:
1361
+ The message with embeds resolved (or original if resolution fails)
1362
+ """
1363
+ try:
1364
+ embed_context = {
1365
+ "artifact_service": self.shared_artifact_service,
1366
+ "session_context": {
1367
+ "app_name": self.gateway_id,
1368
+ "user_id": user_id,
1369
+ "session_id": session_id,
1370
+ },
1371
+ "config": self.get_embed_config(),
1372
+ }
1373
+
1374
+ resolved_text, _, _ = await resolve_embeds_in_string(
1375
+ text=message_content,
1376
+ context=embed_context,
1377
+ resolver_func=evaluate_embed,
1378
+ types_to_resolve=EARLY_EMBED_TYPES,
1379
+ log_identifier=log_identifier,
1380
+ config=embed_context["config"],
1381
+ )
1382
+
1383
+ return resolved_text
1384
+
1385
+ except Exception as e:
1386
+ log.warning(
1387
+ "%s Error resolving embeds for storage: %s. Using original message.",
1388
+ log_identifier,
1389
+ e,
1390
+ )
1391
+ return message_content
1392
+
1290
1393
  def _start_listener(self) -> None:
1291
1394
  """
1292
1395
  GDK Hook: Starts the FastAPI/Uvicorn server.
@@ -1308,8 +1411,8 @@ class WebUIBackendComponent(BaseGatewayComponent):
1308
1411
  pass
1309
1412
 
1310
1413
  async def _translate_external_input(
1311
- self, external_event_data: Dict[str, Any]
1312
- ) -> Tuple[str, List[A2APart], Dict[str, Any]]:
1414
+ self, external_event_data: dict[str, Any]
1415
+ ) -> tuple[str, list[ContentPart], dict[str, Any]]:
1313
1416
  """
1314
1417
  Translates raw HTTP request data (from FastAPI form) into A2A task parameters.
1315
1418
 
@@ -1321,7 +1424,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
1321
1424
  Returns:
1322
1425
  A tuple containing:
1323
1426
  - target_agent_name (str): The name of the A2A agent to target.
1324
- - a2a_parts (List[A2APart]): A list of A2A Part objects for the message.
1427
+ - a2a_parts (List[ContentPart]): A list of unwrapped A2A Part objects.
1325
1428
  - external_request_context (Dict[str, Any]): Context for TaskContextManager.
1326
1429
  """
1327
1430
  log_id_prefix = f"{self.log_identifier}[TranslateInput]"
@@ -1333,10 +1436,9 @@ class WebUIBackendComponent(BaseGatewayComponent):
1333
1436
 
1334
1437
  target_agent_name: str = external_event_data.get("agent_name")
1335
1438
  user_message: str = external_event_data.get("message", "")
1336
- files: Optional[List[UploadFile]] = external_event_data.get("files")
1439
+ files: list[UploadFile] | None = external_event_data.get("files")
1337
1440
  client_id: str = external_event_data.get("client_id")
1338
1441
  a2a_session_id: str = external_event_data.get("a2a_session_id")
1339
-
1340
1442
  if not target_agent_name:
1341
1443
  raise ValueError("Target agent name is missing in external_event_data.")
1342
1444
  if not client_id or not a2a_session_id:
@@ -1344,10 +1446,9 @@ class WebUIBackendComponent(BaseGatewayComponent):
1344
1446
  "Client ID or A2A Session ID is missing in external_event_data."
1345
1447
  )
1346
1448
 
1347
- a2a_parts: List[A2APart] = []
1449
+ a2a_parts: list[ContentPart] = []
1348
1450
 
1349
- if files and self.shared_artifact_service:
1350
- file_metadata_summary_parts = []
1451
+ if files:
1351
1452
  for upload_file in files:
1352
1453
  try:
1353
1454
  content_bytes = await upload_file.read()
@@ -1358,52 +1459,21 @@ class WebUIBackendComponent(BaseGatewayComponent):
1358
1459
  upload_file.filename,
1359
1460
  )
1360
1461
  continue
1361
- save_result = await save_artifact_with_metadata(
1362
- artifact_service=self.shared_artifact_service,
1363
- app_name=self.gateway_id,
1364
- user_id=client_id,
1365
- session_id=a2a_session_id,
1366
- filename=upload_file.filename,
1462
+
1463
+ # The BaseGatewayComponent will handle normalization based on policy.
1464
+ # Here, we just create the FilePart with inline bytes.
1465
+ file_part = a2a.create_file_part_from_bytes(
1367
1466
  content_bytes=content_bytes,
1368
- mime_type=upload_file.content_type
1369
- or "application/octet-stream",
1370
- metadata_dict={
1371
- "source": "webui_gateway_upload",
1372
- "original_filename": upload_file.filename,
1373
- "upload_timestamp_utc": datetime.now(
1374
- timezone.utc
1375
- ).isoformat(),
1376
- "gateway_id": self.gateway_id,
1377
- "web_client_id": client_id,
1378
- "a2a_session_id": a2a_session_id,
1379
- },
1380
- timestamp=datetime.now(timezone.utc),
1467
+ name=upload_file.filename,
1468
+ mime_type=upload_file.content_type,
1469
+ )
1470
+ a2a_parts.append(file_part)
1471
+ log.info(
1472
+ "%s Created inline FilePart for uploaded file: %s (%d bytes)",
1473
+ log_id_prefix,
1474
+ upload_file.filename,
1475
+ len(content_bytes),
1381
1476
  )
1382
-
1383
- if save_result["status"] in ["success", "partial_success"]:
1384
- data_version = save_result.get("data_version", 0)
1385
- artifact_uri = f"artifact://{self.gateway_id}/{client_id}/{a2a_session_id}/{upload_file.filename}?version={data_version}"
1386
- file_content = FileContent(
1387
- name=upload_file.filename,
1388
- mimeType=upload_file.content_type,
1389
- uri=artifact_uri,
1390
- )
1391
- a2a_parts.append(FilePart(file=file_content))
1392
- file_metadata_summary_parts.append(
1393
- f"- {upload_file.filename} ({upload_file.content_type}, {len(content_bytes)} bytes, URI: {artifact_uri})"
1394
- )
1395
- log.info(
1396
- "%s Processed and created URI for uploaded file: %s",
1397
- log_id_prefix,
1398
- artifact_uri,
1399
- )
1400
- else:
1401
- log.error(
1402
- "%s Failed to save artifact %s: %s",
1403
- log_id_prefix,
1404
- upload_file.filename,
1405
- save_result.get("message"),
1406
- )
1407
1477
 
1408
1478
  except Exception as e:
1409
1479
  log.exception(
@@ -1415,15 +1485,8 @@ class WebUIBackendComponent(BaseGatewayComponent):
1415
1485
  finally:
1416
1486
  await upload_file.close()
1417
1487
 
1418
- if file_metadata_summary_parts:
1419
- user_message = (
1420
- "The user uploaded the following file(s):\n"
1421
- + "\n".join(file_metadata_summary_parts)
1422
- + f"\n\nUser message: {user_message}"
1423
- )
1424
-
1425
1488
  if user_message:
1426
- a2a_parts.append(TextPart(text=user_message))
1489
+ a2a_parts.append(a2a.create_text_part(text=user_message))
1427
1490
 
1428
1491
  external_request_context = {
1429
1492
  "app_name_for_artifacts": self.gateway_id,
@@ -1443,17 +1506,23 @@ class WebUIBackendComponent(BaseGatewayComponent):
1443
1506
 
1444
1507
  async def _send_update_to_external(
1445
1508
  self,
1446
- external_request_context: Dict[str, Any],
1447
- event_data: Union[TaskStatusUpdateEvent, TaskArtifactUpdateEvent],
1509
+ external_request_context: dict[str, Any],
1510
+ event_data: TaskStatusUpdateEvent | TaskArtifactUpdateEvent,
1448
1511
  is_final_chunk_of_update: bool,
1449
1512
  ) -> None:
1450
1513
  """
1451
1514
  Sends an intermediate update (TaskStatusUpdateEvent or TaskArtifactUpdateEvent)
1452
- to the external platform (Web UI via SSE).
1515
+ to the external platform (Web UI via SSE) and stores agent messages in the database.
1453
1516
  """
1454
1517
  log_id_prefix = f"{self.log_identifier}[SendUpdate]"
1455
1518
  sse_task_id = external_request_context.get("a2a_task_id_for_event")
1456
- a2a_task_id = event_data.id
1519
+ a2a_task_id = event_data.task_id
1520
+
1521
+ log.debug(
1522
+ "%s _send_update_to_external called with event_type: %s",
1523
+ log_id_prefix,
1524
+ type(event_data).__name__,
1525
+ )
1457
1526
 
1458
1527
  if not sse_task_id:
1459
1528
  log.error(
@@ -1474,9 +1543,10 @@ class WebUIBackendComponent(BaseGatewayComponent):
1474
1543
  if isinstance(event_data, TaskArtifactUpdateEvent):
1475
1544
  sse_event_type = "artifact_update"
1476
1545
 
1477
- sse_payload = JSONRPCResponse(id=a2a_task_id, result=event_data).model_dump(
1478
- exclude_none=True
1546
+ sse_payload_model = a2a.create_success_response(
1547
+ result=event_data, request_id=a2a_task_id
1479
1548
  )
1549
+ sse_payload = sse_payload_model.model_dump(by_alias=True, exclude_none=True)
1480
1550
 
1481
1551
  try:
1482
1552
  await self.sse_manager.send_event(
@@ -1488,6 +1558,10 @@ class WebUIBackendComponent(BaseGatewayComponent):
1488
1558
  sse_event_type,
1489
1559
  a2a_task_id,
1490
1560
  )
1561
+
1562
+ # Note: Agent message storage is handled in _send_final_response_to_external
1563
+ # to avoid duplicate storage of intermediate status updates
1564
+
1491
1565
  except Exception as e:
1492
1566
  log.exception(
1493
1567
  "%s Failed to send %s via SSE for A2A Task ID %s: %s",
@@ -1498,7 +1572,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
1498
1572
  )
1499
1573
 
1500
1574
  async def _send_final_response_to_external(
1501
- self, external_request_context: Dict[str, Any], task_data: Task
1575
+ self, external_request_context: dict[str, Any], task_data: Task
1502
1576
  ) -> None:
1503
1577
  """
1504
1578
  Sends the final A2A Task result to the external platform (Web UI via SSE).
@@ -1507,6 +1581,8 @@ class WebUIBackendComponent(BaseGatewayComponent):
1507
1581
  sse_task_id = external_request_context.get("a2a_task_id_for_event")
1508
1582
  a2a_task_id = task_data.id
1509
1583
 
1584
+ log.debug("%s _send_final_response_to_external called", log_id_prefix)
1585
+
1510
1586
  if not sse_task_id:
1511
1587
  log.error(
1512
1588
  "%s Cannot send final response: 'a2a_task_id_for_event' missing from external_request_context.",
@@ -1521,9 +1597,10 @@ class WebUIBackendComponent(BaseGatewayComponent):
1521
1597
  sse_task_id,
1522
1598
  )
1523
1599
 
1524
- sse_payload = JSONRPCResponse(id=a2a_task_id, result=task_data).model_dump(
1525
- exclude_none=True
1600
+ sse_payload_model = a2a.create_success_response(
1601
+ result=task_data, request_id=a2a_task_id
1526
1602
  )
1603
+ sse_payload = sse_payload_model.model_dump(by_alias=True, exclude_none=True)
1527
1604
 
1528
1605
  try:
1529
1606
  await self.sse_manager.send_event(
@@ -1534,6 +1611,63 @@ class WebUIBackendComponent(BaseGatewayComponent):
1534
1611
  log_id_prefix,
1535
1612
  a2a_task_id,
1536
1613
  )
1614
+
1615
+ # Store final agent response in persistence layer if available
1616
+ if hasattr(self, "persistence_service") and self.persistence_service:
1617
+ try:
1618
+ session_id = external_request_context.get("a2a_session_id")
1619
+ user_id = external_request_context.get("user_id_for_a2a")
1620
+ agent_name = external_request_context.get(
1621
+ "target_agent_name", "agent"
1622
+ )
1623
+
1624
+ # Extract message content from the task status
1625
+ message_text = ""
1626
+ if task_data.status and task_data.status.message:
1627
+ parts = a2a.get_parts_from_message(task_data.status.message)
1628
+ for part in parts:
1629
+ if hasattr(part, "text") and part.text:
1630
+ if message_text:
1631
+ message_text += "\n"
1632
+ message_text += part.text
1633
+
1634
+ log.info(
1635
+ "%s Final agent response storage debug - session_id: %s, user_id: %s, message_text: '%s', parts_count: %s",
1636
+ log_id_prefix,
1637
+ session_id,
1638
+ user_id,
1639
+ message_text[:100] if message_text else None,
1640
+ len(a2a.get_parts_from_message(task_data.status.message))
1641
+ if task_data.status and task_data.status.message
1642
+ else 0,
1643
+ )
1644
+
1645
+ if message_text and session_id and user_id:
1646
+ from .dependencies import get_session_service
1647
+ from .shared.enums import SenderType
1648
+
1649
+ session_service = get_session_service(self)
1650
+ session_service.add_message_to_session(
1651
+ session_id=session_id,
1652
+ user_id=user_id,
1653
+ message=message_text,
1654
+ sender_type=SenderType.AGENT,
1655
+ sender_name=agent_name,
1656
+ agent_id=agent_name,
1657
+ )
1658
+ log.info(
1659
+ "%s Final agent response stored in session %s",
1660
+ log_id_prefix,
1661
+ session_id,
1662
+ )
1663
+ except Exception as storage_error:
1664
+ log.warning(
1665
+ "%s Failed to store final agent response: %s",
1666
+ log_id_prefix,
1667
+ storage_error,
1668
+ )
1669
+ # Don't fail the SSE send if storage fails
1670
+
1537
1671
  except Exception as e:
1538
1672
  log.exception(
1539
1673
  "%s Failed to send final_response via SSE for A2A Task ID %s: %s",
@@ -1550,7 +1684,7 @@ class WebUIBackendComponent(BaseGatewayComponent):
1550
1684
  )
1551
1685
 
1552
1686
  async def _send_error_to_external(
1553
- self, external_request_context: Dict[str, Any], error_data: JSONRPCError
1687
+ self, external_request_context: dict[str, Any], error_data: JSONRPCError
1554
1688
  ) -> None:
1555
1689
  """
1556
1690
  Sends an error notification to the external platform (Web UI via SSE).
@@ -1572,10 +1706,11 @@ class WebUIBackendComponent(BaseGatewayComponent):
1572
1706
  error_data,
1573
1707
  )
1574
1708
 
1575
- sse_payload = JSONRPCResponse(
1576
- id=external_request_context.get("original_rpc_id", sse_task_id),
1709
+ sse_payload_model = a2a.create_error_response(
1577
1710
  error=error_data,
1578
- ).model_dump(exclude_none=True)
1711
+ request_id=external_request_context.get("original_rpc_id", sse_task_id),
1712
+ )
1713
+ sse_payload = sse_payload_model.model_dump(by_alias=True, exclude_none=True)
1579
1714
 
1580
1715
  try:
1581
1716
  await self.sse_manager.send_event(