bohr-agent-sdk 0.1.17__py3-none-any.whl → 0.1.19__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bohr-agent-sdk
3
- Version: 0.1.17
3
+ Version: 0.1.19
4
4
  Summary: SDK for scientific agents
5
5
  Home-page: https://github.com/dptech-corp/bohr-agent-sdk/
6
6
  Author: DP Technology
@@ -1,7 +1,8 @@
1
1
  dp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  dp/agent/__init__.py,sha256=4kwF1khocxEkqJjnVhBV5cCsWkR_MBGrUthuHk44pT8,153
3
- dp/agent/adapter/adk/__init__.py,sha256=x3OZoxHzBcSP3_LAK9oDulRN1vYkEfMEm5ZdocTLlQA,395
4
- dp/agent/adapter/adk/utils.py,sha256=S6Y9IuQngKXMg0fy8mcCiuAUvOGKYm0xzFFYicgc41I,7025
3
+ dp/agent/adapter/adk/__init__.py,sha256=mxX2SxGgDjwQ7WjfKMw9mQkq4ddl8XlCA6gnUinCW0w,493
4
+ dp/agent/adapter/adk/storage_artifact_service.py,sha256=Hd1CmEB8HgEz3x_riHVSSmYdw6W2bDYYa3c5ySCqmCE,5414
5
+ dp/agent/adapter/adk/utils.py,sha256=Hf6zZOmNi0YSI9t5acmR5kzowWaYr-UfyPiN8fefPkI,7359
5
6
  dp/agent/adapter/adk/client/__init__.py,sha256=F1xfFNa4ZG8jV9adeGI2D3YBiSX-5RkvqEdTqNdJce4,209
6
7
  dp/agent/adapter/adk/client/calculation_mcp_tool.py,sha256=njEf4DvZt7HaAMpMZ1od2emWwlFUP5FtebBJ17pi20U,11774
7
8
  dp/agent/adapter/camel/__init__.py,sha256=RN1NhdmsJyN43fTxTXFld4UKZksjpSV0b2QvFn5gK7o,77
@@ -28,12 +29,12 @@ dp/agent/server/executor/dispatcher_executor.py,sha256=zp9RDUoT2RYZGbKXsUsk4HbS0
28
29
  dp/agent/server/executor/local_executor.py,sha256=alNpEPicZQOQMtXc1vqGSo4El_9agXAJ5ldqi-OJGL4,3887
29
30
  dp/agent/server/storage/__init__.py,sha256=Sgsyp5hb0_hhIGugAPfQFzBHt_854rS_MuMuE3sn8Gs,389
30
31
  dp/agent/server/storage/base_storage.py,sha256=728-oNG6N8isV95gZVnyi4vTznJPJhSjxw9Gl5Y_y5o,2356
31
- dp/agent/server/storage/bohrium_storage.py,sha256=iJqssa6JX3crlaTYGdpmzgOa0X_AbFxYlTESm_k8C88,10474
32
+ dp/agent/server/storage/bohrium_storage.py,sha256=EsKX4dWWvZTn2TEhZv4zsvihfDK0mmPFecrln-Ytk40,10488
32
33
  dp/agent/server/storage/http_storage.py,sha256=KiySq7g9-iJr12XQCKKyJLn8wJoDnSRpQAR5_qPJ1ZU,1471
33
34
  dp/agent/server/storage/local_storage.py,sha256=t1wfjByjXew9ws3PuUxWxmZQ0-Wt1a6t4wmj3fW62GI,1352
34
35
  dp/agent/server/storage/oss_storage.py,sha256=pgjmi7Gir3Y5wkMDCvU4fvSls15fXT7Ax-h9MYHFPK0,3359
35
- bohr_agent_sdk-0.1.17.dist-info/METADATA,sha256=Wn0wUU7TBvmlJZeswoD6wtr7tiYX8WRDDbaVI-Ilwt4,6329
36
- bohr_agent_sdk-0.1.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- bohr_agent_sdk-0.1.17.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
38
- bohr_agent_sdk-0.1.17.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
39
- bohr_agent_sdk-0.1.17.dist-info/RECORD,,
36
+ bohr_agent_sdk-0.1.19.dist-info/METADATA,sha256=tDUixjAnwCjzdtcuXXc-71c2ZhUiJgKmRlXVMQguDEE,6329
37
+ bohr_agent_sdk-0.1.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ bohr_agent_sdk-0.1.19.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
39
+ bohr_agent_sdk-0.1.19.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
40
+ bohr_agent_sdk-0.1.19.dist-info/RECORD,,
@@ -3,6 +3,7 @@ from .client import (
3
3
  CalculationMCPToolset,
4
4
  BackgroundJobWatcher,
5
5
  )
