solace-agent-mesh 1.0.8__py3-none-any.whl → 1.1.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 (162) 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 +7 -5
  7. solace_agent_mesh/agent/adk/setup.py +11 -0
  8. solace_agent_mesh/agent/adk/stream_parser.py +8 -1
  9. solace_agent_mesh/agent/adk/tool_wrapper.py +10 -3
  10. solace_agent_mesh/agent/agent_llm.txt +355 -18
  11. solace_agent_mesh/agent/protocol/event_handlers.py +433 -296
  12. solace_agent_mesh/agent/protocol/protocol_llm.txt +54 -7
  13. solace_agent_mesh/agent/sac/app.py +1 -1
  14. solace_agent_mesh/agent/sac/component.py +212 -517
  15. solace_agent_mesh/agent/sac/sac_llm.txt +133 -63
  16. solace_agent_mesh/agent/testing/testing_llm.txt +25 -58
  17. solace_agent_mesh/agent/tools/peer_agent_tool.py +15 -11
  18. solace_agent_mesh/agent/tools/tools_llm.txt +234 -69
  19. solace_agent_mesh/agent/utils/artifact_helpers.py +35 -1
  20. solace_agent_mesh/agent/utils/utils_llm.txt +90 -105
  21. solace_agent_mesh/assets/docs/404.html +3 -3
  22. solace_agent_mesh/assets/docs/assets/js/{3d406171.7d02a73b.js → 3d406171.0b9eeed1.js} +1 -1
  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.a75ecc0d.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 +8 -8
  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-1756992446316.json +1 -0
  65. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  66. solace_agent_mesh/assets/docs/search-doc-1756992446316.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/web_add_agent_step.py +12 -3
  71. solace_agent_mesh/cli/commands/add_cmd/web_add_gateway_step.py +10 -14
  72. solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +2 -15
  73. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +6 -2
  74. solace_agent_mesh/cli/utils.py +15 -0
  75. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DvlO62me.js → authCallback-BmF2l6vg.js} +1 -1
  76. solace_agent_mesh/client/webui/frontend/static/assets/{client-bp6u3qVZ.js → client-D881Dttc.js} +4 -4
  77. solace_agent_mesh/client/webui/frontend/static/assets/main-C0jZjYa8.js +699 -0
  78. solace_agent_mesh/client/webui/frontend/static/assets/main-CCeG324-.css +1 -0
  79. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +2 -2
  80. solace_agent_mesh/client/webui/frontend/static/index.html +3 -3
  81. solace_agent_mesh/common/a2a/__init__.py +213 -0
  82. solace_agent_mesh/common/a2a/a2a_llm.txt +182 -0
  83. solace_agent_mesh/common/a2a/artifact.py +328 -0
  84. solace_agent_mesh/common/a2a/events.py +183 -0
  85. solace_agent_mesh/common/a2a/message.py +307 -0
  86. solace_agent_mesh/common/a2a/protocol.py +513 -0
  87. solace_agent_mesh/common/a2a/task.py +127 -0
  88. solace_agent_mesh/common/a2a/translation.py +653 -0
  89. solace_agent_mesh/common/a2a/types.py +54 -0
  90. solace_agent_mesh/common/a2a_spec/a2a.json +2576 -0
  91. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +407 -0
  92. solace_agent_mesh/common/a2a_spec/schemas/agent_progress_update.json +18 -0
  93. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +31 -0
  94. solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +18 -0
  95. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +235 -0
  96. solace_agent_mesh/common/a2a_spec/schemas/tool_invocation_start.json +26 -0
  97. solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +25 -0
  98. solace_agent_mesh/common/agent_registry.py +1 -1
  99. solace_agent_mesh/common/common_llm.txt +192 -70
  100. solace_agent_mesh/common/data_parts.py +99 -0
  101. solace_agent_mesh/common/middleware/middleware_llm.txt +17 -17
  102. solace_agent_mesh/common/sac/__init__.py +0 -0
  103. solace_agent_mesh/common/sac/sac_llm.txt +71 -0
  104. solace_agent_mesh/common/sac/sam_component_base.py +252 -0
  105. solace_agent_mesh/common/services/providers/providers_llm.txt +51 -84
  106. solace_agent_mesh/common/services/services_llm.txt +206 -26
  107. solace_agent_mesh/common/utils/artifact_utils.py +29 -0
  108. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +176 -80
  109. solace_agent_mesh/common/utils/utils_llm.txt +323 -42
  110. solace_agent_mesh/config_portal/backend/common.py +1 -1
  111. solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-MqsrTd6g.js → _index-Bym6YkMd.js} +74 -24
  112. solace_agent_mesh/config_portal/frontend/static/client/assets/{components-B7lKcHVY.js → components-Rk0n-9cK.js} +1 -1
  113. solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-CEumGClk.js → entry.client-mvZjNKiz.js} +1 -1
  114. solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DSo1AH_7.js → index-DzNKzXrc.js} +1 -1
  115. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-d845808d.js +1 -0
  116. solace_agent_mesh/config_portal/frontend/static/client/assets/{root-C4XmHinv.js → root-BWvk5-gF.js} +1 -1
  117. solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
  118. solace_agent_mesh/core_a2a/core_a2a_llm.txt +10 -8
  119. solace_agent_mesh/core_a2a/service.py +20 -44
  120. solace_agent_mesh/gateway/base/app.py +27 -1
  121. solace_agent_mesh/gateway/base/base_llm.txt +177 -72
  122. solace_agent_mesh/gateway/base/component.py +294 -523
  123. solace_agent_mesh/gateway/gateway_llm.txt +299 -58
  124. solace_agent_mesh/gateway/http_sse/component.py +156 -183
  125. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +29 -29
  126. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +272 -36
  127. solace_agent_mesh/gateway/http_sse/main.py +8 -10
  128. solace_agent_mesh/gateway/http_sse/routers/agents.py +1 -1
  129. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +18 -4
  130. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +231 -5
  131. solace_agent_mesh/gateway/http_sse/routers/sessions.py +12 -7
  132. solace_agent_mesh/gateway/http_sse/routers/tasks.py +116 -169
  133. solace_agent_mesh/gateway/http_sse/services/agent_service.py +1 -1
  134. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +89 -135
  135. solace_agent_mesh/gateway/http_sse/services/task_service.py +2 -5
  136. solace_agent_mesh/solace_agent_mesh_llm.txt +362 -0
  137. solace_agent_mesh/templates/gateway_component_template.py +149 -98
  138. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/METADATA +5 -4
  139. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/RECORD +143 -126
  140. solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +0 -1
  141. solace_agent_mesh/assets/docs/assets/js/main.6dba4a66.js +0 -2
  142. solace_agent_mesh/assets/docs/assets/js/runtime~main.6415ad00.js +0 -1
  143. solace_agent_mesh/assets/docs/lunr-index-1756153049706.json +0 -1
  144. solace_agent_mesh/assets/docs/search-doc-1756153049706.json +0 -1
  145. solace_agent_mesh/client/webui/frontend/static/assets/main-BCpII1-0.css +0 -1
  146. solace_agent_mesh/client/webui/frontend/static/assets/main-BucUdn9m.js +0 -673
  147. solace_agent_mesh/common/a2a_protocol.py +0 -564
  148. solace_agent_mesh/common/client/__init__.py +0 -4
  149. solace_agent_mesh/common/client/card_resolver.py +0 -21
  150. solace_agent_mesh/common/client/client.py +0 -85
  151. solace_agent_mesh/common/client/client_llm.txt +0 -133
  152. solace_agent_mesh/common/server/__init__.py +0 -4
  153. solace_agent_mesh/common/server/server.py +0 -122
  154. solace_agent_mesh/common/server/server_llm.txt +0 -169
  155. solace_agent_mesh/common/server/task_manager.py +0 -291
  156. solace_agent_mesh/common/server/utils.py +0 -28
  157. solace_agent_mesh/common/types.py +0 -411
  158. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-28271392.js +0 -1
  159. /solace_agent_mesh/assets/docs/assets/js/{main.6dba4a66.js.LICENSE.txt → main.a75ecc0d.js.LICENSE.txt} +0 -0
  160. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/WHEEL +0 -0
  161. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/entry_points.txt +0 -0
  162. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,653 @@
