bohr-agent-sdk 0.1.122__py3-none-any.whl → 0.1.123__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.122
3
+ Version: 0.1.123
4
4
  Summary: SDK for scientific agents
5
5
  Home-page: https://github.com/dptech-corp/bohr-agent-sdk/
6
6
  Author: DP Technology
@@ -62,12 +62,12 @@ dp/agent/device/device/__init__.py,sha256=w7_1S16S1vWUq0RGl0GFgjq2vFkc5oNvy8cQTn
62
62
  dp/agent/device/device/device.py,sha256=9ZRIJth-4qMO-i-u_b_cO3d6a4eTbTQjPaxFsV_zEkc,9643
63
63
  dp/agent/device/device/types.py,sha256=JuxB-hjf1CjjvfBxCLwRAXVFlYS-nPEdiJpBWLFVCzo,1924
64
64
  dp/agent/server/__init__.py,sha256=rckaYd8pbYyB4ENEhgjXKeGMXjdnrgcJpdM1gu5u1Wc,508
65
- dp/agent/server/calculation_mcp_server.py,sha256=oD93SjlcHgSmGtxGJnl4V6cN1icUJSW3KEYt0Lctc6Q,19756
65
+ dp/agent/server/calculation_mcp_server.py,sha256=z4tzhT2hMN9HRiTp0cg2AwYuqlz146SMFbv7bHdAxZ8,23062
66
66
  dp/agent/server/preprocessor.py,sha256=XUWu7QOwo_sIDMYS2b1OTrM33EXEVH_73vk-ju1Ok8A,1264
67
67
  dp/agent/server/utils.py,sha256=cIKaAg8UaP5yMwvIVTgUVBjy-B3S16bEdnucUf4UDIM,2055
68
68
  dp/agent/server/executor/__init__.py,sha256=s95M5qKQk39Yi9qaVJZhk_nfj54quSf7EDghR3OCFUA,248
69
69
  dp/agent/server/executor/base_executor.py,sha256=nR2jI-wFvKoOk8QaK11pnSAkHj2MsE6uyzPWDx-vgJA,3018
70
- dp/agent/server/executor/dispatcher_executor.py,sha256=lWofctAt2Nemrd3OF-IMBVWgVLKVanHDo3iMTE5lot0,12529
70
+ dp/agent/server/executor/dispatcher_executor.py,sha256=LA79QIsTu6YY60mkn6o4iW1VWRqpAcJHQhKMENKPN1c,12643
71
71
  dp/agent/server/executor/local_executor.py,sha256=qRVnfqhvaCGBXr-NO4uxcUteFja2BE_6NXauVJ8vnoo,6642
72
72
  dp/agent/server/storage/__init__.py,sha256=Sgsyp5hb0_hhIGugAPfQFzBHt_854rS_MuMuE3sn8Gs,389
73
73
  dp/agent/server/storage/base_storage.py,sha256=728-oNG6N8isV95gZVnyi4vTznJPJhSjxw9Gl5Y_y5o,2356
@@ -75,8 +75,8 @@ dp/agent/server/storage/bohrium_storage.py,sha256=EsKX4dWWvZTn2TEhZv4zsvihfDK0mm
75
75
  dp/agent/server/storage/http_storage.py,sha256=KiySq7g9-iJr12XQCKKyJLn8wJoDnSRpQAR5_qPJ1ZU,1471
76
76
  dp/agent/server/storage/local_storage.py,sha256=t1wfjByjXew9ws3PuUxWxmZQ0-Wt1a6t4wmj3fW62GI,1352
77
77
  dp/agent/server/storage/oss_storage.py,sha256=pgjmi7Gir3Y5wkMDCvU4fvSls15fXT7Ax-h9MYHFPK0,3359
78
- bohr_agent_sdk-0.1.122.dist-info/METADATA,sha256=6PXBgaHU2sAKQJHYdN52tD18u7uIpmFEAgVbMBeSNWA,11070
79
- bohr_agent_sdk-0.1.122.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
- bohr_agent_sdk-0.1.122.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
81
- bohr_agent_sdk-0.1.122.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
82
- bohr_agent_sdk-0.1.122.dist-info/RECORD,,
78
+ bohr_agent_sdk-0.1.123.dist-info/METADATA,sha256=5ptmdilTXVCoFGsYIAplF1uiG7vn9gwHaBX4xpS2sks,11070
79
+ bohr_agent_sdk-0.1.123.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
80
+ bohr_agent_sdk-0.1.123.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
81
+ bohr_agent_sdk-0.1.123.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
82
+ bohr_agent_sdk-0.1.123.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -7,7 +7,7 @@ from copy import deepcopy
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
9
  from urllib.parse import urlparse
