solace-agent-mesh 1.0.6__py3-none-any.whl → 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/adk/artifacts/__init__.py +1 -0
- solace_agent_mesh/agent/adk/{filesystem_artifact_service.py → artifacts/filesystem_artifact_service.py} +14 -15
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +440 -0
- solace_agent_mesh/agent/adk/callbacks.py +123 -159
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +316 -0
- solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +414 -0
- solace_agent_mesh/agent/adk/mcp_content_processor.py +665 -0
- solace_agent_mesh/agent/adk/services.py +35 -1
- solace_agent_mesh/agent/adk/setup.py +85 -45
- solace_agent_mesh/agent/adk/tool_wrapper.py +19 -3
- solace_agent_mesh/agent/protocol/event_handlers.py +1 -1
- solace_agent_mesh/agent/sac/app.py +67 -0
- solace_agent_mesh/agent/sac/component.py +14 -86
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/04989206.b9dfe831.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/0e682baa.b3bbde9a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1023fc19.364235d5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1523c6b4.1b0ec6f9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/166ab619.e8f3a7c7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/21ceee5f.3bf39250.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3d406171.7d02a73b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/42b3f8d8.8ccb9901.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/442a8107.b3159bb2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4c2787c2.fc6804f2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5b4258a4.0d080cd9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/75384d09.ccd480c4.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/768e31b0.8b51cd70.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/945fb41e.c63791d1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{9eff14a2.036c35ea.js → 9eff14a2.472b0310.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/a3a92b25.4b7fa6a2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/aba87c2f.76376d7c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ae4415af.7a2f0bbf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/b7006a3a.73a79653.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/beecea0d.ae31f6a7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c2c06897.587b4af5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{cd3d4052.ca6eed8c.js → cd3d4052.b6535013.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f897a61a.0aa29dbb.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.d79f063b.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.6415ad00.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +28 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +8 -8
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +34 -5
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +72 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +7 -7
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +35 -16
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +17 -11
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +8 -8
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +8 -8
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +35 -23
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
- solace_agent_mesh/assets/docs/lunr-index-1756146501924.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1756146501924.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +1 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +67 -10
- solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +2 -2
- solace_agent_mesh/cli/commands/eval_cmd.py +8 -2
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +20 -2
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +25 -1
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +45 -1
- solace_agent_mesh/cli/utils.py +21 -12
- solace_agent_mesh/client/webui/frontend/static/assets/main-BucUdn9m.js +673 -0
- solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
- solace_agent_mesh/common/a2a_protocol.py +1 -1
- solace_agent_mesh/common/utils/mime_helpers.py +60 -1
- solace_agent_mesh/config_portal/backend/server.py +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-xSu2leR8.js → _index-MqsrTd6g.js} +9 -9
- solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-950eb3be.js → manifest-28271392.js} +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +1 -1
- solace_agent_mesh/core_a2a/service.py +1 -1
- solace_agent_mesh/evaluation/run.py +149 -15
- solace_agent_mesh/evaluation/summary_builder.py +5 -3
- solace_agent_mesh/gateway/http_sse/dependencies.py +1 -1
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +1 -1
- solace_agent_mesh/gateway/http_sse/services/task_service.py +1 -1
- solace_agent_mesh/llm_detail.txt +2 -2
- solace_agent_mesh/templates/agent_template.yaml +1 -1
- solace_agent_mesh/templates/plugin_agent_config_template.yaml +3 -3
- solace_agent_mesh/templates/plugin_readme_template.md +1 -1
- solace_agent_mesh/templates/shared_config.yaml +8 -1
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.7.dist-info}/METADATA +4 -1
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.7.dist-info}/RECORD +113 -108
- solace_agent_mesh/assets/docs/assets/js/04989206.da8246cd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/0e682baa.79f0ab22.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/1023fc19.8e6d174c.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/1523c6b4.91c7bc01.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/166ab619.7d97ccaf.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/21ceee5f.614fa8dd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3d406171.9b081d5f.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/42b3f8d8.36090198.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/442a8107.5ba94b65.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/4c2787c2.66ee00e9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/5b4258a4.bda20761.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/75384d09.c3991823.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/768e31b0.a12673db.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/945fb41e.74d728aa.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/a3a92b25.26ca071f.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/aba87c2f.a6b84da6.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ae4415af.96189a93.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/b7006a3a.38c0cf3d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/bb2ef573.56931473.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/c2c06897.63b76e9e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.5aff74ab.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f897a61a.862b0514.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/main.ea9672b6.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.aa687c82.js +0 -1
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +0 -17
- solace_agent_mesh/assets/docs/lunr-index-1755285974624.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1755285974624.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-DzKPMTRs.js +0 -673
- /solace_agent_mesh/assets/docs/assets/js/{main.ea9672b6.js.LICENSE.txt → main.d79f063b.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.7.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.7.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.7.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
|
|
5
|
+
import asyncio
|
|
6
6
|
import json
|
|
7
|
-
import shutil
|
|
8
7
|
import logging
|
|
9
|
-
import
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
190
|
-
) ->
|
|
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,
|
|
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 (
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|