1
+ """
2
+ Helpers for translating between A2A protocol objects and other domains,
3
+ such as the Google ADK.
4
+ """
5
+
6
+ from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
7
+ import json
8
+ import base64
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from urllib.parse import urlparse, parse_qs
12
+ from solace_ai_connector.common.log import log
13
+ from google.genai import types as adk_types
14
+ from google.adk.events import Event as ADKEvent
15
+
16
+ from a2a.types import (
17
+ Message as A2AMessage,
18
+ TextPart,
19
+ FilePart,
20
+ FileWithBytes,
21
+ FileWithUri,
22
+ DataPart,
23
+ JSONRPCResponse,
24
+ InternalError,
25
+ )
26
+
27
+ from .. import a2a
28
+ from ...agent.utils.context_helpers import get_original_session_id
29
+
30
+ if TYPE_CHECKING:
31
+ from google.adk.artifacts import BaseArtifactService
32
+ from ...agent.sac.component import SamAgentComponent
33
+
34
+
35
+ A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY = "temp:llm_stream_chunks_processed"
36
+ A2A_STATUS_SIGNAL_STORAGE_KEY = "temp:a2a_status_signals_collected"
37
+
38
+
39
+ async def _prepare_a2a_filepart_for_adk(
40
+ part: FilePart,
41
+ component: "SamAgentComponent",
42
+ user_id: str,
43
+ session_id: str,
44
+ ) -> Optional[adk_types.Part]:
45
+ """
46
+ Prepares an incoming A2A FilePart for the ADK by converting it into a
47
+ textual summary of its metadata.
48
+
49
+ - If the part has bytes, it saves it to the artifact store first.
50
+ - If the part has a URI, it loads metadata from the store.
51
+ - It then formats this information into a text string for the LLM.
52
+ """
53
+ from ...agent.utils.artifact_helpers import (
54
+ save_artifact_with_metadata,
55
+ load_artifact_content_or_metadata,
56
+ format_metadata_for_llm,
57
+ )
58
+
59
+ log_id = f"{component.log_identifier}[PrepareFilePartForADK]"
60
+ app_name = component.get_config("agent_name")
61
+ artifact_service = component.artifact_service
62
+
63
+ if not artifact_service:
64
+ log.error(
65
+ "%s Artifact service is not configured. Cannot process FilePart.", log_id
66
+ )
67
+ return adk_types.Part(
68
+ text="[System Note: File part ignored due to missing artifact service.]"
69
+ )
70
+
71
+ filename = None
72
+ version = None
73
+ mime_type = None
74
+
75
+ try:
76
+ if isinstance(part.file, FileWithBytes):
77
+ log.debug("%s FilePart contains bytes. Saving to artifact store.", log_id)
78
+ filename = part.file.name or f"upload-{uuid.uuid4().hex}"
79
+ mime_type = part.file.mime_type or "application/octet-stream"
80
+ content_bytes = base64.b64decode(part.file.bytes)
81
+
82
+ save_result = await save_artifact_with_metadata(
83
+ artifact_service=artifact_service,
84
+ app_name=app_name,
85
+ user_id=user_id,
86
+ session_id=session_id,
87
+ filename=filename,
88
+ content_bytes=content_bytes,
89
+ mime_type=mime_type,
90
+ metadata_dict={"source": "a2a_filepart_upload"},
91
+ timestamp=datetime.now(timezone.utc),
92
+ )
93
+ if save_result["status"] == "success":
94
+ version = save_result["data_version"]
95
+ log.info(
96
+ "%s Saved incoming file '%s' as version %d.",
97
+ log_id,
98
+ filename,
99
+ version,
100
+ )
101
+ else:
102
+ raise IOError(
103
+ f"Failed to save artifact and its metadata: {save_result['message']}"
104
+ )
105
+
106
+ elif isinstance(part.file, FileWithUri):
107
+ log.debug("%s FilePart contains URI. Loading metadata.", log_id)
108
+ uri = part.file.uri
109
+ parsed_uri = urlparse(uri)
110
+ path_parts = parsed_uri.path.strip("/").split("/")
111
+ if len(path_parts) < 3:
112
+ raise ValueError(f"Invalid artifact URI format: {uri}")
113
+ filename = path_parts[-1]
114
+ version_str = parse_qs(parsed_uri.query).get("version", [None])[0]
115
+ version = int(version_str) if version_str else None
116
+ mime_type = part.file.mime_type
117
+
118
+ else:
119
+ raise TypeError("FilePart contains neither bytes nor a valid URI.")
120
+
121
+ # At this point, we must have filename and version to proceed
122
+ if filename is None or version is None:
123
+ raise ValueError("Could not determine filename and version for artifact.")
124
+
125
+ # Fetch metadata only.
126
+ load_result = await load_artifact_content_or_metadata(
127
+ artifact_service=artifact_service,
128
+ app_name=app_name,
129
+ user_id=user_id,
130
+ session_id=get_original_session_id(session_id),
131
+ filename=filename,
132
+ version=version,
133
+ load_metadata_only=True,
134
+ )
135
+
136
+ if load_result["status"] != "success":
137
+ raise RuntimeError(f"Failed to load metadata: {load_result['message']}")
138
+
139
+ metadata_dict = load_result.get("metadata", {})
140
+ metadata_dict["filename"] = filename
141
+ metadata_dict["version"] = version
142
+
143
+ # Format the final text for the LLM
144
+ formatted_summary = format_metadata_for_llm(metadata_dict)
145
+ final_text = (
146
+ "The user has provided the following file as context for your task. "
147
+ "Use the information contained within its metadata to complete your objective. "
148
+ "You can access the full content using your tools if necessary.\n\n"
149
+ f"{formatted_summary}"
150
+ )
151
+ return adk_types.Part(text=final_text)
152
+
153
+ except Exception as e:
154
+ log.exception("%s Error processing FilePart for ADK: %s", log_id, e)
155
+ failed_filename = filename or (part.file.name if part.file else "unknown file")
156
+ return adk_types.Part(
157
+ text=f"[System Note: The file '{failed_filename}' could not be processed. Error: {e}]"
158
+ )
159
+
160
+
161
+ async def translate_a2a_to_adk_content(
162
+ a2a_message: A2AMessage,
163
+ component: "SamAgentComponent",
164
+ user_id: str,
165
+ session_id: str,
166
+ ) -> adk_types.Content:
167
+ """
168
+ Translates an A2A Message object to ADK Content.
169
+ FileParts are converted to textual metadata summaries.
170
+ """
171
+ adk_parts: List[adk_types.Part] = []
172
+ unwrapped_parts = a2a.get_parts_from_message(a2a_message)
173
+ log_identifier = component.log_identifier
174
+
175
+ for part in unwrapped_parts:
176
+ try:
177
+ if isinstance(part, TextPart):
178
+ adk_parts.append(adk_types.Part(text=a2a.get_text_from_text_part(part)))
179
+ elif isinstance(part, FilePart):
180
+ adk_part = await _prepare_a2a_filepart_for_adk(
181
+ part, component, user_id, session_id
182
+ )
183
+ if adk_part:
184
+ adk_parts.append(adk_part)
185
+ elif isinstance(part, DataPart):
186
+ try:
187
+ data_str = json.dumps(a2a.get_data_from_data_part(part), indent=2)
188
+ adk_parts.append(
189
+ adk_types.Part(text=f"Received data:\n```json\n{data_str}\n```")
190
+ )
191
+ except Exception as e:
192
+ log.warning(
193
+ "%s Could not serialize DataPart for ADK: %s",
194
+ log_identifier,
195
+ e,
196
+ )
197
+ adk_parts.append(
198
+ adk_types.Part(text="Received unserializable structured data.")
199
+ )
200
+ else:
201
+ log.warning(
202
+ "%s Unsupported A2A part type: %s", log_identifier, type(part)
203
+ )
204
+ except Exception as e:
205
+ log.exception("%s Error translating A2A part: %s", log_identifier, e)
206
+ adk_parts.append(adk_types.Part(text="[Error processing received part]"))
207
+
208
+ adk_role = "user" if a2a_message.role == "user" else "model"
209
+ return adk_types.Content(role=adk_role, parts=adk_parts)
210
+
211
+
212
+ def translate_adk_function_response_to_a2a_parts(
213
+ adk_part: adk_types.Part,
214
+ ) -> List[a2a.ContentPart]:
215
+ """
216
+ Translates an ADK Part containing a function_response into a list of A2A Parts.
217
+ - If the response is a dict, it becomes a DataPart.
218
+ - Otherwise, it becomes a TextPart.
219
+ """
220
+ if not adk_part.function_response:
221
+ return []
222
+
223
+ a2a_parts: List[a2a.ContentPart] = []
224
+ try:
225
+ response_data = adk_part.function_response.response
226
+ tool_name = adk_part.function_response.name
227
+ if isinstance(response_data, dict):
228
+ a2a_parts.append(
229
+ a2a.create_data_part(
230
+ data=response_data,
231
+ metadata={"tool_name": tool_name},
232
+ )
233
+ )
234
+ else:
235
+ a2a_parts.append(
236
+ a2a.create_text_part(
237
+ text=f"Tool {tool_name} result: {str(response_data)}"
238
+ )
239
+ )
240
+ except Exception:
241
+ # Ensure tool_name is available even if accessing .response fails
242
+ tool_name = "unknown_tool"
243
+ if hasattr(adk_part.function_response, "name"):
244
+ tool_name = adk_part.function_response.name
245
+ a2a_parts.append(
246
+ a2a.create_text_part(text=f"[Tool {tool_name} result omitted]")
247
+ )
248
+ return a2a_parts
249
+
250
+
251
+ def _extract_text_from_parts(parts: List[a2a.ContentPart]) -> str:
252
+ """
253
+ Extracts and combines text/file info from a list of A2A parts
254
+ into a single string for display or logging.
255
+
256
+ Note: This function intentionally ignores DataPart types.
257
+ """
258
+ output_parts = []
259
+ for part in parts:
260
+ if isinstance(part, TextPart):
261
+ output_parts.append(a2a.get_text_from_text_part(part))
262
+ elif isinstance(part, DataPart):
263
+ log.debug("Skipping DataPart in _extract_text_from_parts")
264
+ continue
265
+ elif isinstance(part, FilePart):
266
+ file = a2a.get_file_from_file_part(part)
267
+ file_info = "File: '%s' (%s)" % (
268
+ a2a.get_filename_from_file_part(part) or "unknown",
269
+ a2a.get_mimetype_from_file_part(part) or "unknown",
270
+ )
271
+ if isinstance(file, FileWithUri) and a2a.get_uri_from_file_part(part):
272
+ file_info += " URI: %s" % a2a.get_uri_from_file_part(part)
273
+ elif isinstance(file, FileWithBytes) and a2a.get_bytes_from_file_part(part):
274
+ try:
275
+ size = len(base64.b64decode(a2a.get_bytes_from_file_part(part)))
276
+ file_info += " (Size: %d bytes)" % size
277
+ except Exception:
278
+ file_info += " (Encoded Bytes)"
279
+ output_parts.append(file_info)
280
+ else:
281
+ if isinstance(part, dict):
282
+ part_type = part.get("type")
283
+ if part_type == "text":
284
+ output_parts.append(part.get("text", "[Missing text content]"))
285
+ elif part_type == "data":
286
+ log.debug("Skipping DataPart (dict) in _extract_text_from_parts")
287
+ continue
288
+ elif part_type == "file":
289
+ file_content = part.get("file", {})
290
+ file_info = "File: '%s' (%s)" % (
291
+ file_content.get("name", "unknown"),
292
+ file_content.get("mime_type", "unknown"),
293
+ )
294
+ if file_content.get("uri"):
295
+ file_info += " URI: %s" % file_content["uri"]
296
+ elif file_content.get("bytes"):
297
+ try:
298
+ size = len(base64.b64decode(file_content["bytes"]))
299
+ file_info += " (Size: %d bytes)" % size
300
+ except Exception:
301
+ file_info += " (Encoded Bytes)"
302
+ output_parts.append(file_info)
303
+ else:
304
+ output_parts.append(
305
+ "[Unsupported part type in dict: %s]" % part_type
306
+ )
307
+ else:
308
+ output_parts.append("[Unsupported part type: %s]" % type(part))
309
+
310
+ return "\n".join(output_parts)
311
+
312
+
313
+ def format_adk_event_as_a2a(
314
+ adk_event: ADKEvent,
315
+ a2a_context: Dict,
316
+ log_identifier: str,
317
+ ) -> Tuple[Optional[JSONRPCResponse], List[Tuple[int, Any]]]:
318
+ """
319
+ Translates an intermediate ADK Event (containing content or errors during the run)
320
+ into an A2A JSON-RPC message payload (TaskStatusUpdateEvent or InternalError).
321
+ Also extracts any "a2a_status_signals_collected" from the event's state_delta.
322
+ Returns None if the event should not result in an intermediate A2A message (e.g., empty, non-streaming final).
323
+ Artifact updates are handled separately by the calling component.
324
+
325
+ Note: This function preserves DataPart from function responses.
326
+ """
327
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
328
+ logical_task_id = a2a_context.get("logical_task_id")
329
+ is_streaming = a2a_context.get("is_streaming", False)
330
+
331
+ if adk_event.error_code or adk_event.error_message:
332
+ error_msg = f"Agent error during execution: {adk_event.error_message or adk_event.error_code}"
333
+ log.error("%s ADK Event contains error: %s", log_identifier, error_msg)
334
+ a2a_error = InternalError(
335
+ message=error_msg,
336
+ data={
337
+ "adk_error_code": adk_event.error_code,
338
+ "taskId": logical_task_id,
339
+ },
340
+ )
341
+ return JSONRPCResponse(id=jsonrpc_request_id, error=a2a_error), []
342
+
343
+ signals_to_forward: List[Tuple[int, Any]] = []
344
+ is_final_adk_event = (
345
+ # We have a different definition of final for ADK events:
346
+ # For now, the only long running tool IDs are peer agent tasks, which we
347
+ # need to wait for before considering the event final.
348
+ adk_event.is_final_response()
349
+ and (
350
+ not hasattr(adk_event, "long_running_tool_ids")
351
+ or not adk_event.long_running_tool_ids
352
+ )
353
+ )
354
+
355
+ unwrapped_a2a_parts: List[a2a.ContentPart] = []
356
+ if adk_event.content and adk_event.content.parts:
357
+ for part in adk_event.content.parts:
358
+ try:
359
+ if part.text:
360
+ unwrapped_a2a_parts.append(a2a.create_text_part(text=part.text))
361
+ elif part.inline_data:
362
+ log.debug(
363
+ "%s Skipping ADK inline_data part in status update translation.",
364
+ log_identifier,
365
+ )
366
+ elif part.function_call or part.function_response:
367
+ log.debug(
368
+ "%s Skipping ADK function call part in A2A translation.",
369
+ log_identifier,
370
+ )
371
+ else:
372
+ log.warning(
373
+ "%s Skipping unknown ADK part type during A2A translation: %s",
374
+ log_identifier,
375
+ part,
376
+ )
377
+ except Exception as e:
378
+ log.exception("%s Error translating ADK part: %s", log_identifier, e)
379
+ unwrapped_a2a_parts.append(
380
+ a2a.create_text_part(text="[Error processing agent output part]")
381
+ )
382
+
383
+ if is_final_adk_event and not is_streaming:
384
+ if not unwrapped_a2a_parts:
385
+ log.debug(
386
+ "%s Skipping non-streaming final ADK event %s with no content in format_adk_event_as_a2a.",
387
+ log_identifier,
388
+ adk_event.id,
389
+ )
390
+ return None, signals_to_forward
391
+ else:
392
+ log.debug(
393
+ "%s Processing non-streaming final ADK event %s with content in format_adk_event_as_a2a.",
394
+ log_identifier,
395
+ adk_event.id,
396
+ )
397
+
398
+ should_send_status = (is_streaming and bool(unwrapped_a2a_parts)) or (
399
+ is_final_adk_event and bool(unwrapped_a2a_parts)
400
+ )
401
+
402
+ if not should_send_status:
403
+ log.debug(
404
+ "%s ADK event %s resulted in no intermediate A2A status update to send. Skipping.",
405
+ log_identifier,
406
+ adk_event.id,
407
+ )
408
+ return None, signals_to_forward
409
+
410
+ a2a_message = a2a.create_agent_parts_message(
411
+ parts=unwrapped_a2a_parts,
412
+ message_id=uuid.uuid4().hex,
413
+ )
414
+ is_final_update_for_this_event = is_final_adk_event
415
+
416
+ host_agent_name = a2a_context.get("host_agent_name", "unknown_agent")
417
+ event_metadata = {"agent_name": host_agent_name}
418
+
419
+ intermediate_result_obj = a2a.create_status_update(
420
+ task_id=logical_task_id,
421
+ context_id=a2a_context.get("contextId"),
422
+ message=a2a_message,
423
+ is_final=is_final_update_for_this_event,
424
+ metadata=event_metadata,
425
+ )
426
+ log.debug(
427
+ "%s Formatting intermediate A2A response (TaskStatusUpdateEvent, final=%s) for Task ID %s",
428
+ log_identifier,
429
+ is_final_update_for_this_event,
430
+ logical_task_id,
431
+ )
432
+ json_rpc_response_obj = JSONRPCResponse(
433
+ id=jsonrpc_request_id, result=intermediate_result_obj
434
+ )
435
+ return json_rpc_response_obj, signals_to_forward
436
+
437
+
438
+ async def format_and_route_adk_event(
439
+ adk_event: ADKEvent,
440
+ a2a_context: Dict,
441
+ component,
442
+ ) -> Tuple[Optional[Dict], Optional[str], Optional[Dict], List[Tuple[int, Any]]]:
443
+ """
444
+ Formats an intermediate ADK event (content or error) to an A2A payload dict,
445
+ and determines the target status topic.
446
+ Returns (None, None, []) if no intermediate message should be sent.
447
+ Signal extraction from state_delta is REMOVED as it's handled upstream by SamAgentComponent.
448
+ Final responses and artifact updates are handled elsewhere.
449
+ """
450
+ signals_found: List[Tuple[int, Any]] = []
451
+ try:
452
+ a2a_response_obj, _ = format_adk_event_as_a2a(
453
+ adk_event, a2a_context, component.log_identifier
454
+ )
455
+
456
+ if not a2a_response_obj:
457
+ return None, None, None, []
458
+
459
+ a2a_payload = a2a_response_obj.model_dump(exclude_none=True)
460
+ target_topic = None
461
+ logical_task_id = a2a_context.get("logical_task_id")
462
+ peer_status_topic = a2a_context.get("statusTopic")
463
+ namespace = component.get_config("namespace")
464
+
465
+ if peer_status_topic:
466
+ target_topic = peer_status_topic
467
+ log.debug(
468
+ "%s Determined status update topic (to peer delegator): %s",
469
+ component.log_identifier,
470
+ target_topic,
471
+ )
472
+ else:
473
+ gateway_id = component.get_gateway_id()
474
+ target_topic = a2a.get_gateway_status_topic(
475
+ namespace, gateway_id, logical_task_id
476
+ )
477
+ log.debug(
478
+ "%s Determined status update topic (to gateway): %s",
479
+ component.log_identifier,
480
+ target_topic,
481
+ )
482
+
483
+ user_properties = {}
484
+ if a2a_context.get("a2a_user_config"):
485
+ user_properties["a2aUserConfig"] = a2a_context["a2a_user_config"]
486
+
487
+ return a2a_payload, target_topic, user_properties, signals_found
488
+
489
+ except Exception as e:
490
+ log.exception(
491
+ "%s Error formatting or routing intermediate ADK event %s: %s",
492
+ component.log_identifier,
493
+ adk_event.id,
494
+ e,
495
+ )
496
+ try:
497
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
498
+ logical_task_id = a2a_context.get("logical_task_id")
499
+ namespace = component.get_config("namespace")
500
+ gateway_id = component.get_gateway_id()
501
+ peer_reply_topic = a2a_context.get("replyToTopic")
502
+
503
+ error_response = JSONRPCResponse(
504
+ id=jsonrpc_request_id,
505
+ error=InternalError(
506
+ message=f"Error processing agent event: {e}",
507
+ data={"taskId": logical_task_id},
508
+ ),
509
+ )
510
+ if peer_reply_topic:
511
+ target_topic = peer_reply_topic
512
+ else:
513
+ target_topic = a2a.get_gateway_response_topic(
514
+ namespace, gateway_id, logical_task_id
515
+ )
516
+ user_properties = {}
517
+ if a2a_context.get("a2a_user_config"):
518
+ user_properties["a2aUserConfig"] = a2a_context["a2a_user_config"]
519
+
520
+ return (
521
+ error_response.model_dump(exclude_none=True),
522
+ target_topic,
523
+ user_properties,
524
+ [],
525
+ )
526
+ except Exception as inner_e:
527
+ log.error(
528
+ "%s Failed to generate error response after formatting error: %s",
529
+ component.log_identifier,
530
+ inner_e,
531
+ )
532
+ return None, None, None, []
533
+
534
+
535
+ async def translate_adk_part_to_a2a_filepart(
536
+ adk_part: adk_types.Part,
537
+ filename: str,
538
+ a2a_context: Dict,
539
+ artifact_service: "BaseArtifactService",
540
+ artifact_handling_mode: str,
541
+ adk_app_name: str,
542
+ log_identifier: str,
543
+ version: Optional[int] = None,
544
+ ) -> Optional[FilePart]:
545
+ """
546
+ Translates a loaded ADK Part (with inline_data) to an A2A FilePart
547
+ based on the configured artifact_handling_mode.
548
+ If version is not provided, it will be resolved to the latest.
549
+ """
550
+ from ...common.utils.artifact_utils import get_latest_artifact_version
551
+ from a2a.types import FilePart, FileWithBytes, FileWithUri
552
+
553
+ if artifact_handling_mode == "ignore":
554
+ log.debug(
555
+ "%s Artifact handling mode is 'ignore'. Skipping translation for '%s'.",
556
+ log_identifier,
557
+ filename,
558
+ )
559
+ return None
560
+
561
+ if not adk_part or not adk_part.inline_data:
562
+ log.warning(
563
+ "%s Cannot translate artifact '%s': ADK Part is missing or has no inline_data.",
564
+ log_identifier,
565
+ filename,
566
+ )
567
+ return None
568
+
569
+ resolved_version = version
570
+ if resolved_version is None:
571
+ try:
572
+ resolved_version = await get_latest_artifact_version(
573
+ artifact_service=artifact_service,
574
+ app_name=adk_app_name,
575
+ user_id=a2a_context.get("user_id"),
576
+ session_id=a2a_context.get("session_id"),
577
+ filename=filename,
578
+ )
579
+ if resolved_version is None:
580
+ log.error(
581
+ "%s Could not resolve latest version for artifact '%s'.",
582
+ log_identifier,
583
+ filename,
584
+ )
585
+ return None
586
+ except Exception as e:
587
+ log.exception(
588
+ "%s Failed to resolve latest version for artifact '%s': %s",
589
+ log_identifier,
590
+ filename,
591
+ e,
592
+ )
593
+ return None
594
+
595
+ mime_type = adk_part.inline_data.mime_type
596
+ data_bytes = adk_part.inline_data.data
597
+ file_content: Optional[Union[FileWithBytes, FileWithUri]] = None
598
+
599
+ try:
600
+ if artifact_handling_mode == "embed":
601
+ encoded_bytes = base64.b64encode(data_bytes).decode("utf-8")
602
+ file_content = FileWithBytes(
603
+ name=filename, mime_type=mime_type, bytes=encoded_bytes
604
+ )
605
+ log.debug(
606
+ "%s Embedding artifact '%s' (size: %d bytes) for A2A message.",
607
+ log_identifier,
608
+ filename,
609
+ len(data_bytes),
610
+ )
611
+
612
+ elif artifact_handling_mode == "reference":
613
+ user_id = a2a_context.get("user_id")
614
+ original_session_id = a2a_context.get("session_id")
615
+
616
+ if not all([adk_app_name, user_id, original_session_id]):
617
+ log.error(
618
+ "%s Cannot create artifact reference URI: missing context (app_name, user_id, or session_id).",
619
+ log_identifier,
620
+ )
621
+ return None
622
+
623
+ artifact_uri = f"artifact://{adk_app_name}/{user_id}/{original_session_id}/{filename}?version={resolved_version}"
624
+
625
+ log.info(
626
+ "%s Creating reference URI for artifact: %s",
627
+ log_identifier,
628
+ artifact_uri,
629
+ )
630
+ file_content = FileWithUri(
631
+ name=filename, mime_type=mime_type, uri=artifact_uri
632
+ )
633
+
634
+ if file_content:
635
+ return FilePart(file=file_content)
636
+ else:
637
+ log.warning(
638
+ "%s No FileContent created for artifact '%s' despite mode '%s'.",
639
+ log_identifier,
640
+ filename,
641
+ artifact_handling_mode,
642
+ )
643
+ return None
644
+
645
+ except Exception as e:
646
+ log.exception(
647
+ "%s Error translating artifact '%s' to A2A FilePart (mode: %s): %s",
648
+ log_identifier,
649
+ filename,
650
+ artifact_handling_mode,
651
+ e,
652
+ )
653
+ return None