10
- from typing import Annotated, Literal, Optional, List, Dict
10
+ from typing import Annotated, Literal, Optional, List, Dict, Union, Any, get_origin, get_args
11
11
 
12
12
  from mcp.server.fastmcp import FastMCP
13
13
  from mcp.server.fastmcp.utilities.context_injection import (
@@ -30,12 +30,18 @@ CALCULATION_MCP_WORKDIR = os.getenv("CALCULATION_MCP_WORKDIR", os.getcwd())
30
30
 
31
31
 
32
32
  def parse_uri(uri):
33
- scheme = urlparse(uri).scheme
33
+ parsed = urlparse(uri)
34
+ scheme = parsed.scheme
34
35
  if scheme == "":
35
36
  key = uri
36
37
  scheme = "local"
37
38
  else:
38
- key = uri[len(scheme)+3:]
39
+ if parsed.netloc:
40
+ key = parsed.netloc + parsed.path
41
+ else:
42
+ key = parsed.path
43
+ if parsed.query:
44
+ key += "?" + parsed.query
39
45
  return scheme, key
40
46
 
41
47
 
@@ -105,96 +111,128 @@ def terminate_job(job_id: str, executor: Optional[dict] = None):
105
111
  logger.info("Job %s is terminated" % job_id)
106
112
 
107
113
 
114
+ def _normalize_annotation(ann):
115
+ if ann is None:
116
+ return None
117
+ origin = get_origin(ann)
118
+ if origin is Annotated:
119
+ return _normalize_annotation(get_args(ann)[0])
120
+ if origin is Union:
121
+ args = get_args(ann)
122
+ if type(None) in args:
123
+ non_none = [a for a in args if a is not type(None)]
124
+ if non_none:
125
+ return _normalize_annotation(non_none[0])
126
+ return ann
127
+
128
+
129
+ def _download_artifact(uri, storage, storage_type, input_artifacts,
130
+ input_name, path_trace):
131
+ scheme, key = parse_uri(uri)
132
+ if scheme == storage_type:
133
+ s = storage
134
+ else:
135
+ s = storage_dict[scheme]()
136
+ rel = "/".join(str(p) for p in path_trace) if path_trace else ""
137
+ download_dir = os.path.join("inputs", input_name, rel) if rel else os.path.join("inputs", input_name)
138
+ os.makedirs(download_dir, exist_ok=True)
139
+ path = s.download(key, download_dir)
140
+ logger.info("Artifact %s downloaded to %s" % (uri, path))
141
+ if input_name not in input_artifacts:
142
+ input_artifacts[input_name] = {"storage_type": scheme, "uri": []}
143
+ if isinstance(input_artifacts[input_name].get("uri"), list):
144
+ input_artifacts[input_name]["uri"].append(uri)
145
+ return path
146
+
147
+
148
+ def _traverse_and_process(value, annotation, storage_type, storage,
149
+ input_artifacts, input_name, path_trace=None):
150
+ if path_trace is None:
151
+ path_trace = []
152
+ ann = _normalize_annotation(annotation)
153
+ if ann is None:
154
+ return value
155
+ origin = get_origin(ann)
156
+ args = get_args(ann)
157
+
158
+ # Path: only when resolved annotation is Path (Optional[Path] is normalized to Path)
159
+ if ann is Path:
160
+ s = str(value)
161
+ if not s:
162
+ return Path(".")
163
+ parsed = urlparse(s)
164
+ if parsed.scheme and len(parsed.scheme) > 1:
165
+ return Path(_download_artifact(
166
+ s, storage, storage_type, input_artifacts, input_name, path_trace))
167
+ return Path(value)
168
+
169
+ # BaseModel: schema-driven traversal over model fields (check before dict so nesting works)
170
+ if isinstance(ann, type) and issubclass(ann, BaseModel):
171
+ # Convert to dict for traversal; re-instantiate to model at the end so callers get objects
172
+ if isinstance(value, BaseModel):
173
+ value = value.model_dump()
174
+ if not isinstance(value, dict):
175
+ return value
176
+ out = dict(value)
177
+ for field_name, field_info in ann.model_fields.items():
178
+ if field_name in out and out[field_name] is not None:
179
+ out[field_name] = _traverse_and_process(
180
+ out[field_name],
181
+ field_info.annotation,
182
+ storage_type,
183
+ storage,
184
+ input_artifacts,
185
+ input_name,
186
+ path_trace + [field_name],
187
+ )
188
+ # Re-instantiate so tool functions receive model instances (dot notation works)
189
+ try:
190
+ return ann.model_validate(out)
191
+ except Exception as e:
192
+ logger.warning("Failed to re-instantiate model %s: %s", ann.__name__, e)
193
+ return out
194
+
195
+ # List: use inner type from type args
196
+ if origin in (list, List) and isinstance(value, (list, tuple)):
197
+ inner = _normalize_annotation(args[0]) if args else Any
198
+ return [
199
+ _traverse_and_process(
200
+ item, inner, storage_type, storage,
201
+ input_artifacts, input_name, path_trace + [i])
202
+ for i, item in enumerate(value)
203
+ ]
204
+
205
+ # Dict: use value type from type args (e.g. Dict[str, Path] processes values as Path)
206
+ if origin in (dict, Dict) and isinstance(value, dict):
207
+ value_type = _normalize_annotation(args[1]) if (args and len(args) > 1) else Any
208
+ return {
209
+ k: _traverse_and_process(
210
+ v, value_type, storage_type, storage,
211
+ input_artifacts, input_name, path_trace + [k])
212
+ for k, v in value.items()
213
+ }
214
+
215
+ return value
216
+
217
+
108
218
  def handle_input_artifacts(fn, kwargs, storage):
109
219
  storage_type, storage = init_storage(storage)
110
- sig = inspect.signature(fn)
220
+ sig = inspect.signature(fn, eval_str=True)
111
221
  input_artifacts = {}
222
+ new_kwargs = {}
112
223
  for name, param in sig.parameters.items():
113
- if param.annotation is Path or (
114
- param.annotation is Optional[Path] and
115
- kwargs.get(name) is not None):
116
- uri = kwargs[name]
117
- scheme, key = parse_uri(uri)
118
- if scheme == storage_type:
119
- s = storage
120
- else:
121
- s = storage_dict[scheme]()
122
- path = s.download(key, "inputs/%s" % name)
123
- logger.info("Artifact %s downloaded to %s" % (
124
- uri, path))
125
- kwargs[name] = Path(path)
126
- input_artifacts[name] = {
127
- "storage_type": scheme,
128
- "uri": uri,
129
- }
130
- elif param.annotation is List[Path] or (
131
- param.annotation is Optional[List[Path]] and
132
- kwargs.get(name) is not None):
133
- uris = kwargs[name]
134
- new_paths = []
135
- for i, uri in enumerate(uris):
136
- scheme, key = parse_uri(uri)
137
- if scheme == storage_type:
138
- s = storage
139
- else:
140
- s = storage_dict[scheme]()
141
- dest_dir = Path("inputs") / name / f"item_{i:03d}"
142
- dest_dir.mkdir(parents=True, exist_ok=True)
143
- path = s.download(key, str(dest_dir))
144
- new_paths.append(Path(path))
145
- logger.info("Artifact %s downloaded to %s" % (
146
- uri, path))
147
- kwargs[name] = new_paths
148
- input_artifacts[name] = {
149
- "storage_type": storage_type,
150
- "uri": uris,
151
- }
152
- elif param.annotation is Dict[str, Path] or (
153
- param.annotation is Optional[Dict[str, Path]] and
154
- kwargs.get(name) is not None):
155
- uris_dict = kwargs[name]
156
- new_paths_dict = {}
157
- for key_name, uri in uris_dict.items():
158
- scheme, key = parse_uri(uri)
159
- if scheme == storage_type:
160
- s = storage
161
- else:
162
- s = storage_dict[scheme]()
163
- path = s.download(key, f"inputs/{name}/{key_name}")
164
- new_paths_dict[key_name] = Path(path)
165
- logger.info("Artifact %s (key=%s) downloaded to %s" % (
166
- uri, key_name, path))
167
- kwargs[name] = new_paths_dict
168
- input_artifacts[name] = {
169
- "storage_type": storage_type,
170
- "uri": uris_dict,
171
- }
172
- elif param.annotation is Dict[str, List[Path]] or (
173
- param.annotation is Optional[Dict[str, List[Path]]] and
174
- kwargs.get(name) is not None):
175
- uris_dict = kwargs[name]
176
- new_paths_dict = {}
177
- for key_name, uris in uris_dict.items():
178
- new_paths = []
179
- for i, uri in enumerate(uris):
180
- scheme, key = parse_uri(uri)
181
- if scheme == storage_type:
182
- s = storage
183
- else:
184
- s = storage_dict[scheme]()
185
- dest_dir = Path("inputs") / name / key_name / f"item_{i:03d}"
186
- dest_dir.mkdir(parents=True, exist_ok=True)
187
- path = s.download(key, str(dest_dir))
188
- new_paths.append(Path(path))
189
- logger.info("Artifact %s (key=%s) downloaded to %s" % (
190
- uri, key_name, path))
191
- new_paths_dict[key_name] = new_paths
192
- kwargs[name] = new_paths_dict
193
- input_artifacts[name] = {
194
- "storage_type": storage_type,
195
- "uri": uris_dict,
196
- }
197
- return kwargs, input_artifacts
224
+ if name not in kwargs:
225
+ if param.default is not inspect.Parameter.empty:
226
+ new_kwargs[name] = param.default
227
+ continue
228
+ val = kwargs[name]
229
+ if val is None and _normalize_annotation(param.annotation) != param.annotation:
230
+ new_kwargs[name] = val
231
+ continue
232
+ new_kwargs[name] = _traverse_and_process(
233
+ val, param.annotation, storage_type, storage,
234
+ input_artifacts, name)
235
+ return new_kwargs, input_artifacts
198
236
 
199
237
 
200
238
  def handle_output_artifacts(results, exec_id, storage):
@@ -269,6 +307,57 @@ annotation_map = {
269
307
  }
270
308
 
271
309
 
310
+ # Cache for schema models derived from BaseModel (Path -> str in JSON schema)
311
+ _schema_model_cache: Dict[type, type] = {}
312
+
313
+ def get_schema_annotation(annotation: Any) -> Any:
314
+ """
315
+ Map an annotation to the type used in JSON schema (e.g. Path -> str).
316
+ For BaseModel, build a schema model with the same structure but Path fields as str.
317
+ Handles List[...], Dict[...], Optional[...] and nested BaseModel recursively.
318
+ """
319
+ if annotation is None or annotation is type(None):
320
+ return annotation
321
+ origin = get_origin(annotation)
322
+ if origin is Annotated:
323
+ return get_schema_annotation(get_args(annotation)[0])
324
+ if origin is Union:
325
+ args = get_args(annotation)
326
+ if type(None) in args:
327
+ non_none = [a for a in args if a is not type(None)]
328
+ if len(non_none) == 1:
329
+ return Optional[get_schema_annotation(non_none[0])]
330
+ if annotation in annotation_map:
331
+ return annotation_map[annotation]
332
+ # List[X] -> List[schema(X)] so e.g. List[BaseModel] becomes List[BaseModelSchema]
333
+ if origin in (list, List):
334
+ type_args = get_args(annotation)
335
+ inner = get_schema_annotation(type_args[0]) if type_args else Any
336
+ return List[inner]
337
+ # Dict[K, V] -> Dict[K, schema(V)]
338
+ if origin in (dict, Dict):
339
+ type_args = get_args(annotation)
340
+ if type_args and len(type_args) >= 2:
341
+ return Dict[type_args[0], get_schema_annotation(type_args[1])]
342
+ return annotation
343
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
344
+ if annotation not in _schema_model_cache:
345
+ schema_fields = {}
346
+ for name, field_info in annotation.model_fields.items():
347
+ fa = get_schema_annotation(field_info.annotation)
348
+ if field_info.is_required():
349
+ schema_fields[name] = (fa, Field())
350
+ else:
351
+ default = getattr(field_info, "default", None)
352
+ schema_fields[name] = (fa, Field(default=default))
353
+ _schema_model_cache[annotation] = create_model(
354
+ f"{annotation.__name__}Schema",
355
+ **schema_fields,
356
+ )
357
+ return _schema_model_cache[annotation]
358
+ return annotation
359
+
360
+
272
361
  class SubmitResult(BaseModel):
273
362
  job_id: str
274
363
  extra_info: dict | None = None
@@ -329,11 +418,8 @@ class CalculationMCPServer:
329
418
  for n, annotation in \
330
419
  func_arg_metadata.arg_model.__annotations__.items():
331
420
  param = params[n]
332
- if param.annotation in annotation_map:
333
- model_params[n] = Annotated[
334
- (annotation_map[param.annotation], Field())]
335
- else:
336
- model_params[n] = annotation
421
+ schema_ann = get_schema_annotation(param.annotation)
422
+ model_params[n] = Annotated[(schema_ann, Field())]
337
423
  if param.default is not inspect.Parameter.empty:
338
424
  model_params[n] = (model_params[n], param.default)
339
425
  for n, param in inspect.signature(new_fn).parameters.items():
@@ -200,6 +200,9 @@ class DispatcherExecutor(BaseExecutor):
200
200
  if isinstance(value, Path):
201
201
  forward_files.append(str(value))
202
202
 
203
+ if os.path.isdir("inputs") and "inputs" not in forward_files:
204
+ forward_files.append("inputs")
205
+
203
206
  task = {
204
207
  "task_work_path": "./",
205
208
  "outlog": "log",