6
+ from .storage_artifact_service import StorageArtifactService
6
7
  from .utils import (
7
8
  search_error_in_memory_handler,
8
9
  update_session_handler,
@@ -11,4 +12,5 @@ from .utils import (
11
12
 
12
13
  __all__ = ["CalculationMCPTool", "CalculationMCPToolset",
13
14
  "update_session_handler", "search_error_in_memory_handler",
14
- "BackgroundJobWatcher", "extract_job_info"]
15
+ "BackgroundJobWatcher", "extract_job_info",
16
+ "StorageArtifactService"]
@@ -0,0 +1,184 @@
1
+
2
+ import logging
3
+ import mimetypes
4
+ import os
5
+ import tempfile
6
+ from typing import Optional
7
+
8
+ from google.adk.artifacts import BaseArtifactService
9
+ from google.genai import types
10
+ from typing_extensions import override
11
+
12
+ from ...server.storage import BaseStorage
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class StorageArtifactService(BaseArtifactService):
18
+ """An artifact service implementation using storage plugin."""
19
+ def __init__(self, storage: BaseStorage):
20
+ self.storage = storage
21
+
22
+ def _file_has_user_namespace(self, filename: str) -> bool:
23
+ """Checks if the filename has a user namespace.
24
+
25
+ Args:
26
+ filename: The filename to check.
27
+
28
+ Returns:
29
+ True if the filename has a user namespace (starts with "user:"),
30
+ False otherwise.
31
+ """
32
+ return filename.startswith("user:")
33
+
34
+ def _get_key(
35
+ self,
36
+ app_name: str,
37
+ user_id: str,
38
+ session_id: str,
39
+ filename: str,
40
+ version: int,
41
+ ) -> str:
42
+ """Constructs the key.
43
+
44
+ Args:
45
+ app_name: The name of the application.
46
+ user_id: The ID of the user.
47
+ session_id: The ID of the session.
48
+ filename: The name of the artifact file.
49
+ version: The version of the artifact.
50
+
51
+ Returns:
52
+ The constructed key.
53
+ """
54
+ if self._file_has_user_namespace(filename):
55
+ return f"{app_name}/{user_id}/user/{filename}/{version}"
56
+ return f"{app_name}/{user_id}/{session_id}/{filename}/{version}"
57
+
58
+ @override
59
+ async def save_artifact(
60
+ self,
61
+ *,
62
+ app_name: str,
63
+ user_id: str,
64
+ session_id: str,
65
+ filename: str,
66
+ artifact: types.Part,
67
+ ) -> int:
68
+ versions = await self.list_versions(
69
+ app_name=app_name,
70
+ user_id=user_id,
71
+ session_id=session_id,
72
+ filename=filename,
73
+ )
74
+ version = 0 if not versions else max(versions) + 1
75
+
76
+ key = self._get_key(
77
+ app_name, user_id, session_id, filename, version
78
+ )
79
+ with tempfile.TemporaryDirectory() as tmpdir:
80
+ path = os.path.join(tmpdir, filename)
81
+ with open(path, "wb") as f:
82
+ f.write(artifact.inline_data.data)
83
+ self.storage._upload(key, path)
84
+
85
+ return version
86
+
87
+ @override
88
+ async def load_artifact(
89
+ self,
90
+ *,
91
+ app_name: str,
92
+ user_id: str,
93
+ session_id: str,
94
+ filename: str,
95
+ version: Optional[int] = None,
96
+ ) -> Optional[types.Part]:
97
+ if version is None:
98
+ versions = await self.list_versions(
99
+ app_name=app_name,
100
+ user_id=user_id,
101
+ session_id=session_id,
102
+ filename=filename,
103
+ )
104
+ if not versions:
105
+ return None
106
+ version = max(versions)
107
+
108
+ key = self._get_key(
109
+ app_name, user_id, session_id, filename, version
110
+ )
111
+ with tempfile.TemporaryDirectory() as tmpdir:
112
+ path = os.path.join(tmpdir, filename)
113
+ self.storage._download(key, path)
114
+ with open(path, "rb") as f:
115
+ artifact_bytes = f.read()
116
+
117
+ mime_type, _ = mimetypes.guess_type(filename)
118
+ artifact = types.Part.from_bytes(
119
+ data=artifact_bytes, mime_type=mime_type
120
+ )
121
+ return artifact
122
+
123
+ @override
124
+ async def list_artifact_keys(
125
+ self, *, app_name: str, user_id: str, session_id: str
126
+ ) -> list[str]:
127
+ filenames = set()
128
+
129
+ session_prefix = f"{app_name}/{user_id}/{session_id}/"
130
+ keys = self.storage.list(session_prefix)
131
+ for key in keys:
132
+ _, _, _, filename, _ = key.split("/")[-5:]
133
+ filenames.add(filename)
134
+
135
+ user_namespace_prefix = f"{app_name}/{user_id}/user/"
136
+ user_namespace_keys = self.storage.list(user_namespace_prefix)
137
+ for key in user_namespace_keys:
138
+ _, _, _, filename, _ = key.split("/")[-5:]
139
+ filenames.add(filename)
140
+
141
+ return sorted(list(filenames))
142
+
143
+ @override
144
+ async def delete_artifact(
145
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
146
+ ) -> None:
147
+ raise NotImplementedError()
148
+
149
+ @override
150
+ async def list_versions(
151
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
152
+ ) -> list[int]:
153
+ prefix = self._get_key(app_name, user_id, session_id, filename, "")
154
+ keys = self.storage.list(prefix)
155
+ versions = []
156
+ for key in keys:
157
+ _, _, _, _, version = key.split("/")[-5:]
158
+ versions.append(int(version))
159
+ return versions
160
+
161
+ async def get_permanent_read_url(
162
+ self,
163
+ *,
164
+ app_name: str,
165
+ user_id: str,
166
+ session_id: str,
167
+ filename: str,
168
+ version: Optional[int] = None,
169
+ ) -> str:
170
+ if version is None:
171
+ versions = await self.list_versions(
172
+ app_name=app_name,
173
+ user_id=user_id,
174
+ session_id=session_id,
175
+ filename=filename,
176
+ )
177
+ if not versions:
178
+ return None
179
+ version = max(versions)
180
+
181
+ key = self._get_key(
182
+ app_name, user_id, session_id, filename, version
183
+ )
184
+ return self.storage.get_http_url(key)
@@ -116,6 +116,7 @@ def search_error_in_memory_handler(toolset):
116
116
  def extract_job_info(events: List[Event]) -> dict:
117
117
  jobs = {}
118
118
  artifacts = {}
119
+ events.sort(key=lambda event: event.timestamp)
119
120
  for event in events:
120
121
  if event.content and event.content.parts:
121
122
  for part in event.content.parts:
@@ -135,39 +136,42 @@ def extract_job_info(events: List[Event]) -> dict:
135
136
  }
