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
@@ -4,19 +4,16 @@ Base Component class for Gateway implementations in the Solace AI Connector.
4
4
 
5
5
  import asyncio
6
6
  import queue
7
- import re
8
- import threading
9
7
  import base64
10
8
  import uuid
11
9
  from datetime import datetime, timezone
12
10
  from typing import Any, Dict, Optional, List, Tuple, Union
13
- from urllib.parse import urlparse, parse_qs
14
11
 
15
- from solace_ai_connector.components.component_base import ComponentBase
16
12
  from solace_ai_connector.common.log import log
17
13
  from google.adk.artifacts import BaseArtifactService
18
14
 
19
15
  from ...common.agent_registry import AgentRegistry
16
+ from ...common.sac.sam_component_base import SamComponentBase
20
17
  from ...core_a2a.service import CoreA2AService
21
18
  from ...agent.adk.services import initialize_artifact_service
22
19
  from ...common.services.identity_service import (
@@ -24,8 +21,8 @@ from ...common.services.identity_service import (
24
21
  create_identity_service,
25
22
  )
26
23
  from .task_context import TaskContextManager
27
- from ...common.types import (
28
- Part as A2APart,
24
+ from ...common.a2a.types import ContentPart
25
+ from a2a.types import (
29
26
  Message as A2AMessage,
30
27
  AgentCard,
31
28
  JSONRPCResponse,
@@ -34,21 +31,11 @@ from ...common.types import (
34
31
  TaskArtifactUpdateEvent,
35
32
  JSONRPCError,
36
33
  TextPart,
37
- TaskStatus,
38
- TaskState,
39
34
  FilePart,
40
- DataPart,
35
+ FileWithBytes,
41
36
  Artifact as A2AArtifact,
42
37
  )
43
- from ...common.a2a_protocol import (
44
- get_gateway_response_topic,
45
- get_gateway_response_subscription_topic,
46
- get_gateway_status_topic,
47
- get_gateway_status_subscription_topic,
48
- get_discovery_topic,
49
- _topic_matches_subscription,
50
- _subscription_to_regex,
51
- )
38
+ from ...common import a2a
52
39
  from ...common.utils import is_text_based_mime_type
53
40
  from ...common.utils.embeds import (
54
41
  resolve_embeds_in_string,
@@ -65,9 +52,6 @@ from solace_ai_connector.common.event import Event, EventType
65
52
  from abc import abstractmethod
66
53
 
67
54
  from ...common.middleware.registry import MiddlewareRegistry
68
- from ...agent.utils.artifact_helpers import (
69
- load_artifact_content_or_metadata,
70
- )
71
55
 
72
56
  info = {
73
57
  "class_name": "BaseGatewayComponent",
@@ -88,7 +72,7 @@ info = {
88
72
  }
89
73
 
90
74
 
91
- class BaseGatewayComponent(ComponentBase):
75
+ class BaseGatewayComponent(SamComponentBase):
92
76
  """
93
77
  Abstract base class for Gateway components.
94
78
 
@@ -111,17 +95,16 @@ class BaseGatewayComponent(ComponentBase):
111
95
 
112
96
  return super().get_config(key, default)
113
97
 
114
- def __init__(self, **kwargs: Any):
98
+ def __init__(self, resolve_artifact_uris_in_gateway: bool = True, **kwargs: Any):
115
99
  super().__init__(info, **kwargs)
100
+ self.resolve_artifact_uris_in_gateway = resolve_artifact_uris_in_gateway
116
101
  log.info("%s Initializing Base Gateway Component...", self.log_identifier)
117
102
 
118
103
  try:
119
- self.namespace: str = self.get_config("namespace")
104
+ # Note: self.namespace and self.max_message_size_bytes are initialized in SamComponentBase
120
105
  self.gateway_id: str = self.get_config("gateway_id")
121
- if not self.namespace or not self.gateway_id:
122
- raise ValueError(
123
- "Namespace and Gateway ID must be configured in the app_config."
124
- )
106
+ if not self.gateway_id:
107
+ raise ValueError("Gateway ID must be configured in the app_config.")
125
108
 
126
109
  self.enable_embed_resolution: bool = self.get_config(
127
110
  "enable_embed_resolution", True
@@ -132,8 +115,8 @@ class BaseGatewayComponent(ComponentBase):
132
115
  self.gateway_recursive_embed_depth: int = self.get_config(
133
116
  "gateway_recursive_embed_depth"
134
117
  )
135
- self.gateway_artifact_content_limit_bytes: int = self.get_config(
136
- "gateway_artifact_content_limit_bytes"
118
+ self.artifact_handling_mode: str = self.get_config(
119
+ "artifact_handling_mode", "embed"
137
120
  )
138
121
  _ = self.get_config("artifact_service")
139
122
 
@@ -162,9 +145,6 @@ class BaseGatewayComponent(ComponentBase):
162
145
 
163
146
  self.task_context_manager: TaskContextManager = TaskContextManager()
164
147
  self.internal_event_queue: queue.Queue = queue.Queue()
165
- self.message_processor_thread: Optional[threading.Thread] = None
166
- self.async_loop: Optional[asyncio.AbstractEventLoop] = None
167
- self.async_thread: Optional[threading.Thread] = None
168
148
 
169
149
  identity_service_config = self.get_config("identity_service")
170
150
  self.identity_service: Optional[BaseIdentityService] = create_identity_service(
@@ -181,40 +161,6 @@ class BaseGatewayComponent(ComponentBase):
181
161
  "%s Base Gateway Component initialized successfully.", self.log_identifier
182
162
  )
183
163
 
184
- def publish_a2a_message(
185
- self, topic: str, payload: Dict, user_properties: Optional[Dict] = None
186
- ) -> None:
187
- log.debug(
188
- "%s Publishing A2A message to topic: %s via App", self.log_identifier, topic
189
- )
190
- try:
191
- app = self.get_app()
192
- if app:
193
- app.send_message(
194
- payload=payload, topic=topic, user_properties=user_properties
195
- )
196
- log.debug(
197
- "%s Successfully published message to %s via App",
198
- self.log_identifier,
199
- topic,
200
- )
201
- else:
202
- log.error(
203
- "%s Cannot publish message: Not running within a SAC App context.",
204
- self.log_identifier,
205
- )
206
- raise RuntimeError(
207
- "Cannot publish message: Not running within a SAC App context."
208
- )
209
- except Exception as e:
210
- log.exception(
211
- "%s Failed to publish A2A message to topic %s via App: %s",
212
- self.log_identifier,
213
- topic,
214
- e,
215
- )
216
- raise
217
-
218
164
  async def authenticate_and_enrich_user(
219
165
  self, external_event_data: Any
220
166
  ) -> Optional[Dict[str, Any]]:
@@ -255,7 +201,7 @@ class BaseGatewayComponent(ComponentBase):
255
201
  async def submit_a2a_task(
256
202
  self,
257
203
  target_agent_name: str,
258
- a2a_parts: List[A2APart],
204
+ a2a_parts: List[ContentPart],
259
205
  external_request_context: Dict[str, Any],
260
206
  user_identity: Any,
261
207
  is_streaming: bool = True,
@@ -343,7 +289,7 @@ class BaseGatewayComponent(ComponentBase):
343
289
  )
344
290
  external_request_context["a2a_session_id"] = a2a_session_id
345
291
 
346
- a2a_metadata = {}
292
+ a2a_metadata = {"agent_name": target_agent_name}
347
293
  invoked_artifacts = external_request_context.get("invoked_with_artifacts")
348
294
  if invoked_artifacts:
349
295
  a2a_metadata["invoked_with_artifacts"] = invoked_artifacts
@@ -353,67 +299,46 @@ class BaseGatewayComponent(ComponentBase):
353
299
  len(invoked_artifacts),
354
300
  )
355
301
 
356
- a2a_message = A2AMessage(role="user", parts=a2a_parts, metadata=a2a_metadata)
357
- reply_topic_pattern = get_gateway_response_topic(
358
- self.namespace, self.gateway_id, "{task_id}"
359
- )
360
- status_topic_pattern = get_gateway_status_topic(
361
- self.namespace, self.gateway_id, "{task_id}"
362
- )
302
+ # This correlation ID is used by the gateway to track the task
303
+ task_id = f"gdk-task-{uuid.uuid4().hex}"
363
304
 
364
- task_metadata_override: Dict[str, Any] = {}
365
- system_purpose = self.get_config("system_purpose", "")
366
- response_format = self.get_config("response_format", "")
305
+ prepared_a2a_parts = await self._prepare_parts_for_publishing(
306
+ parts=a2a_parts,
307
+ user_id=user_id_for_a2a,
308
+ session_id=a2a_session_id,
309
+ target_agent_name=target_agent_name,
310
+ )
367
311
 
368
- if system_purpose:
369
- task_metadata_override["system_purpose"] = system_purpose
370
- log.debug("%s Adding system_purpose to task metadata.", log_id_prefix)
371
- if response_format:
372
- task_metadata_override["response_format"] = response_format
373
- log.debug("%s Adding response_format to task metadata.", log_id_prefix)
312
+ a2a_message = a2a.create_user_message(
313
+ parts=prepared_a2a_parts,
314
+ metadata=a2a_metadata,
315
+ context_id=a2a_session_id,
316
+ )
374
317
 
375
318
  if is_streaming:
376
- target_topic, payload, user_properties = (
377
- self.core_a2a_service.submit_streaming_task(
378
- agent_name=target_agent_name,
379
- a2a_message=a2a_message,
380
- session_id=a2a_session_id,
381
- client_id=self.gateway_id,
382
- reply_to_topic=reply_topic_pattern,
383
- status_to_topic=status_topic_pattern,
384
- user_id=user_id_for_a2a,
385
- a2a_user_config=user_config,
386
- metadata_override=task_metadata_override,
387
- )
319
+ a2a_request = a2a.create_send_streaming_message_request(
320
+ message=a2a_message, task_id=task_id
388
321
  )
389
322
  else:
390
- target_topic, payload, user_properties = self.core_a2a_service.submit_task(
391
- agent_name=target_agent_name,
392
- a2a_message=a2a_message,
393
- session_id=a2a_session_id,
394
- client_id=self.gateway_id,
395
- reply_to_topic=reply_topic_pattern,
396
- user_id=user_id_for_a2a,
397
- a2a_user_config=user_config,
398
- metadata_override=task_metadata_override,
323
+ a2a_request = a2a.create_send_message_request(
324
+ message=a2a_message, task_id=task_id
399
325
  )
400
326
 
401
- task_id = payload.get("params", {}).get("id")
402
- if not task_id:
403
- log.error(
404
- "%s CoreA2AService did not return a task ID in the payload.",
405
- log_id_prefix,
406
- )
407
- raise ValueError("CoreA2AService did not return a task ID in the payload.")
327
+ payload = a2a_request.model_dump(by_alias=True, exclude_none=True)
328
+ target_topic = a2a.get_agent_request_topic(self.namespace, target_agent_name)
408
329
 
409
- if user_properties is None:
410
- user_properties = {}
330
+ user_properties = {
331
+ "clientId": self.gateway_id,
332
+ "userId": user_id_for_a2a,
333
+ }
334
+ if user_config:
335
+ user_properties["a2aUserConfig"] = user_config
411
336
 
412
- user_properties["replyTo"] = get_gateway_response_topic(
337
+ user_properties["replyTo"] = a2a.get_gateway_response_topic(
413
338
  self.namespace, self.gateway_id, task_id
414
339
  )
415
340
  if is_streaming:
416
- user_properties["a2aStatusTopic"] = get_gateway_status_topic(
341
+ user_properties["a2aStatusTopic"] = a2a.get_gateway_status_topic(
417
342
  self.namespace, self.gateway_id, task_id
418
343
  )
419
344
 
@@ -421,7 +346,7 @@ class BaseGatewayComponent(ComponentBase):
421
346
  log.info("%s Stored external context for task_id: %s", log_id_prefix, task_id)
422
347
 
423
348
  self.publish_a2a_message(
424
- topic=target_topic, payload=payload, user_properties=user_properties
349
+ payload=payload, topic=target_topic, user_properties=user_properties
425
350
  )
426
351
  log.info(
427
352
  "%s Submitted A2A task %s to agent %s. Streaming: %s",
@@ -511,15 +436,12 @@ class BaseGatewayComponent(ComponentBase):
511
436
  )
512
437
  continue
513
438
  try:
514
- signal_data_part = DataPart(
515
- data={"type": "agent_status", "text": status_text},
516
- metadata={"source": "agent_progress_update"},
517
- )
518
- signal_a2a_message = A2AMessage(
519
- role="agent", parts=[signal_data_part]
520
- )
521
- signal_task_status = TaskStatus(
522
- state=TaskState.WORKING, message=signal_a2a_message
439
+ signal_a2a_message = a2a.create_agent_data_message(
440
+ data={
441
+ "type": "agent_progress_update",
442
+ "status_text": status_text,
443
+ },
444
+ part_metadata={"source": "gateway_signal"},
523
445
  )
524
446
  a2a_task_id_for_signal = external_request_context.get(
525
447
  "a2a_task_id_for_event", original_rpc_id
@@ -531,10 +453,11 @@ class BaseGatewayComponent(ComponentBase):
531
453
  )
532
454
  continue
533
455
 
534
- signal_event = TaskStatusUpdateEvent(
535
- id=a2a_task_id_for_signal,
536
- status=signal_task_status,
537
- final=False,
456
+ signal_event = a2a.create_status_update(
457
+ task_id=a2a_task_id_for_signal,
458
+ context_id=external_request_context.get("a2a_session_id"),
459
+ message=signal_a2a_message,
460
+ is_final=False,
538
461
  )
539
462
  await self._send_update_to_external(
540
463
  external_request_context=external_request_context,
@@ -556,104 +479,53 @@ class BaseGatewayComponent(ComponentBase):
556
479
  signal_type,
557
480
  )
558
481
 
559
- async def _resolve_uri_in_file_part(self, part: A2APart):
482
+ async def _resolve_uri_in_file_part(self, file_part: FilePart):
560
483
  """
561
- Checks if a part is a FilePart with a resolvable URI and, if so,
562
- resolves it and mutates the part in-place.
484
+ Checks if a FilePart has a resolvable URI and, if so,
485
+ resolves it and mutates the part in-place by calling the common utility.
563
486
  """
564
- if not (
565
- isinstance(part, FilePart)
566
- and part.file
567
- and part.file.uri
568
- and part.file.uri.startswith("artifact://")
569
- ):
570
- return
571
-
572
- if not self.shared_artifact_service:
573
- log.warning(
574
- "%s Cannot resolve artifact URI, shared_artifact_service is not configured.",
575
- self.log_identifier,
576
- )
577
- return
578
-
579
- uri = part.file.uri
580
- log_id_prefix = f"{self.log_identifier}[ResolveURI]"
581
- try:
582
- log.info("%s Found artifact URI to resolve: %s", log_id_prefix, uri)
583
- parsed_uri = urlparse(uri)
584
- app_name = parsed_uri.netloc
585
- path_parts = parsed_uri.path.strip("/").split("/")
586
-
587
- if not app_name or len(path_parts) != 3:
588
- raise ValueError(
589
- "Invalid URI structure. Expected artifact://app_name/user_id/session_id/filename"
590
- )
591
-
592
- user_id, session_id, filename = path_parts
593
- version = int(parse_qs(parsed_uri.query).get("version", [None])[0])
594
-
595
- loaded_artifact = await load_artifact_content_or_metadata(
596
- artifact_service=self.shared_artifact_service,
597
- app_name=app_name,
598
- user_id=user_id,
599
- session_id=session_id,
600
- filename=filename,
601
- version=version,
602
- return_raw_bytes=True,
603
- )
604
-
605
- if loaded_artifact.get("status") == "success":
606
- content_bytes = loaded_artifact.get("raw_bytes")
607
- part.file.bytes = base64.b64encode(content_bytes).decode("utf-8")
608
- part.file.uri = None
609
- log.info(
610
- "%s Successfully resolved and embedded artifact: %s",
611
- log_id_prefix,
612
- uri,
613
- )
614
- else:
615
- log.error(
616
- "%s Failed to resolve artifact URI '%s': %s",
617
- log_id_prefix,
618
- uri,
619
- loaded_artifact.get("message"),
620
- )
621
- except Exception as e:
622
- log.exception(
623
- "%s Error resolving artifact URI '%s': %s", log_id_prefix, uri, e
624
- )
487
+ await a2a.resolve_file_part_uri(
488
+ part=file_part,
489
+ artifact_service=self.shared_artifact_service,
490
+ log_identifier=self.log_identifier,
491
+ )
625
492
 
626
- async def _resolve_uris_in_parts_list(self, parts: List[A2APart]):
627
- """Iterates over a list of A2APart objects and resolves any FilePart URIs."""
493
+ async def _resolve_uris_in_parts_list(self, parts: List[ContentPart]):
494
+ """Iterates over a list of part objects and resolves any FilePart URIs."""
628
495
  if not parts:
629
496
  return
630
497
  for part in parts:
631
- await self._resolve_uri_in_file_part(part)
498
+ if isinstance(part, FilePart):
499
+ await self._resolve_uri_in_file_part(part)
632
500
 
633
501
  async def _resolve_uris_in_payload(self, parsed_event: Any):
634
502
  """
635
503
  Dispatcher that calls the appropriate targeted URI resolver based on the
636
504
  Pydantic model type of the event.
637
505
  """
506
+ parts_to_resolve: List[ContentPart] = []
638
507
  if isinstance(parsed_event, TaskStatusUpdateEvent):
639
- if parsed_event.status and parsed_event.status.message:
640
- await self._resolve_uris_in_parts_list(
641
- parsed_event.status.message.parts
642
- )
508
+ message = a2a.get_message_from_status_update(parsed_event)
509
+ if message:
510
+ parts_to_resolve.extend(a2a.get_parts_from_message(message))
643
511
  elif isinstance(parsed_event, TaskArtifactUpdateEvent):
644
- if parsed_event.artifact:
645
- await self._resolve_uris_in_parts_list(parsed_event.artifact.parts)
512
+ artifact = a2a.get_artifact_from_artifact_update(parsed_event)
513
+ if artifact:
514
+ parts_to_resolve.extend(a2a.get_parts_from_artifact(artifact))
646
515
  elif isinstance(parsed_event, Task):
647
516
  if parsed_event.status and parsed_event.status.message:
648
- await self._resolve_uris_in_parts_list(
649
- parsed_event.status.message.parts
517
+ parts_to_resolve.extend(
518
+ a2a.get_parts_from_message(parsed_event.status.message)
650
519
  )
651
520
  if parsed_event.artifacts:
652
521
  for artifact in parsed_event.artifacts:
653
- await self._resolve_uris_in_parts_list(artifact.parts)
522
+ parts_to_resolve.extend(a2a.get_parts_from_artifact(artifact))
523
+
524
+ if parts_to_resolve:
525
+ await self._resolve_uris_in_parts_list(parts_to_resolve)
654
526
  else:
655
527
  log.debug(
656
- "%s Payload type '%s' does not support targeted URI resolution. Skipping.",
528
+ "%s Payload type '%s' did not yield any parts for URI resolution. Skipping.",
657
529
  self.log_identifier,
658
530
  type(parsed_event).__name__,
659
531
  )
@@ -673,78 +545,34 @@ class BaseGatewayComponent(ComponentBase):
673
545
  )
674
546
  return False
675
547
 
676
- def _extract_task_id_from_topic(
677
- self, topic: str, subscription_pattern: str
678
- ) -> Optional[str]:
679
- """Extracts the task ID from the end of a topic string based on the subscription."""
680
- base_regex_str = _subscription_to_regex(subscription_pattern).replace(r".*", "")
681
- match = re.match(base_regex_str, topic)
682
- if match:
683
- task_id_part = topic[match.end() :]
684
- task_id = task_id_part.lstrip("/")
685
- if task_id:
686
- log.debug(
687
- "%s Extracted Task ID '%s' from topic '%s'",
688
- self.log_identifier,
689
- task_id,
690
- topic,
691
- )
692
- return task_id
693
- log.warning(
694
- "%s Could not extract Task ID from topic '%s' using pattern '%s'",
695
- self.log_identifier,
696
- topic,
697
- subscription_pattern,
698
- )
699
- return None
700
-
701
- def _parse_a2a_event_from_rpc_result(
702
- self, rpc_result: Dict, expected_task_id: Optional[str]
703
- ) -> Optional[Union[Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent]]:
548
+ async def _prepare_parts_for_publishing(
549
+ self,
550
+ parts: List[ContentPart],
551
+ user_id: str,
552
+ session_id: str,
553
+ target_agent_name: str,
554
+ ) -> List[ContentPart]:
704
555
  """
705
- Parses the result field of a JSONRPCResponse into a specific A2A Pydantic model.
706
- Verifies task ID if expected_task_id is provided.
556
+ Prepares message parts for publishing according to the configured artifact_handling_mode
557
+ by calling the common utility function.
707
558
  """
708
- if not isinstance(rpc_result, dict):
709
- log.error(
710
- "%s RPC result is not a dictionary. Cannot parse.", self.log_identifier
711
- )
712
- return None
713
-
714
- actual_task_id = rpc_result.get("id")
715
- if expected_task_id and actual_task_id != expected_task_id:
716
- log.error(
717
- "%s Task ID mismatch! Expected: %s, Got from payload: %s.",
718
- self.log_identifier,
719
- expected_task_id,
720
- actual_task_id,
721
- )
722
- return None
723
-
724
- try:
725
- if "status" in rpc_result and "final" in rpc_result:
726
- return TaskStatusUpdateEvent(**rpc_result)
727
- elif "artifact" in rpc_result:
728
- return TaskArtifactUpdateEvent(**rpc_result)
729
- elif "status" in rpc_result and "sessionId" in rpc_result:
730
- return Task(**rpc_result)
731
- else:
732
- log.warning(
733
- "%s Unknown result structure in RPC response for task %s: %s",
734
- self.log_identifier,
735
- actual_task_id or "unknown",
736
- rpc_result,
559
+ processed_parts: List[ContentPart] = []
560
+ for part in parts:
561
+ if isinstance(part, FilePart):
562
+ processed_part = await a2a.prepare_file_part_for_publishing(
563
+ part=part,
564
+ mode=self.artifact_handling_mode,
565
+ artifact_service=self.shared_artifact_service,
566
+ user_id=user_id,
567
+ session_id=session_id,
568
+ target_agent_name=target_agent_name,
569
+ log_identifier=self.log_identifier,
737
570
  )
738
- return None
739
- except Exception as e:
740
- log.error(
741
- "%s Failed to parse RPC result into A2A Pydantic model for task %s: %s. Result: %s",
742
- self.log_identifier,
743
- actual_task_id or "unknown",
744
- e,
745
- rpc_result,
746
- )
747
- return None
571
+ if processed_part:
572
+ processed_parts.append(processed_part)
573
+ else:
574
+ processed_parts.append(part)
575
+ return processed_parts
748
576
 
749
577
  async def _resolve_embeds_and_handle_signals(
750
578
  self,
@@ -792,7 +620,7 @@ class BaseGatewayComponent(ComponentBase):
792
620
  parts_owner = event_with_parts.artifact
793
621
 
794
622
  if parts_owner and parts_owner.parts:
795
- new_parts_for_owner: List[A2APart] = []
623
+ new_parts: List[ContentPart] = []
796
624
  stream_buffer_key = f"{a2a_task_id}_stream_buffer"
797
625
  current_buffer = ""
798
626
 
@@ -801,13 +629,19 @@ class BaseGatewayComponent(ComponentBase):
801
629
  self.task_context_manager.get_context(stream_buffer_key) or ""
802
630
  )
803
631
 
804
- for part_obj in parts_owner.parts:
805
- if isinstance(part_obj, TextPart) and part_obj.text is not None:
806
- text_to_resolve = part_obj.text
807
- original_part_text = part_obj.text
632
+ parts: List[ContentPart] = []
633
+ if isinstance(parts_owner, A2AMessage):
634
+ parts = a2a.get_parts_from_message(parts_owner)
635
+ elif isinstance(parts_owner, A2AArtifact):
636
+ parts = a2a.get_parts_from_artifact(parts_owner)
637
+
638
+ for part in parts:
639
+ if isinstance(part, TextPart) and part.text is not None:
640
+ text_to_resolve = part.text
641
+ original_part_text = part.text
808
642
 
809
643
  if is_streaming_status_update:
810
- current_buffer += part_obj.text
644
+ current_buffer += part.text
811
645
  text_to_resolve = current_buffer
812
646
 
813
647
  resolved_text, processed_idx, signals = (
@@ -831,7 +665,7 @@ class BaseGatewayComponent(ComponentBase):
831
665
  content_modified_or_signal_handled = True
832
666
 
833
667
  if resolved_text is not None:
834
- new_parts_for_owner.append(TextPart(text=resolved_text))
668
+ new_parts.append(a2a.create_text_part(text=resolved_text))
835
669
  if is_streaming_status_update:
836
670
  if resolved_text != text_to_resolve[:processed_idx]:
837
671
  content_modified_or_signal_handled = True
@@ -851,61 +685,73 @@ class BaseGatewayComponent(ComponentBase):
851
685
  )
852
686
  content_modified_or_signal_handled = True
853
687
 
854
- elif (
855
- isinstance(part_obj, FilePart)
856
- and part_obj.file
857
- and part_obj.file.bytes
858
- ):
859
- mime_type = part_obj.file.mimeType or ""
860
- is_container = is_text_based_mime_type(mime_type)
861
- try:
862
- decoded_content_for_check = base64.b64decode(
863
- part_obj.file.bytes
864
- ).decode("utf-8", errors="ignore")
865
- if (
866
- is_container
867
- and EMBED_DELIMITER_OPEN in decoded_content_for_check
868
- ):
869
- original_content = decoded_content_for_check
870
- resolved_content = (
871
- await resolve_embeds_recursively_in_string(
872
- text=original_content,
873
- context=embed_eval_context,
874
- resolver_func=evaluate_embed,
875
- types_to_resolve=LATE_EMBED_TYPES,
876
- log_identifier=log_id_prefix,
877
- config=embed_eval_config,
878
- max_depth=self.gateway_recursive_embed_depth,
879
- )
880
- )
881
- if resolved_content != original_content:
882
- new_file_content = part_obj.file.model_copy()
883
- new_file_content.bytes = base64.b64encode(
884
- resolved_content.encode("utf-8")
885
- ).decode("utf-8")
886
- new_parts_for_owner.append(
887
- FilePart(
888
- file=new_file_content,
889
- metadata=part_obj.metadata,
688
+ elif isinstance(part, FilePart) and part.file:
689
+ if isinstance(part.file, FileWithBytes) and part.file.bytes:
690
+ mime_type = part.file.mime_type or ""
691
+ is_container = is_text_based_mime_type(mime_type)
692
+ try:
693
+ decoded_content_for_check = base64.b64decode(
694
+ part.file.bytes
695
+ ).decode("utf-8", errors="ignore")
696
+ if (
697
+ is_container
698
+ and EMBED_DELIMITER_OPEN in decoded_content_for_check
699
+ ):
700
+ original_content = decoded_content_for_check
701
+ resolved_content = (
702
+ await resolve_embeds_recursively_in_string(
703
+ text=original_content,
704
+ context=embed_eval_context,
705
+ resolver_func=evaluate_embed,
706
+ types_to_resolve=LATE_EMBED_TYPES,
707
+ log_identifier=log_id_prefix,
708
+ config=embed_eval_config,
709
+ max_depth=self.gateway_recursive_embed_depth,
890
710
  )
891
711
  )
892
- content_modified_or_signal_handled = True
712
+ if resolved_content != original_content:
713
+ new_file_content = part.file.model_copy()
714
+ new_file_content.bytes = base64.b64encode(
715
+ resolved_content.encode("utf-8")
716
+ ).decode("utf-8")
717
+ new_parts.append(
718
+ FilePart(
719
+ file=new_file_content,
720
+ metadata=part.metadata,
721
+ )
722
+ )
723
+ content_modified_or_signal_handled = True
724
+ else:
725
+ new_parts.append(part)
893
726
  else:
894
- new_parts_for_owner.append(part_obj)
895
- else:
896
- new_parts_for_owner.append(part_obj)
897
- except Exception as e:
898
- log.warning(
899
- "%s Error during recursive FilePart resolution for %s: %s. Using original.",
900
- log_id_prefix,
901
- part_obj.file.name,
902
- e,
903
- )
904
- new_parts_for_owner.append(part_obj)
727
+ new_parts.append(part)
728
+ except Exception as e:
729
+ log.warning(
730
+ "%s Error during recursive FilePart resolution for %s: %s. Using original.",
731
+ log_id_prefix,
732
+ part.file.name,
733
+ e,
734
+ )
735
+ new_parts.append(part)
736
+ else:
737
+ # This is a FileWithUri or empty FileWithBytes, which we don't process for embeds here.
738
+ new_parts.append(part)
905
739
  else:
906
- new_parts_for_owner.append(part_obj)
740
+ new_parts.append(part)
907
741
 
908
- parts_owner.parts = new_parts_for_owner
742
+ if isinstance(parts_owner, A2AMessage):
743
+ if isinstance(event_with_parts, TaskStatusUpdateEvent):
744
+ event_with_parts.status.message = a2a.update_message_parts(
745
+ message=parts_owner, new_parts=new_parts
746
+ )
747
+ elif isinstance(event_with_parts, Task):
748
+ event_with_parts.status.message = a2a.update_message_parts(
749
+ message=parts_owner, new_parts=new_parts
750
+ )
751
+ elif isinstance(parts_owner, A2AArtifact):
752
+ event_with_parts.artifact = a2a.update_artifact_parts(
753
+ artifact=parts_owner, new_parts=new_parts
754
+ )
909
755
 
910
756
  if is_streaming_status_update:
911
757
  self.task_context_manager.store_context(
@@ -945,7 +791,7 @@ class BaseGatewayComponent(ComponentBase):
945
791
  elif isinstance(parsed_event, Task):
946
792
  is_finalizing_context_for_embeds = True
947
793
 
948
- if self.get_config("resolve_artifact_uris_in_gateway", False):
794
+ if self.resolve_artifact_uris_in_gateway:
949
795
  log.debug(
950
796
  "%s Resolving artifact URIs before sending to external...",
951
797
  log_id_prefix,
@@ -1002,13 +848,11 @@ class BaseGatewayComponent(ComponentBase):
1002
848
  log.debug(
1003
849
  "%s Resolving embeds in final task response...", log_id_prefix
1004
850
  )
1005
- combined_text = ""
1006
- non_text_parts = []
1007
- for part in parsed_event.status.message.parts:
1008
- if isinstance(part, TextPart) and part.text:
1009
- combined_text += part.text
1010
- else:
1011
- non_text_parts.append(part)
851
+ message = parsed_event.status.message
852
+ combined_text = a2a.get_text_from_message(message)
853
+ data_parts = a2a.get_data_parts_from_message(message)
854
+ file_parts = a2a.get_file_parts_from_message(message)
855
+ non_text_parts = data_parts + file_parts
1012
856
 
1013
857
  if combined_text:
1014
858
  embed_eval_context = {
@@ -1052,10 +896,15 @@ class BaseGatewayComponent(ComponentBase):
1052
896
  )
1053
897
 
1054
898
  new_parts = (
1055
- [TextPart(text=resolved_text)] if resolved_text else []
899
+ [a2a.create_text_part(text=resolved_text)]
900
+ if resolved_text
901
+ else []
1056
902
  )
1057
903
  new_parts.extend(non_text_parts)
1058
- parsed_event.status.message.parts = new_parts
904
+ parsed_event.status.message = a2a.update_message_parts(
905
+ message=parsed_event.status.message,
906
+ new_parts=new_parts,
907
+ )
1059
908
  log.info(
1060
909
  "%s Final response text updated with resolved embeds.",
1061
910
  log_id_prefix,
@@ -1086,7 +935,7 @@ class BaseGatewayComponent(ComponentBase):
1086
935
  },
1087
936
  }
1088
937
  embed_eval_config = {
1089
- "gateway_artifact_content_limit_bytes": self.gateway_artifact_content_limit_bytes,
938
+ "gateway_max_artifact_resolve_size_bytes": self.gateway_max_artifact_resolve_size_bytes,
1090
939
  "gateway_recursive_embed_depth": self.gateway_recursive_embed_depth,
1091
940
  }
1092
941
  resolved_remaining_text, _, signals = (
@@ -1106,17 +955,14 @@ class BaseGatewayComponent(ComponentBase):
1106
955
  is_finalizing_context=True,
1107
956
  )
1108
957
  if resolved_remaining_text:
1109
- flush_status = TaskStatus(
1110
- state=TaskState.WORKING,
1111
- message=A2AMessage(
1112
- role="agent",
1113
- parts=[TextPart(text=resolved_remaining_text)],
1114
- ),
958
+ flush_message = a2a.create_agent_text_message(
959
+ text=resolved_remaining_text
1115
960
  )
1116
- flush_event = TaskStatusUpdateEvent(
1117
- id=a2a_task_id,
1118
- status=flush_status,
1119
- final=False,
961
+ flush_event = a2a.create_status_update(
962
+ task_id=a2a_task_id,
963
+ context_id=external_request_context.get("a2a_session_id"),
964
+ message=flush_message,
965
+ is_final=False,
1120
966
  )
1121
967
  await self._send_update_to_external(
1122
968
  external_request_context, flush_event, True
@@ -1157,7 +1003,7 @@ class BaseGatewayComponent(ComponentBase):
1157
1003
  Parses the payload, retrieves context using task_id_from_topic, and dispatches for processing.
1158
1004
  """
1159
1005
  try:
1160
- rpc_response = JSONRPCResponse(**payload)
1006
+ rpc_response = JSONRPCResponse.model_validate(payload)
1161
1007
  except Exception as e:
1162
1008
  log.error(
1163
1009
  "%s Failed to parse payload as JSONRPCResponse for topic %s (Task ID from topic: %s): %s. Payload: %s",
@@ -1169,7 +1015,7 @@ class BaseGatewayComponent(ComponentBase):
1169
1015
  )
1170
1016
  return False
1171
1017
 
1172
- original_rpc_id = str(rpc_response.id)
1018
+ original_rpc_id = str(a2a.get_response_id(rpc_response))
1173
1019
 
1174
1020
  external_request_context = self.task_context_manager.get_context(
1175
1021
  task_id_from_topic
@@ -1190,19 +1036,43 @@ class BaseGatewayComponent(ComponentBase):
1190
1036
  parsed_event_obj: Union[
1191
1037
  Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, JSONRPCError, None
1192
1038
  ] = None
1193
- if rpc_response.error:
1194
- parsed_event_obj = rpc_response.error
1195
- elif rpc_response.result:
1196
- parsed_event_obj = self._parse_a2a_event_from_rpc_result(
1197
- rpc_response.result, task_id_from_topic
1198
- )
1039
+ error = a2a.get_response_error(rpc_response)
1040
+ if error:
1041
+ parsed_event_obj = error
1042
+ else:
1043
+ result = a2a.get_response_result(rpc_response)
1044
+ if result:
1045
+ # The result is already a parsed Pydantic model.
1046
+ parsed_event_obj = result
1047
+
1048
+ # Validate task ID match
1049
+ actual_task_id = None
1050
+ if isinstance(parsed_event_obj, Task):
1051
+ actual_task_id = parsed_event_obj.id
1052
+ elif isinstance(
1053
+ parsed_event_obj, (TaskStatusUpdateEvent, TaskArtifactUpdateEvent)
1054
+ ):
1055
+ actual_task_id = parsed_event_obj.task_id
1056
+
1057
+ if (
1058
+ task_id_from_topic
1059
+ and actual_task_id
1060
+ and actual_task_id != task_id_from_topic
1061
+ ):
1062
+ log.error(
1063
+ "%s Task ID mismatch! Expected: %s, Got from payload: %s.",
1064
+ self.log_identifier,
1065
+ task_id_from_topic,
1066
+ actual_task_id,
1067
+ )
1068
+ parsed_event_obj = None
1199
1069
 
1200
1070
  if not parsed_event_obj:
1201
1071
  log.error(
1202
1072
  "%s Failed to parse or validate A2A event from RPC result for task %s. Result: %s",
1203
1073
  self.log_identifier,
1204
1074
  task_id_from_topic,
1205
- rpc_response.result,
1075
+ a2a.get_response_result(rpc_response) or "N/A",
1206
1076
  )
1207
1077
  generic_error = JSONRPCError(
1208
1078
  code=-32000, message="Invalid event structure received from agent."
@@ -1239,9 +1109,36 @@ class BaseGatewayComponent(ComponentBase):
1239
1109
  )
1240
1110
  return False
1241
1111
 
1112
+ async def _async_setup_and_run(self) -> None:
1113
+ """Main async logic for the gateway component."""
1114
+ log.info(
1115
+ "%s Starting _start_listener() to initiate external platform connection.",
1116
+ self.log_identifier,
1117
+ )
1118
+ self._start_listener()
1119
+
1120
+ log.info(
1121
+ "%s Starting _message_processor_loop as an asyncio task.",
1122
+ self.log_identifier,
1123
+ )
1124
+ await self._message_processor_loop()
1125
+
1126
+ def _pre_async_cleanup(self) -> None:
1127
+ """Pre-cleanup actions for the gateway component."""
1128
+ log.info("%s Calling _stop_listener()...", self.log_identifier)
1129
+ self._stop_listener()
1130
+
1131
+ if self.internal_event_queue:
1132
+ log.info(
1133
+ "%s Signaling _message_processor_loop to stop by putting sentinel on queue...",
1134
+ self.log_identifier,
1135
+ )
1136
+ # This unblocks the `self.internal_event_queue.get()` call in the loop
1137
+ self.internal_event_queue.put(None)
1138
+
1242
1139
  async def _message_processor_loop(self):
1243
1140
  log.info("%s Starting message processor loop...", self.log_identifier)
1244
- loop = asyncio.get_running_loop()
1141
+ loop = self.get_async_loop()
1245
1142
 
1246
1143
  while not self.stop_signal.is_set():
1247
1144
  original_broker_message: Optional[SolaceMessage] = None
@@ -1272,38 +1169,38 @@ class BaseGatewayComponent(ComponentBase):
1272
1169
  processed_successfully = False
1273
1170
  continue
1274
1171
 
1275
- if _topic_matches_subscription(
1276
- topic, get_discovery_topic(self.namespace)
1172
+ if a2a.topic_matches_subscription(
1173
+ topic, a2a.get_discovery_topic(self.namespace)
1277
1174
  ):
1278
1175
  processed_successfully = await self._handle_discovery_message(
1279
1176
  payload
1280
1177
  )
1281
- elif _topic_matches_subscription(
1178
+ elif a2a.topic_matches_subscription(
1282
1179
  topic,
1283
- get_gateway_response_subscription_topic(
1180
+ a2a.get_gateway_response_subscription_topic(
1284
1181
  self.namespace, self.gateway_id
1285
1182
  ),
1286
- ) or _topic_matches_subscription(
1183
+ ) or a2a.topic_matches_subscription(
1287
1184
  topic,
1288
- get_gateway_status_subscription_topic(
1185
+ a2a.get_gateway_status_subscription_topic(
1289
1186
  self.namespace, self.gateway_id
1290
1187
  ),
1291
1188
  ):
1292
1189
  task_id_from_topic: Optional[str] = None
1293
- response_sub = get_gateway_response_subscription_topic(
1190
+ response_sub = a2a.get_gateway_response_subscription_topic(
1294
1191
  self.namespace, self.gateway_id
1295
1192
  )
1296
- status_sub = get_gateway_status_subscription_topic(
1193
+ status_sub = a2a.get_gateway_status_subscription_topic(
1297
1194
  self.namespace, self.gateway_id
1298
1195
  )
1299
1196
 
1300
- if _topic_matches_subscription(topic, response_sub):
1301
- task_id_from_topic = self._extract_task_id_from_topic(
1302
- topic, response_sub
1197
+ if a2a.topic_matches_subscription(topic, response_sub):
1198
+ task_id_from_topic = a2a.extract_task_id_from_topic(
1199
+ topic, response_sub, self.log_identifier
1303
1200
  )
1304
- elif _topic_matches_subscription(topic, status_sub):
1305
- task_id_from_topic = self._extract_task_id_from_topic(
1306
- topic, status_sub
1201
+ elif a2a.topic_matches_subscription(topic, status_sub):
1202
+ task_id_from_topic = a2a.extract_task_id_from_topic(
1203
+ topic, status_sub, self.log_identifier
1307
1204
  )
1308
1205
 
1309
1206
  if task_id_from_topic:
@@ -1355,145 +1252,6 @@ class BaseGatewayComponent(ComponentBase):
1355
1252
 
1356
1253
  log.info("%s Message processor loop finished.", self.log_identifier)
1357
1254
 
1358
- def _run_async_operations(self):
1359
- log.info(
1360
- "%s Initializing asyncio event loop in dedicated thread...",
1361
- self.log_identifier,
1362
- )
1363
- self.async_loop = asyncio.new_event_loop()
1364
- asyncio.set_event_loop(self.async_loop)
1365
-
1366
- processor_task = None
1367
- try:
1368
- log.info(
1369
- "%s Starting _message_processor_loop as an asyncio task.",
1370
- self.log_identifier,
1371
- )
1372
- processor_task = self.async_loop.create_task(self._message_processor_loop())
1373
-
1374
- log.info(
1375
- "%s Calling _start_listener() to initiate external platform connection.",
1376
- self.log_identifier,
1377
- )
1378
- self._start_listener()
1379
-
1380
- log.info(
1381
- "%s Running asyncio event loop forever (or until stop_signal).",
1382
- self.log_identifier,
1383
- )
1384
- self.async_loop.run_forever()
1385
-
1386
- except Exception as e:
1387
- log.exception(
1388
- "%s Unhandled exception in _run_async_operations: %s",
1389
- self.log_identifier,
1390
- e,
1391
- )
1392
- self.stop_signal.set()
1393
- finally:
1394
- if processor_task and not processor_task.done():
1395
- log.info(
1396
- "%s Cancelling _message_processor_loop task.", self.log_identifier
1397
- )
1398
- processor_task.cancel()
1399
- try:
1400
- self.async_loop.run_until_complete(
1401
- asyncio.gather(processor_task, return_exceptions=True)
1402
- )
1403
- except RuntimeError as loop_err:
1404
- log.warning(
1405
- "%s Error awaiting processor task during cleanup (loop closed?): %s",
1406
- self.log_identifier,
1407
- loop_err,
1408
- )
1409
-
1410
- if self.async_loop.is_running():
1411
- log.info(
1412
- "%s Stopping asyncio event loop from _run_async_operations finally block.",
1413
- self.log_identifier,
1414
- )
1415
- self.async_loop.stop()
1416
- log.info(
1417
- "%s Async operations loop finished in dedicated thread.",
1418
- self.log_identifier,
1419
- )
1420
-
1421
- def run(self):
1422
- log.info("%s Starting BaseGatewayComponent run method.", self.log_identifier)
1423
- if not self.async_thread or not self.async_thread.is_alive():
1424
- self.async_thread = threading.Thread(
1425
- target=self._run_async_operations,
1426
- name=f"{self.name}_AsyncOpsThread",
1427
- daemon=True,
1428
- )
1429
- self.async_thread.start()
1430
- log.info("%s Async operations thread started.", self.log_identifier)
1431
- else:
1432
- log.warning(
1433
- "%s Async operations thread already running.", self.log_identifier
1434
- )
1435
-
1436
- super().run()
1437
- log.info("%s BaseGatewayComponent run method finished.", self.log_identifier)
1438
-
1439
- def cleanup(self):
1440
- log.info("%s Starting cleanup for BaseGatewayComponent...", self.log_identifier)
1441
-
1442
- log.info("%s Calling _stop_listener()...", self.log_identifier)
1443
- try:
1444
- if (
1445
- self.async_loop
1446
- and not self.async_loop.is_running()
1447
- and self.async_thread
1448
- and self.async_thread.is_alive()
1449
- ):
1450
- log.warning(
1451
- "%s Async loop not running during cleanup, _stop_listener might face issues if it needs the loop.",
1452
- self.log_identifier,
1453
- )
1454
- self._stop_listener()
1455
- except Exception as e:
1456
- log.exception(
1457
- "%s Error during _stop_listener(): %s", self.log_identifier, e
1458
- )
1459
-
1460
- if self.internal_event_queue:
1461
- log.info(
1462
- "%s Signaling _message_processor_loop to stop...", self.log_identifier
1463
- )
1464
- self.internal_event_queue.put(None)
1465
-
1466
- if self.async_loop and self.async_loop.is_running():
1467
- log.info("%s Requesting asyncio loop to stop...", self.log_identifier)
1468
- self.async_loop.call_soon_threadsafe(self.async_loop.stop)
1469
-
1470
- if self.async_thread and self.async_thread.is_alive():
1471
- log.info(
1472
- "%s Joining async operations thread (timeout 10s)...",
1473
- self.log_identifier,
1474
- )
1475
- self.async_thread.join(timeout=10)
1476
- if self.async_thread.is_alive():
1477
- log.warning(
1478
- "%s Async operations thread did not join cleanly.",
1479
- self.log_identifier,
1480
- )
1481
-
1482
- if self.async_loop and not self.async_loop.is_closed():
1483
- if self.async_loop.is_running():
1484
- self.async_loop.call_soon_threadsafe(self.async_loop.stop)
1485
- log.info(
1486
- "%s Closing asyncio event loop (if not already closed by its thread).",
1487
- self.log_identifier,
1488
- )
1489
- if not self.async_loop.is_running():
1490
- self.async_loop.close()
1491
- else:
1492
- self.async_loop.call_soon_threadsafe(self.async_loop.close)
1493
-
1494
- super().cleanup()
1495
- log.info("%s BaseGatewayComponent cleanup finished.", self.log_identifier)
1496
-
1497
1255
  @abstractmethod
1498
1256
  async def _extract_initial_claims(
1499
1257
  self, external_event_data: Any
@@ -1514,17 +1272,30 @@ class BaseGatewayComponent(ComponentBase):
1514
1272
  pass
1515
1273
 
1516
1274
  @abstractmethod
1517
- def _start_listener(self) -> None:
1275
+ async def _translate_external_input(
1276
+ self, external_event: Any
1277
+ ) -> Tuple[str, List[ContentPart], Dict[str, Any]]:
1278
+ """
1279
+ Translates raw platform-specific event data into A2A task parameters.
1280
+
1281
+ Args:
1282
+ external_event: Raw event data from the external platform
1283
+ (e.g., FastAPIRequest, Slack event dictionary).
1284
+
1285
+ Returns:
1286
+ A tuple containing:
1287
+ - target_agent_name (str): The name of the A2A agent to target.
1288
+ - a2a_parts (List[ContentPart]): A list of A2A Part objects.
1289
+ - external_request_context (Dict[str, Any]): Context for TaskContextManager.
1290
+ """
1518
1291
  pass
1519
1292
 
1520
1293
  @abstractmethod
1521
- def _stop_listener(self) -> None:
1294
+ def _start_listener(self) -> None:
1522
1295
  pass
1523
1296
 
1524
1297
  @abstractmethod
1525
- def _translate_external_input(
1526
- self, external_event: Any
1527
- ) -> Tuple[str, List[A2APart], Dict[str, Any]]:
1298
+ def _stop_listener(self) -> None:
1528
1299
  pass
1529
1300
 
1530
1301
  @abstractmethod