bohr-agent-sdk 0.1.110__py3-none-any.whl → 0.1.112__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.110
3
+ Version: 0.1.112
4
4
  Summary: SDK for scientific agents
5
5
  Home-page: https://github.com/dptech-corp/bohr-agent-sdk/
6
6
  Author: DP Technology
@@ -51,7 +51,7 @@ dp/agent/cli/templates/ui/server/session_manager.py,sha256=ZbNHGCFvswa-LKWn6c6RM
51
51
  dp/agent/cli/templates/ui/server/user_files.py,sha256=khkiyY2UOOysHqO6JgCPUDqtrInp83G1M62i3Lj-0aY,2995
52
52
  dp/agent/cli/templates/ui/server/utils.py,sha256=f4NfwFBq_RdZyFn_KCW6ZThYW8TvQyVruK7PJZ-DA80,1530
53
53
  dp/agent/client/__init__.py,sha256=yu7HYZwAkD7g5dL9JttLkGmspBcyOf-6OoCjci4oPDA,59
54
- dp/agent/client/mcp_client.py,sha256=xp8GSM5effjjp34yGp5-HwldalyqT-CWb6PxlqkclCA,6313
54
+ dp/agent/client/mcp_client.py,sha256=glbQa-fv2aOBhv_GC2ldwyWHOVcSIEwtVLAjzLZvp0c,6289
55
55
  dp/agent/cloud/__init__.py,sha256=e16ymCZX2f-S8DyGB5jSK8gnQqVObRIsvtLXLALIKxQ,441
56
56
  dp/agent/cloud/main.py,sha256=5QIEjpZ1RxWnR8wyLf-vlgz1bn9oOnxCYn158LBaLN4,727
57
57
  dp/agent/cloud/mcp.py,sha256=tsAwC3doVMLYr6Oh8PxVqF-qCygYkDZJTIhoF_h8eGQ,4537
@@ -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=hsTxuguyqgq_4HSeJYS7VnW-jTvqf9DI0mpvRj0wK3w,13566
65
+ dp/agent/server/calculation_mcp_server.py,sha256=dMIFUQttO7yZyivcuf4BWEZra0Fkh7M7fB2b8tpFix0,17203
66
66
  dp/agent/server/preprocessor.py,sha256=XUWu7QOwo_sIDMYS2b1OTrM33EXEVH_73vk-ju1Ok8A,1264
67
- dp/agent/server/utils.py,sha256=ui3lca9EagcGqmYf8BKLsPARIzXxJ3jgN98yuEO3OSQ,1668
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=-P1HAuHDbfLi-3mkT29I84x2aMvmXiM2h-qfZGEobjI,10872
70
+ dp/agent/server/executor/dispatcher_executor.py,sha256=CZRxbVkLaDvStXhNaMKrKcx2Z0tPPVzIxkU1ufqWgYc,12081
71
71
  dp/agent/server/executor/local_executor.py,sha256=pGXlDOrfjfP40hSMzbF-Wls3OnOTsH8PdkQjcDjP6_w,6580
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.110.dist-info/METADATA,sha256=zqodSHxsxgJ5AYkjncOWOL4qA5hWhiY8Sx1AvPOfMO0,11070
79
- bohr_agent_sdk-0.1.110.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
- bohr_agent_sdk-0.1.110.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
81
- bohr_agent_sdk-0.1.110.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
82
- bohr_agent_sdk-0.1.110.dist-info/RECORD,,
78
+ bohr_agent_sdk-0.1.112.dist-info/METADATA,sha256=qnbHAl8Ey0kVMpxD_3Q6qWhOZCC0uocIZXwUQZk11_s,11070
79
+ bohr_agent_sdk-0.1.112.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
+ bohr_agent_sdk-0.1.112.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
81
+ bohr_agent_sdk-0.1.112.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
82
+ bohr_agent_sdk-0.1.112.dist-info/RECORD,,
@@ -125,7 +125,7 @@ class MCPClient:
125
125
 
126
126
  executor = arguments.get("executor")
127
127
  storage = arguments.get("storage")