136
137
  job = jobs[resp.id]
137
138
  job["timestamp"] = event.timestamp
138
- if "result" in resp.response and isinstance(
139
- resp.response["result"], types.CallToolResult):
140
- res = resp.response["result"]
139
+ res = resp.response.get("result")
140
+ if isinstance(res, dict) and "content" in res \
141
+ and "isError" in res:
142
+ res = types.CallToolResult(
143
+ content=res["content"], isError=res["isError"])
144
+ if isinstance(res, types.CallToolResult):
141
145
  if res.isError:
142
146
  err_msg = res.content[0].text
143
147
  if err_msg.startswith("Error executing tool"):
144
148
  err_msg = err_msg[err_msg.find(":")+2:]
145
149
  job["err_msg"] = err_msg
146
- else:
150
+ elif hasattr(res.content[0], "text"):
147
151
  result = jsonpickle.loads(res.content[0].text)
148
152
  if "job_id" not in result:
149
153
  job["result"] = result
150
- if hasattr(res.content[0], "job_info"):
151
- job_info = res.content[0].job_info
152
- job.update(job_info)
153
- for name, art in job_info.get(
154
- "input_artifacts", {}).items():
155
- if art["uri"] not in artifacts:
156
- artifacts[art["uri"]] = {
157
- "type": "input",
158
- "name": name,
159
- "job_id": job_info["job_id"],
160
- **art,
161
- }
162
- for name, art in job_info.get(
163
- "output_artifacts", {}).items():
164
- if art["uri"] not in artifacts:
165
- artifacts[art["uri"]] = {
166
- "type": "output",
167
- "name": name,
168
- "job_id": job_info["job_id"],
169
- **art,
170
- }
154
+ if hasattr(res.content[0], "job_info"):
155
+ job_info = res.content[0].job_info
156
+ job.update(job_info)
157
+ for name, art in job_info.get(
158
+ "input_artifacts", {}).items():
159
+ if art["uri"] not in artifacts:
160
+ artifacts[art["uri"]] = {
161
+ "type": "input",
162
+ "name": name,
163
+ "job_id": job_info["job_id"],
164
+ **art,
165
+ }
166
+ for name, art in job_info.get(
167
+ "output_artifacts", {}).items():
168
+ if art["uri"] not in artifacts:
169
+ artifacts[art["uri"]] = {
170
+ "type": "output",
171
+ "name": name,
172
+ "job_id": job_info["job_id"],
173
+ **art,
174
+ }
171
175
  return {
172
176
  "jobs": list(jobs.values()),
173
177
  "artifacts": list(artifacts.values()),
@@ -50,9 +50,9 @@ def login(username=None, phone=None, password=None, bohrium_url=None):
50
50
  password = config["password"]
51
51
  if bohrium_url is None:
52
52
  bohrium_url = config["bohrium_url"]
53
- config["authorization"] = _login(
53
+ authorization = _login(
54
54
  bohrium_url + "/account/login", username, phone, password)
55
- return config["authorization"]
55
+ return authorization
56
56
 
57
57
 
58
58
  def _login(login_url=None, username=None, phone=None, password=None):
@@ -256,6 +256,7 @@ class BohriumStorage(BaseStorage):
256
256
  return meta["entityTag"] if "entityTag" in meta else ""
257
257
 
258
258
  def get_http_url(self, key):
259
+ key = self.prefixing(key)
259
260
  url = self.tiefblue_url + "/api/setacl"
260
261
  headers = {
261
262
  "Content-type": "application/json",