solace-agent-mesh 1.0.6__py3-none-any.whl → 1.0.8__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 (141) hide show
  1. solace_agent_mesh/agent/adk/artifacts/__init__.py +1 -0
  2. solace_agent_mesh/agent/adk/{filesystem_artifact_service.py → artifacts/filesystem_artifact_service.py} +14 -15
  3. solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +440 -0
  4. solace_agent_mesh/agent/adk/callbacks.py +123 -159
  5. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +316 -0
  6. solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +414 -0
  7. solace_agent_mesh/agent/adk/mcp_content_processor.py +665 -0
  8. solace_agent_mesh/agent/adk/services.py +43 -1
  9. solace_agent_mesh/agent/adk/setup.py +85 -45
  10. solace_agent_mesh/agent/adk/tool_wrapper.py +19 -3
  11. solace_agent_mesh/agent/protocol/event_handlers.py +1 -1
  12. solace_agent_mesh/agent/sac/app.py +67 -0
  13. solace_agent_mesh/agent/sac/component.py +14 -86
  14. solace_agent_mesh/assets/docs/404.html +3 -3
  15. solace_agent_mesh/assets/docs/assets/js/04989206.b9dfe831.js +1 -0
  16. solace_agent_mesh/assets/docs/assets/js/0e682baa.b3bbde9a.js +1 -0
  17. solace_agent_mesh/assets/docs/assets/js/1023fc19.364235d5.js +1 -0
  18. solace_agent_mesh/assets/docs/assets/js/1523c6b4.1b0ec6f9.js +1 -0
  19. solace_agent_mesh/assets/docs/assets/js/166ab619.e8f3a7c7.js +1 -0
  20. solace_agent_mesh/assets/docs/assets/js/21ceee5f.3bf39250.js +1 -0
  21. solace_agent_mesh/assets/docs/assets/js/3d406171.7d02a73b.js +1 -0
  22. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.8ccb9901.js +1 -0
  23. solace_agent_mesh/assets/docs/assets/js/442a8107.b3159bb2.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/4c2787c2.fc6804f2.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/5b4258a4.0d080cd9.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/75384d09.ccd480c4.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/768e31b0.8b51cd70.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/945fb41e.c63791d1.js +1 -0
  29. solace_agent_mesh/assets/docs/assets/js/{9eff14a2.036c35ea.js → 9eff14a2.472b0310.js} +1 -1
  30. solace_agent_mesh/assets/docs/assets/js/a3a92b25.4b7fa6a2.js +1 -0
  31. solace_agent_mesh/assets/docs/assets/js/aba87c2f.76376d7c.js +1 -0
  32. solace_agent_mesh/assets/docs/assets/js/ae4415af.7a2f0bbf.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/b7006a3a.73a79653.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/beecea0d.ae31f6a7.js +1 -0
  35. solace_agent_mesh/assets/docs/assets/js/c2c06897.587b4af5.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/{cd3d4052.ca6eed8c.js → cd3d4052.b6535013.js} +1 -1
  37. solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +1 -0
  38. solace_agent_mesh/assets/docs/assets/js/f897a61a.0aa29dbb.js +1 -0
  39. solace_agent_mesh/assets/docs/assets/js/main.6dba4a66.js +2 -0
  40. solace_agent_mesh/assets/docs/assets/js/runtime~main.6415ad00.js +1 -0
  41. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +28 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +6 -6
  43. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +8 -8
  44. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +5 -5
  45. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +5 -5
  46. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +34 -5
  47. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +5 -5
  49. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +6 -6
  51. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +72 -0
  52. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +5 -5
  53. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +7 -7
  54. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +35 -16
  55. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  56. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +17 -11
  57. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +5 -5
  59. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +6 -6
  61. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +6 -6
  62. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +6 -6
  63. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  64. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +8 -8
  65. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +14 -14
  66. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +8 -8
  67. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
  68. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +6 -6
  69. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +35 -23
  70. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
  71. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +6 -6
  72. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
  73. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
  74. solace_agent_mesh/assets/docs/lunr-index-1756153049706.json +1 -0
  75. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  76. solace_agent_mesh/assets/docs/search-doc-1756153049706.json +1 -0
  77. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  78. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  79. solace_agent_mesh/cli/__init__.py +1 -1
  80. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +1 -1
  81. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +67 -10
  82. solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +2 -2
  83. solace_agent_mesh/cli/commands/eval_cmd.py +8 -2
  84. solace_agent_mesh/cli/commands/init_cmd/__init__.py +20 -2
  85. solace_agent_mesh/cli/commands/init_cmd/env_step.py +25 -1
  86. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +45 -1
  87. solace_agent_mesh/cli/utils.py +21 -12
  88. solace_agent_mesh/client/webui/frontend/static/assets/main-BucUdn9m.js +673 -0
  89. solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
  90. solace_agent_mesh/common/a2a_protocol.py +1 -1
  91. solace_agent_mesh/common/utils/mime_helpers.py +60 -1
  92. solace_agent_mesh/config_portal/backend/server.py +1 -1
  93. solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-xSu2leR8.js → _index-MqsrTd6g.js} +9 -9
  94. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-950eb3be.js → manifest-28271392.js} +1 -1
  95. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  96. solace_agent_mesh/core_a2a/core_a2a_llm.txt +1 -1
  97. solace_agent_mesh/core_a2a/service.py +1 -1
  98. solace_agent_mesh/evaluation/run.py +149 -15
  99. solace_agent_mesh/evaluation/summary_builder.py +5 -3
  100. solace_agent_mesh/gateway/http_sse/dependencies.py +1 -1
  101. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +1 -1
  102. solace_agent_mesh/gateway/http_sse/services/task_service.py +1 -1
  103. solace_agent_mesh/llm_detail.txt +2 -2
  104. solace_agent_mesh/templates/agent_template.yaml +1 -1
  105. solace_agent_mesh/templates/plugin_agent_config_template.yaml +3 -3
  106. solace_agent_mesh/templates/plugin_readme_template.md +1 -1
  107. solace_agent_mesh/templates/shared_config.yaml +8 -1
  108. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/METADATA +4 -1
  109. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/RECORD +113 -108
  110. solace_agent_mesh/assets/docs/assets/js/04989206.da8246cd.js +0 -1
  111. solace_agent_mesh/assets/docs/assets/js/0e682baa.79f0ab22.js +0 -1
  112. solace_agent_mesh/assets/docs/assets/js/1023fc19.8e6d174c.js +0 -1
  113. solace_agent_mesh/assets/docs/assets/js/1523c6b4.91c7bc01.js +0 -1
  114. solace_agent_mesh/assets/docs/assets/js/166ab619.7d97ccaf.js +0 -1
  115. solace_agent_mesh/assets/docs/assets/js/21ceee5f.614fa8dd.js +0 -1
  116. solace_agent_mesh/assets/docs/assets/js/3d406171.9b081d5f.js +0 -1
  117. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.36090198.js +0 -1
  118. solace_agent_mesh/assets/docs/assets/js/442a8107.5ba94b65.js +0 -1
  119. solace_agent_mesh/assets/docs/assets/js/4c2787c2.66ee00e9.js +0 -1
  120. solace_agent_mesh/assets/docs/assets/js/5b4258a4.bda20761.js +0 -1
  121. solace_agent_mesh/assets/docs/assets/js/75384d09.c3991823.js +0 -1
  122. solace_agent_mesh/assets/docs/assets/js/768e31b0.a12673db.js +0 -1
  123. solace_agent_mesh/assets/docs/assets/js/945fb41e.74d728aa.js +0 -1
  124. solace_agent_mesh/assets/docs/assets/js/a3a92b25.26ca071f.js +0 -1
  125. solace_agent_mesh/assets/docs/assets/js/aba87c2f.a6b84da6.js +0 -1
  126. solace_agent_mesh/assets/docs/assets/js/ae4415af.96189a93.js +0 -1
  127. solace_agent_mesh/assets/docs/assets/js/b7006a3a.38c0cf3d.js +0 -1
  128. solace_agent_mesh/assets/docs/assets/js/bb2ef573.56931473.js +0 -1
  129. solace_agent_mesh/assets/docs/assets/js/c2c06897.63b76e9e.js +0 -1
  130. solace_agent_mesh/assets/docs/assets/js/f284c35a.5aff74ab.js +0 -1
  131. solace_agent_mesh/assets/docs/assets/js/f897a61a.862b0514.js +0 -1
  132. solace_agent_mesh/assets/docs/assets/js/main.ea9672b6.js +0 -2
  133. solace_agent_mesh/assets/docs/assets/js/runtime~main.aa687c82.js +0 -1
  134. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +0 -17
  135. solace_agent_mesh/assets/docs/lunr-index-1755285974624.json +0 -1
  136. solace_agent_mesh/assets/docs/search-doc-1755285974624.json +0 -1
  137. solace_agent_mesh/client/webui/frontend/static/assets/main-DzKPMTRs.js +0 -673
  138. /solace_agent_mesh/assets/docs/assets/js/{main.ea9672b6.js.LICENSE.txt → main.6dba4a66.js.LICENSE.txt} +0 -0
  139. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/WHEEL +0 -0
  140. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/entry_points.txt +0 -0
  141. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1 @@
1
+ """Artifact service implementations for Solace Agent Mesh."""
@@ -2,13 +2,12 @@
2
2
  An ADK ArtifactService implementation using the local filesystem for storage.
3
3
  """
4
4
 
5
- import os
5
+ import asyncio
6
6
  import json
7
- import shutil
8
7
  import logging
9
- import asyncio
8
+ import os
9
+ import shutil
10
10
  import unicodedata
11
- from typing import Optional, List
12
11
 
13
12
  from google.adk.artifacts import BaseArtifactService
14
13
  from google.genai import types as adk_types
@@ -124,7 +123,7 @@ class FilesystemArtifactService(BaseArtifactService):
124
123
  artifact_dir,
125
124
  e,
126
125
  )
127
- raise IOError(f"Could not create artifact directory: {e}") from e
126
+ raise OSError(f"Could not create artifact directory: {e}") from e
128
127
 
129
128
  versions = await self.list_versions(
130
129
  app_name=app_name,
@@ -164,7 +163,7 @@ class FilesystemArtifactService(BaseArtifactService):
164
163
  version,
165
164
  )
166
165
  return version
167
- except (IOError, OSError, ValueError, TypeError) as e:
166
+ except (OSError, ValueError, TypeError) as e:
168
167
  logger.error(
169
168
  "%sFailed to save artifact '%s' version %d: %s",
170
169
  log_prefix,
@@ -176,7 +175,7 @@ class FilesystemArtifactService(BaseArtifactService):
176
175
  await asyncio.to_thread(os.remove, version_path)
177
176
  if await asyncio.to_thread(os.path.exists, metadata_path):
178
177
  await asyncio.to_thread(os.remove, metadata_path)
179
- raise IOError(f"Failed to save artifact version {version}: {e}") from e
178
+ raise OSError(f"Failed to save artifact version {version}: {e}") from e
180
179
 
181
180
  @override
182
181
  async def load_artifact(
@@ -186,9 +185,9 @@ class FilesystemArtifactService(BaseArtifactService):
186
185
  user_id: str,
187
186
  session_id: str,
188
187
  filename: str,
189
- version: Optional[int] = None,
190
- ) -> Optional[adk_types.Part]:
191
- log_prefix = "[FSArtifact:Load] "
188
+ version: int | None = None,
189
+ ) -> adk_types.Part | None:
190
+ log_prefix = f"[FSArtifact:Load:{filename}] "
192
191
  filename = self._normalize_filename_unicode(filename)
193
192
  artifact_dir = self._get_artifact_dir(app_name, user_id, session_id, filename)
194
193
 
@@ -228,7 +227,7 @@ class FilesystemArtifactService(BaseArtifactService):
228
227
  try:
229
228
 
230
229
  def _read_metadata_file():
231
- with open(metadata_path, "r", encoding="utf-8") as f:
230
+ with open(metadata_path, encoding="utf-8") as f:
232
231
  return json.load(f)
233
232
 
234
233
  metadata = await asyncio.to_thread(_read_metadata_file)
@@ -253,7 +252,7 @@ class FilesystemArtifactService(BaseArtifactService):
253
252
  )
254
253
  return artifact_part
255
254
 
256
- except (IOError, OSError, json.JSONDecodeError) as e:
255
+ except (OSError, json.JSONDecodeError) as e:
257
256
  logger.error(
258
257
  "%sFailed to load artifact '%s' version %d: %s",
259
258
  log_prefix,
@@ -266,7 +265,7 @@ class FilesystemArtifactService(BaseArtifactService):
266
265
  @override
267
266
  async def list_artifact_keys(
268
267
  self, *, app_name: str, user_id: str, session_id: str
269
- ) -> List[str]:
268
+ ) -> list[str]:
270
269
  log_prefix = "[FSArtifact:ListKeys] "
271
270
  filenames = set()
272
271
  app_name_sanitized = os.path.basename(app_name)
@@ -339,8 +338,8 @@ class FilesystemArtifactService(BaseArtifactService):
339
338
  @override
340
339
  async def list_versions(
341
340
  self, *, app_name: str, user_id: str, session_id: str, filename: str
342
- ) -> List[int]:
343
- log_prefix = "[FSArtifact:ListVersions] "
341
+ ) -> list[int]:
342
+ log_prefix = f"[FSArtifact:ListVersions:{filename}] "
344
343
  artifact_dir = self._get_artifact_dir(app_name, user_id, session_id, filename)
345
344
  versions = []
346
345
 
@@ -0,0 +1,440 @@
1
+ """
2
+ An ADK ArtifactService implementation using Amazon S3 compatible storage.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ import unicodedata
8
+
9
+ import boto3
10
+ from botocore.client import BaseClient
11
+ from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError
12
+ from google.adk.artifacts import BaseArtifactService
13
+ from google.genai import types as adk_types
14
+ from typing_extensions import override
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class S3ArtifactService(BaseArtifactService):
20
+ """
21
+ An artifact service implementation using Amazon S3 compatible storage.
22
+
23
+ Stores artifacts in an S3-compatible bucket with a structured key format:
24
+ {app_name}/{user_id}/{session_id_or_user}/{filename}/{version}
25
+
26
+ Supports AWS S3 and S3-compatible APIs like MinIO.
27
+
28
+ Required S3 Permissions:
29
+ The IAM user or role must have the following minimum permissions for the specific bucket:
30
+ - s3:GetObject: Read artifacts from the bucket
31
+ - s3:PutObject: Store new artifacts to the bucket
32
+ - s3:DeleteObject: Delete artifacts from the bucket
33
+
34
+ Example IAM Policy (replace 'your-bucket-name' with actual bucket):
35
+ {
36
+ "Version": "2012-10-17",
37
+ "Statement": [
38
+ {
39
+ "Effect": "Allow",
40
+ "Action": [
41
+ "s3:GetObject",
42
+ "s3:PutObject",
43
+ "s3:DeleteObject"
44
+ ],
45
+ "Resource": "arn:aws:s3:::your-bucket-name/*"
46
+ },
47
+ {
48
+ "Effect": "Allow",
49
+ "Action": "s3:ListBucket",
50
+ "Resource": "arn:aws:s3:::your-bucket-name"
51
+ }
52
+ ]
53
+ }
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ bucket_name: str,
59
+ s3_client: BaseClient | None = None,
60
+ **kwargs,
61
+ ):
62
+ """
63
+ Args:
64
+ bucket_name: The name of the S3 bucket to use.
65
+ s3_client: Optional pre-configured S3 client. If None, creates a new client.
66
+ **kwargs: Optional parameters for boto3 client configuration.
67
+
68
+ Raises:
69
+ ValueError: If bucket_name is not provided.
70
+ NoCredentialsError: If AWS credentials are not available.
71
+ """
72
+ if not bucket_name:
73
+ raise ValueError("bucket_name cannot be empty for S3ArtifactService")
74
+
75
+ self.bucket_name = bucket_name
76
+
77
+ if s3_client is None:
78
+ try:
79
+ self.s3 = boto3.client("s3", **kwargs)
80
+ except NoCredentialsError as e:
81
+ logger.error("AWS credentials not found. Please configure credentials.")
82
+ raise ValueError(
83
+ "AWS credentials not found. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or configure AWS credentials."
84
+ ) from e
85
+ else:
86
+ self.s3 = s3_client
87
+
88
+ try:
89
+ self.s3.head_bucket(Bucket=self.bucket_name)
90
+ logger.info(
91
+ "S3ArtifactService initialized successfully. Bucket: %s",
92
+ self.bucket_name,
93
+ )
94
+ except ClientError as e:
95
+ error_code = e.response.get("Error", {}).get("Code", "Unknown")
96
+ if error_code == "404":
97
+ logger.error("S3 bucket '%s' does not exist", self.bucket_name)
98
+ raise ValueError(
99
+ f"S3 bucket '{self.bucket_name}' does not exist"
100
+ ) from e
101
+ elif error_code == "403":
102
+ logger.error("Access denied to S3 bucket '%s'", self.bucket_name)
103
+ raise ValueError(
104
+ f"Access denied to S3 bucket '{self.bucket_name}'"
105
+ ) from e
106
+ else:
107
+ logger.error("Failed to access S3 bucket '%s': %s", self.bucket_name, e)
108
+ raise ValueError(
109
+ f"Failed to access S3 bucket '{self.bucket_name}': {e}"
110
+ ) from e
111
+
112
+ def _file_has_user_namespace(self, filename: str) -> bool:
113
+ return filename.startswith("user:")
114
+
115
+ def _get_object_key(
116
+ self,
117
+ app_name: str,
118
+ user_id: str,
119
+ session_id: str,
120
+ filename: str,
121
+ version: int | str,
122
+ ) -> str:
123
+ """Constructs the S3 object key for an artifact."""
124
+ filename = self._normalize_filename_unicode(filename)
125
+
126
+ if self._file_has_user_namespace(filename):
127
+ filename_clean = filename.split(":", 1)[1]
128
+ return f"{app_name}/{user_id}/user/{filename_clean}/{version}"
129
+ return f"{app_name}/{user_id}/{session_id}/{filename}/{version}"
130
+
131
+ def _normalize_filename_unicode(self, filename: str) -> str:
132
+ """Normalizes Unicode characters in a filename to their standard form."""
133
+ return unicodedata.normalize("NFKC", filename)
134
+
135
+ @override
136
+ async def save_artifact(
137
+ self,
138
+ *,
139
+ app_name: str,
140
+ user_id: str,
141
+ session_id: str,
142
+ filename: str,
143
+ artifact: adk_types.Part,
144
+ ) -> int:
145
+ log_prefix = f"[S3Artifact:Save:{filename}] "
146
+
147
+ if not artifact.inline_data or artifact.inline_data.data is None:
148
+ raise ValueError("Artifact Part has no inline_data to save.")
149
+
150
+ filename = self._normalize_filename_unicode(filename)
151
+
152
+ # Get existing versions to determine next version number
153
+ versions = await self.list_versions(
154
+ app_name=app_name,
155
+ user_id=user_id,
156
+ session_id=session_id,
157
+ filename=filename,
158
+ )
159
+ version = 0 if not versions else max(versions) + 1
160
+
161
+ object_key = self._get_object_key(
162
+ app_name, user_id, session_id, filename, version
163
+ )
164
+
165
+ try:
166
+
167
+ def _put_object():
168
+ return self.s3.put_object(
169
+ Bucket=self.bucket_name,
170
+ Key=object_key,
171
+ Body=artifact.inline_data.data,
172
+ ContentType=artifact.inline_data.mime_type,
173
+ Metadata={
174
+ "original_filename": filename,
175
+ "user_id": user_id,
176
+ "session_id": session_id,
177
+ "version": str(version),
178
+ },
179
+ )
180
+
181
+ await asyncio.to_thread(_put_object)
182
+
183
+ logger.info(
184
+ "%sSaved artifact '%s' version %d successfully to S3 key: %s",
185
+ log_prefix,
186
+ filename,
187
+ version,
188
+ object_key,
189
+ )
190
+ return version
191
+
192
+ except ClientError as e:
193
+ logger.error(
194
+ "%sFailed to save artifact '%s' version %d to S3: %s",
195
+ log_prefix,
196
+ filename,
197
+ version,
198
+ e,
199
+ )
200
+ raise OSError(
201
+ f"Failed to save artifact version {version} to S3: {e}"
202
+ ) from e
203
+ except BotoCoreError as e:
204
+ logger.error(
205
+ "%sBotoCore error saving artifact '%s' version %d: %s",
206
+ log_prefix,
207
+ filename,
208
+ version,
209
+ e,
210
+ )
211
+ raise OSError(
212
+ f"BotoCore error saving artifact version {version}: {e}"
213
+ ) from e
214
+
215
+ @override
216
+ async def load_artifact(
217
+ self,
218
+ *,
219
+ app_name: str,
220
+ user_id: str,
221
+ session_id: str,
222
+ filename: str,
223
+ version: int | None = None,
224
+ ) -> adk_types.Part | None:
225
+ log_prefix = f"[S3Artifact:Load:{filename}] "
226
+ filename = self._normalize_filename_unicode(filename)
227
+
228
+ load_version = version
229
+ if load_version is None:
230
+ versions = await self.list_versions(
231
+ app_name=app_name,
232
+ user_id=user_id,
233
+ session_id=session_id,
234
+ filename=filename,
235
+ )
236
+ if not versions:
237
+ logger.debug("%sNo versions found for artifact.", log_prefix)
238
+ return None
239
+ load_version = max(versions)
240
+ logger.debug("%sLoading latest version: %d", log_prefix, load_version)
241
+ else:
242
+ logger.debug("%sLoading specified version: %d", log_prefix, load_version)
243
+
244
+ object_key = self._get_object_key(
245
+ app_name, user_id, session_id, filename, load_version
246
+ )
247
+
248
+ try:
249
+
250
+ def _get_object():
251
+ return self.s3.get_object(Bucket=self.bucket_name, Key=object_key)
252
+
253
+ response = await asyncio.to_thread(_get_object)
254
+ data = response["Body"].read()
255
+ mime_type = response.get("ContentType", "application/octet-stream")
256
+
257
+ artifact_part = adk_types.Part.from_bytes(data=data, mime_type=mime_type)
258
+
259
+ logger.info(
260
+ "%sLoaded artifact '%s' version %d successfully (%d bytes, %s)",
261
+ log_prefix,
262
+ filename,
263
+ load_version,
264
+ len(data),
265
+ mime_type,
266
+ )
267
+ return artifact_part
268
+
269
+ except ClientError as e:
270
+ error_code = e.response.get("Error", {}).get("Code", "Unknown")
271
+ if error_code == "NoSuchKey":
272
+ logger.debug("%sArtifact not found: %s", log_prefix, object_key)
273
+ return None
274
+ else:
275
+ logger.error(
276
+ "%sFailed to load artifact '%s' version %d from S3: %s",
277
+ log_prefix,
278
+ filename,
279
+ load_version,
280
+ e,
281
+ )
282
+ return None
283
+ except BotoCoreError as e:
284
+ logger.error(
285
+ "%sBotoCore error loading artifact '%s' version %d: %s",
286
+ log_prefix,
287
+ filename,
288
+ load_version,
289
+ e,
290
+ )
291
+ return None
292
+
293
+ @override
294
+ async def list_artifact_keys(
295
+ self, *, app_name: str, user_id: str, session_id: str
296
+ ) -> list[str]:
297
+ log_prefix = "[S3Artifact:ListKeys] "
298
+ filenames = set()
299
+
300
+ session_prefix = f"{app_name}/{user_id}/{session_id}/"
301
+ try:
302
+
303
+ def _list_session_objects():
304
+ paginator = self.s3.get_paginator("list_objects_v2")
305
+ return paginator.paginate(
306
+ Bucket=self.bucket_name, Prefix=session_prefix
307
+ )
308
+
309
+ session_pages = await asyncio.to_thread(_list_session_objects)
310
+ for page in session_pages:
311
+ for obj in page.get("Contents", []):
312
+ parts = obj["Key"].split("/")
313
+ if len(parts) >= 5: # scope/user/session/filename/version
314
+ filename = parts[3]
315
+ filenames.add(filename)
316
+ except ClientError as e:
317
+ logger.warning(
318
+ "%sError listing session objects with prefix '%s': %s",
319
+ log_prefix,
320
+ session_prefix,
321
+ e,
322
+ )
323
+
324
+ user_prefix = f"{app_name}/{user_id}/user/"
325
+ try:
326
+
327
+ def _list_user_objects():
328
+ paginator = self.s3.get_paginator("list_objects_v2")
329
+ return paginator.paginate(Bucket=self.bucket_name, Prefix=user_prefix)
330
+
331
+ user_pages = await asyncio.to_thread(_list_user_objects)
332
+ for page in user_pages:
333
+ for obj in page.get("Contents", []):
334
+ parts = obj["Key"].split("/")
335
+ if len(parts) >= 5: # scope/user/user/filename/version
336
+ filename = parts[3]
337
+ filenames.add(f"user:{filename}")
338
+ except ClientError as e:
339
+ logger.warning(
340
+ "%sError listing user objects with prefix '%s': %s",
341
+ log_prefix,
342
+ user_prefix,
343
+ e,
344
+ )
345
+
346
+ sorted_filenames = sorted(list(filenames))
347
+ logger.debug("%sFound %d artifact keys.", log_prefix, len(sorted_filenames))
348
+ return sorted_filenames
349
+
350
+ @override
351
+ async def delete_artifact(
352
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
353
+ ) -> None:
354
+ log_prefix = f"[S3Artifact:Delete:{filename}] "
355
+ filename = self._normalize_filename_unicode(filename)
356
+
357
+ # Get all versions to delete
358
+ versions = await self.list_versions(
359
+ app_name=app_name,
360
+ user_id=user_id,
361
+ session_id=session_id,
362
+ filename=filename,
363
+ )
364
+
365
+ if not versions:
366
+ logger.debug("%sNo versions found to delete for artifact.", log_prefix)
367
+ return
368
+
369
+ # Delete all versions
370
+ for version in versions:
371
+ object_key = self._get_object_key(
372
+ app_name, user_id, session_id, filename, version
373
+ )
374
+ try:
375
+
376
+ def _delete_object():
377
+ return self.s3.delete_object(
378
+ Bucket=self.bucket_name, Key=object_key
379
+ )
380
+
381
+ await asyncio.to_thread(_delete_object)
382
+ logger.debug(
383
+ "%sDeleted version %d: %s", log_prefix, version, object_key
384
+ )
385
+ except ClientError as e:
386
+ logger.warning(
387
+ "%sFailed to delete version %d (%s): %s",
388
+ log_prefix,
389
+ version,
390
+ object_key,
391
+ e,
392
+ )
393
+
394
+ logger.info(
395
+ "%sDeleted artifact '%s' (%d versions)",
396
+ log_prefix,
397
+ filename,
398
+ len(versions),
399
+ )
400
+
401
+ @override
402
+ async def list_versions(
403
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
404
+ ) -> list[int]:
405
+ log_prefix = f"[S3Artifact:ListVersions:{filename}] "
406
+ filename = self._normalize_filename_unicode(filename)
407
+
408
+ # Get the prefix for this specific artifact (without version)
409
+ prefix = self._get_object_key(app_name, user_id, session_id, filename, "")
410
+ versions = []
411
+
412
+ try:
413
+
414
+ def _list_objects():
415
+ paginator = self.s3.get_paginator("list_objects_v2")
416
+ return paginator.paginate(Bucket=self.bucket_name, Prefix=prefix)
417
+
418
+ pages = await asyncio.to_thread(_list_objects)
419
+ for page in pages:
420
+ for obj in page.get("Contents", []):
421
+ parts = obj["Key"].split("/")
422
+ if len(parts) >= 5: # scope/user/session_or_user/filename/version
423
+ try:
424
+ version = int(parts[4])
425
+ versions.append(version)
426
+ except ValueError:
427
+ continue # Skip non-integer versions
428
+
429
+ except ClientError as e:
430
+ logger.error(
431
+ "%sError listing versions with prefix '%s': %s",
432
+ log_prefix,
433
+ prefix,
434
+ e,
435
+ )
436
+ return []
437
+
438
+ sorted_versions = sorted(versions)
439
+ logger.debug("%sFound versions: %s", log_prefix, sorted_versions)
440
+ return sorted_versions