128
- res = await self.session.call_tool("submit_" + tool_name, arguments)
128
+ res = await self.call_tool("submit_" + tool_name, arguments)
129
129
  if res.isError:
130
130
  logger.error("Failed to submit %s: %s" % (
131
131
  tool_name, res.content[0].text))
@@ -137,7 +137,7 @@ class MCPClient:
137
137
  logger.info(job_info["extra_info"])
138
138
 
139
139
  while True:
140
- res = await self.session.call_tool("query_job_status", {
140
+ res = await self.call_tool("query_job_status", {
141
141
  "job_id": job_id, "executor": executor})
142
142
  if res.isError:
143
143
  logger.error(res.content[0].text)
@@ -148,7 +148,7 @@ class MCPClient:
148
148
  break
149
149
  await asyncio.sleep(self.query_interval)
150
150
 
151
- res = await self.session.call_tool("get_job_results", {
151
+ res = await self.call_tool("get_job_results", {
152
152
  "job_id": job_id, "executor": executor, "storage": storage})
153
153
  if res.isError:
154
154
  logger.error("Job %s failed: %s" % (job_id, res.content[0].text))
@@ -7,17 +7,17 @@ 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 Any, Literal, Optional, TypedDict
10
+ from typing import Annotated, Literal, Optional, List, Dict
11
11
 
12
- import mcp
13
12
  from mcp.server.fastmcp import FastMCP
14
13
  from mcp.server.fastmcp.utilities.context_injection import (
15
14
  find_context_parameter,
16
15
  )
17
16
  from mcp.server.fastmcp.utilities.func_metadata import (
18
- _get_typed_signature,
17
+ ArgModelBase,
19
18
  func_metadata,
20
19
  )
20
+ from pydantic import BaseModel, Field, create_model
21
21
  from starlette.responses import JSONResponse
22
22
  from starlette.routing import Route
23
23
 
@@ -65,18 +65,9 @@ def set_directory(workdir: str):
65
65
  os.chdir(cwd)
66
66
 
67
67
 
68
- def load_executor(executor):
69
- if not executor and os.path.exists("executor.json"):
70
- with open("executor.json", "r") as f:
71
- executor = json.load(f)
72
- return executor
73
-
74
-
75
- def load_storage(storage):
76
- if not storage and os.path.exists("storage.json"):
77
- with open("storage.json", "r") as f:
78
- storage = json.load(f)
79
- return storage
68
+ def load_job_info():
69
+ with open("job.json", "r") as f:
70
+ return json.load(f)
80
71
 
81
72
 
82
73
  def query_job_status(job_id: str, executor: Optional[dict] = None
@@ -90,7 +81,7 @@ def query_job_status(job_id: str, executor: Optional[dict] = None
90
81
  """
91
82
  trace_id, exec_id = job_id.split("/")
92
83
  with set_directory(trace_id):
93
- executor = load_executor(executor)
84
+ executor = load_job_info()["executor"] or executor
94
85
  _, executor = init_executor(executor)
95
86
  status = executor.query_status(exec_id)
96
87
  logger.info("Job %s status is %s" % (job_id, status))
@@ -105,7 +96,7 @@ def terminate_job(job_id: str, executor: Optional[dict] = None):
105
96
  """
106
97
  trace_id, exec_id = job_id.split("/")
107
98
  with set_directory(trace_id):
108
- executor = load_executor(executor)
99
+ executor = load_job_info()["executor"] or executor
109
100
  _, executor = init_executor(executor)
110
101
  executor.terminate(exec_id)
111
102
  logger.info("Job %s is terminated" % job_id)
@@ -133,6 +124,69 @@ def handle_input_artifacts(fn, kwargs, storage):
133
124
  "storage_type": scheme,
134
125
  "uri": uri,
135
126
  }
127
+ elif param.annotation is List[Path] or (
128
+ param.annotation is Optional[List[Path]] and
129
+ kwargs.get(name) is not None):
130
+ uris = kwargs[name]
131
+ new_paths = []
132
+ for uri in uris:
133
+ scheme, key = parse_uri(uri)
134
+ if scheme == storage_type:
135
+ s = storage
136
+ else:
137
+ s = storage_dict[scheme]()
138
+ path = s.download(key, "inputs/%s" % name)
139
+ new_paths.append(Path(path))
140
+ logger.info("Artifact %s downloaded to %s" % (
141
+ uri, path))
142
+ kwargs[name] = new_paths
143
+ input_artifacts[name] = {
144
+ "storage_type": storage_type,
145
+ "uri": uris,
146
+ }
147
+ elif param.annotation is Dict[str, Path] or (
148
+ param.annotation is Optional[Dict[str, Path]] and
149
+ kwargs.get(name) is not None):
150
+ uris_dict = kwargs[name]
151
+ new_paths_dict = {}
152
+ for key_name, uri in uris_dict.items():
153
+ scheme, key = parse_uri(uri)
154
+ if scheme == storage_type:
155
+ s = storage
156
+ else:
157
+ s = storage_dict[scheme]()
158
+ path = s.download(key, f"inputs/{name}/{key_name}")
159
+ new_paths_dict[key_name] = Path(path)
160
+ logger.info("Artifact %s (key=%s) downloaded to %s" % (
161
+ uri, key_name, path))
162
+ kwargs[name] = new_paths_dict
163
+ input_artifacts[name] = {
164
+ "storage_type": storage_type,
165
+ "uri": uris_dict,
166
+ }
167
+ elif param.annotation is Dict[str, List[Path]] or (
168
+ param.annotation is Optional[Dict[str, List[Path]]] and
169
+ kwargs.get(name) is not None):
170
+ uris_dict = kwargs[name]
171
+ new_paths_dict = {}
172
+ for key_name, uris in uris_dict.items():
173
+ new_paths = []
174
+ for uri in uris:
175
+ scheme, key = parse_uri(uri)
176
+ if scheme == storage_type:
177
+ s = storage
178
+ else:
179
+ s = storage_dict[scheme]()
180
+ path = s.download(key, f"inputs/{name}/{key_name}")
181
+ new_paths.append(Path(path))
182
+ logger.info("Artifact %s (key=%s) downloaded to %s" % (
183
+ uri, key_name, path))
184
+ new_paths_dict[key_name] = new_paths
185
+ kwargs[name] = new_paths_dict
186
+ input_artifacts[name] = {
187
+ "storage_type": storage_type,
188
+ "uri": uris_dict,
189
+ }
136
190
  return kwargs, input_artifacts
137
191
 
138
192
 
@@ -152,13 +206,28 @@ def handle_output_artifacts(results, exec_id, storage):
152
206
  "storage_type": storage_type,
153
207
  "uri": uri,
154
208
  }
209
+ elif isinstance(results[name], list) and all(
210
+ isinstance(item, Path) for item in results[name]):
211
+ new_uris = []
212
+ for item in results[name]:
213
+ key = storage.upload("%s/outputs/%s" % (exec_id, name),
214
+ item)
215
+ uri = storage_type + "://" + key
216
+ logger.info("Artifact %s uploaded to %s" % (
217
+ item, uri))
218
+ new_uris.append(uri)
219
+ results[name] = new_uris
220
+ output_artifacts[name] = {
221
+ "storage_type": storage_type,
222
+ "uri": new_uris,
223
+ }
155
224
  return results, output_artifacts
156
225
 
157
226
 
158
227
  # MCP does not regard Any as serializable in Python 3.12
159
228
  # use Optional[Any] to work around
160
229
  def get_job_results(job_id: str, executor: Optional[dict] = None,
161
- storage: Optional[dict] = None) -> Optional[Any]:
230
+ storage: Optional[dict] = None):
162
231
  """
163
232
  Get results of a calculation job
164
233
  Args:
@@ -168,8 +237,9 @@ def get_job_results(job_id: str, executor: Optional[dict] = None,
168
237
  """
169
238
  trace_id, exec_id = job_id.split("/")
170
239
  with set_directory(trace_id):
171
- executor = load_executor(executor)
172
- storage = load_storage(storage)
240
+ job_info = load_job_info()
241
+ executor = job_info["executor"] or executor
242
+ storage = job_info["storage"] or storage
173
243
  _, executor = init_executor(executor)
174
244
  results = executor.get_results(exec_id)
175
245
  results, output_artifacts = handle_output_artifacts(
@@ -177,7 +247,24 @@ def get_job_results(job_id: str, executor: Optional[dict] = None,
177
247
  logger.info("Job %s result is %s" % (job_id, results))
178
248
  return JobResult(result=results, job_info={
179
249
  "output_artifacts": output_artifacts,
180
- })
250
+ }, tool_name=job_info["tool_name"])
251
+
252
+
253
+ annotation_map = {
254
+ Path: str,
255
+ Optional[Path]: Optional[str],
256
+ List[Path]: List[str],
257
+ Optional[List[Path]]: Optional[List[str]],
258
+ Dict[str, Path]: Dict[str, str],
259
+ Optional[Dict[str, Path]]: Optional[Dict[str, str]],
260
+ Dict[str, List[Path]]: Dict[str, List[str]],
261
+ Optional[Dict[str, List[Path]]]: Optional[Dict[str, List[str]]],
262
+ }
263
+
264
+
265
+ class SubmitResult(BaseModel):
266
+ job_id: str
267
+ extra_info: dict | None = None
181
268
 
182
269
 
183
270
  class CalculationMCPServer:
@@ -191,47 +278,47 @@ class CalculationMCPServer:
191
278
  self.preprocess_func = preprocess_func
192
279
  self.fastmcp_mode = fastmcp_mode
193
280
  self.mcp = FastMCP(*args, **kwargs)
281
+ self.fn_metadata_map = {}
194
282
 
195
283
  def add_patched_tool(self, fn, new_fn, name, is_async=False, doc=None,
196
284
  override_return_annotation=False):
197
285
  """patch the metadata of the tool"""
198
286
  context_kwarg = find_context_parameter(fn)
199
-
200
- def _get_typed_signature_patched(call):
201
- """patch parameters"""
202
- typed_signature = _get_typed_signature(call)
203
- new_typed_signature = _get_typed_signature(new_fn)
204
- parameters = []
205
- for param in typed_signature.parameters.values():
206
- if param.annotation is Path:
207
- parameters.append(inspect.Parameter(
208
- name=param.name, default=param.default,
209
- annotation=str, kind=param.kind))
210
- elif param.annotation is Optional[Path]:
211
- parameters.append(inspect.Parameter(
212
- name=param.name, default=param.default,
213
- annotation=Optional[str], kind=param.kind))
214
- else:
215
- parameters.append(param)
216
- for param in new_typed_signature.parameters.values():
217
- if param.name != "kwargs":
218
- parameters.append(param)
219
- return inspect.Signature(
220
- parameters,
221
- return_annotation=(new_typed_signature.return_annotation
222
- if override_return_annotation
223
- else typed_signature.return_annotation))
224
-
225
- # Due to the frequent changes of MCP, we use a patching style here
226
- mcp.server.fastmcp.utilities.func_metadata._get_typed_signature = \
227
- _get_typed_signature_patched
228
287
  func_arg_metadata = func_metadata(
229
288
  fn,
230
289
  skip_names=[context_kwarg] if context_kwarg is not None else [],
231
- structured_output=None,
232
290
  )
233
- mcp.server.fastmcp.utilities.func_metadata._get_typed_signature = \
234
- _get_typed_signature
291
+ self.fn_metadata_map[name] = func_arg_metadata
292
+ model_params = {}
293
+ params = inspect.signature(fn, eval_str=True).parameters
294
+ for n, annotation in \
295
+ func_arg_metadata.arg_model.__annotations__.items():
296
+ param = params[n]
297
+ if param.annotation in annotation_map:
298
+ model_params[n] = Annotated[
299
+ (annotation_map[param.annotation], Field())]
300
+ else:
301
+ model_params[n] = annotation
302
+ if param.default is not inspect.Parameter.empty:
303
+ model_params[n] = (model_params[n], param.default)
304
+ for n, param in inspect.signature(new_fn).parameters.items():
305
+ if n == "kwargs":
306
+ continue
307
+ model_params[n] = Annotated[(param.annotation, Field())]
308
+ if param.default is not inspect.Parameter.empty:
309
+ model_params[n] = (model_params[n], param.default)
310
+
311
+ func_arg_metadata.arg_model = create_model(
312
+ f"{fn.__name__}Arguments",
313
+ __base__=ArgModelBase,
314
+ **model_params,
315
+ )
316
+ if override_return_annotation:
317
+ new_func_arg_metadata = func_metadata(new_fn)
318
+ func_arg_metadata.output_model = new_func_arg_metadata.output_model
319
+ func_arg_metadata.output_schema = \
320
+ new_func_arg_metadata.output_schema
321
+ func_arg_metadata.wrap_output = new_func_arg_metadata.wrap_output
235
322
  if self.fastmcp_mode and func_arg_metadata.wrap_output:
236
323
  # Only simulate behavior of fastmcp for output_schema
237
324
  func_arg_metadata.output_schema["x-fastmcp-wrap-result"] = True
@@ -240,16 +327,18 @@ class CalculationMCPServer:
240
327
  tool = Tool(
241
328
  fn=new_fn,
242
329
  name=name,
243
- description=doc or fn.__doc__,
330
+ description=doc or fn.__doc__ or "",
244
331
  parameters=parameters,
245
332
  fn_metadata=func_arg_metadata,
246
333
  is_async=is_async,
247
334
  context_kwarg=context_kwarg,
335
+ fn_metadata_map=self.fn_metadata_map,
248
336
  )
249
337
  self.mcp._tool_manager._tools[name] = tool
250
338
 
251
339
  def add_tool(self, fn, *args, **kwargs):
252
- tool = Tool.from_function(fn, *args, **kwargs)
340
+ tool = Tool.from_function(
341
+ fn, *args, fn_metadata_map=self.fn_metadata_map, **kwargs)
253
342
  self.mcp._tool_manager._tools[tool.name] = tool
254
343
  return tool
255
344
 
@@ -260,20 +349,20 @@ class CalculationMCPServer:
260
349
  def decorator(fn: Callable) -> Callable:
261
350
  def submit_job(executor: Optional[dict] = None,
262
351
  storage: Optional[dict] = None,
263
- **kwargs) -> TypedDict("results", {
264
- "job_id": str, "extra_info": Optional[dict]}):
352
+ **kwargs) -> SubmitResult:
265
353
  trace_id = datetime.today().strftime('%Y-%m-%d-%H:%M:%S.%f')
266
354
  logger.info("Job processing (Trace ID: %s)" % trace_id)
267
355
  with set_directory(trace_id):
268
356
  if preprocess_func is not None:
269
357
  executor, storage, kwargs = preprocess_func(
270
358
  executor, storage, kwargs)
271
- if executor:
272
- with open("executor.json", "w") as f:
273
- json.dump(executor, f, indent=4)
274
- if storage:
275
- with open("storage.json", "w") as f:
276
- json.dump(storage, f, indent=4)
359
+ job = {
360
+ "tool_name": fn.__name__,
361
+ "executor": executor,
362
+ "storage": storage,
363
+ }
364
+ with open("job.json", "w") as f:
365
+ json.dump(job, f, indent=4)
277
366
  kwargs, input_artifacts = handle_input_artifacts(
278
367
  fn, kwargs, storage)
279
368
  executor_type, executor = init_executor(executor)
@@ -281,10 +370,10 @@ class CalculationMCPServer:
281
370
  exec_id = res["job_id"]
282
371
  job_id = "%s/%s" % (trace_id, exec_id)
283
372
  logger.info("Job submitted (ID: %s)" % job_id)
284
- result = {
285
- "job_id": job_id,
286
- "extra_info": res.get("extra_info"),
287
- }
373
+ result = SubmitResult(
374
+ job_id=job_id,
375
+ extra_info=res.get("extra_info"),
376
+ )
288
377
  return JobResult(result=result, job_info={
289
378
  "trace_id": trace_id,
290
379
  "executor_type": executor_type,
@@ -135,7 +135,7 @@ class DispatcherExecutor(BaseExecutor):
135
135
  func_def_script, packages = get_func_def_script(fn)
136
136
  self.python_packages.extend(packages)
137
137
 
138
- script += "import asyncio, jsonpickle, os\n"
138
+ script += "import asyncio, jsonpickle, os, shutil\n"
139
139
  script += "from pathlib import Path\n\n"
140
140
  script += "if __name__ == \"__main__\":\n"
141
141
  script += " cwd = os.getcwd()\n"
@@ -149,11 +149,24 @@ class DispatcherExecutor(BaseExecutor):
149
149
  script += " results = asyncio.run(%s(**kwargs))\n" % fn_name
150
150
  else:
151
151
  script += " results = %s(**kwargs)\n" % fn_name
152
+ script += " result_dir = None\n"
153
+ script += " import uuid\n"
152
154
  script += " if isinstance(results, dict):\n"
153
155
  script += " for name in results:\n"
154
156
  script += " if isinstance(results[name], Path):\n"
155
- script += " results[name] = " \
156
- "results[name].absolute().relative_to(cwd)\n"
157
+ script += " if not results[name].absolute().is_relative_to(cwd):\n"
158
+ script += " if result_dir is None:\n"
159
+ script += " result_dir = Path('result_files_dir_' + str(uuid.uuid4()))\n"
160
+ script += " result_dir.mkdir(parents=True, exist_ok=True)\n"
161
+ script += " dest_path = result_dir / results[name].absolute().relative_to('/')\n"
162
+ script += " dest_path.parent.mkdir(parents=True, exist_ok=True)\n"
163
+ script += " if results[name].is_file():\n"
164
+ script += " shutil.copy2(results[name], dest_path)\n"
165
+ script += " elif results[name].is_dir():\n"
166
+ script += " shutil.copytree(results[name], dest_path, dirs_exist_ok=True)\n"
167
+ script += " results[name] = dest_path.absolute().relative_to(cwd)\n"
168
+ script += " else:\n"
169
+ script += " results[name] = results[name].absolute().relative_to(cwd)\n"
157
170
  script += " except Exception as e:\n"
158
171
  script += " os.chdir(cwd)\n"
159
172
  script += " with open('err', 'w') as f:\n"
dp/agent/server/utils.py CHANGED
@@ -21,6 +21,7 @@ def get_logger(name, level="INFO",
21
21
  class JobResult(BaseModel):
22
22
  result: Any
23
23
  job_info: dict
24
+ tool_name: str | None = None
24
25
 
25
26
 
26
27
  class Tool(mcp.server.fastmcp.tools.Tool):
@@ -28,13 +29,23 @@ class Tool(mcp.server.fastmcp.tools.Tool):
28
29
  Workaround MCP server cannot print traceback
29
30
  Add job info to first unstructured content
30
31
  """
32
+ fn_metadata_map: dict | None = None
33
+
34
+ @classmethod
35
+ def from_function(cls, *args, fn_metadata_map=None, **kwargs):
36
+ tool = super().from_function(*args, **kwargs)
37
+ tool.fn_metadata_map = fn_metadata_map
38
+ return tool
39
+
31
40
  async def run(self, *args, **kwargs):
32
41
  try:
33
42
  kwargs["convert_result"] = False
34
43
  result = await super().run(*args, **kwargs)
35
44
  if isinstance(result, JobResult):
36
45
  job_info = result.job_info
37
- result = self.fn_metadata.convert_result(result.result)
46
+ fn_metadata = self.fn_metadata_map.get(
47
+ result.tool_name, self.fn_metadata)
48
+ result = fn_metadata.convert_result(result.result)
38
49
  if isinstance(result, tuple) and len(result) == 2:
39
50
  unstructured_content, _ = result
40
51
  else: