bohr-agent-sdk 0.1.106__py3-none-any.whl → 0.1.109__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.106
3
+ Version: 0.1.109
4
4
  Summary: SDK for scientific agents
5
5
  Home-page: https://github.com/dptech-corp/bohr-agent-sdk/
6
6
  Author: DP Technology
@@ -259,6 +259,33 @@ BOHRIUM_PASSWORD=your_password
259
259
 
260
260
  Note: The `dp-agent fetch config` command automatically downloads configuration files and replaces dynamic variables (such as MQTT_DEVICE_ID). For security reasons, this feature is only available in internal network environments.
261
261
 
262
+ ## 🔒 Authentication Configuration
263
+
264
+ For private deployments or development environment debugging, you need to configure the following environment variables:
265
+
266
+ - `BOHR_ACCESS_KEY`: Requires a real Access Key obtained from [Bohrium User Settings](https://www.bohrium.com/settings/user)
267
+ - `BOHR_APP_KEY`: Can be set to any value for development
268
+
269
+ ### Linux/macOS:
270
+ ```bash
271
+ export BOHR_ACCESS_KEY=your_real_ak_from_bohrium_settings
272
+ export BOHR_APP_KEY=any_value_for_dev
273
+ ```
274
+
275
+ ### Windows (Command Prompt):
276
+ ```cmd
277
+ set BOHR_ACCESS_KEY=your_real_ak_from_bohrium_settings
278
+ set BOHR_APP_KEY=any_value_for_dev
279
+ ```
280
+
281
+ ### Windows (PowerShell):
282
+ ```powershell
283
+ $env:BOHR_ACCESS_KEY="your_real_ak_from_bohrium_settings"
284
+ $env:BOHR_APP_KEY="any_value_for_dev"
285
+ ```
286
+
287
+ For agents deployed on Bohrium APP, authentication parameters will be automatically obtained from cookies.
288
+
262
289
  ## 🎯 Application Scenarios
263
290
 
264
291
  - **Materials Science Computing**: Molecular dynamics simulation, first-principles calculations
@@ -22,13 +22,13 @@ dp/agent/cli/templates/ui/api/__init__.py,sha256=eq1djuBJYvWlGTgxXvT9A386Eu21FNC
22
22
  dp/agent/cli/templates/ui/api/config.py,sha256=ZhHApcXDrmO1JjI-Ok09ETYB-BVis390ZrMRt140DhQ,1018
23
23
  dp/agent/cli/templates/ui/api/constants.py,sha256=mHY81zf4ksfKEOsSiVsWD_A_KcQ6wveLvAImCay_kVM,1391
24
24
  dp/agent/cli/templates/ui/api/debug.py,sha256=URC1k429s84Ue7ElcK7jFh73QeK04IlRyXEjVgLXnbY,8490
25
- dp/agent/cli/templates/ui/api/files.py,sha256=DDqU8-u5yQmHz2sd9vuEURpdgTkQVlBjAyv7dhWz-mA,16469
25
+ dp/agent/cli/templates/ui/api/files.py,sha256=9jQNUOdCVeCCd0PfB95NqKXQX_vHzcHIIZBkAMEsVwo,16026
26
26
  dp/agent/cli/templates/ui/api/files_upload.py,sha256=SOPy7GdQQG4grj5DvuJt3p1_HHJYdk6EuehqtKMAcqs,4411
27
27
  dp/agent/cli/templates/ui/api/files_user.py,sha256=mEFCpZChHzxNUxYUM7A3VqyMbgM3cR0M9FdT93wXJR8,1718
28
28
  dp/agent/cli/templates/ui/api/messages.py,sha256=umIkjfVA5rlXIonWQP7JRLqpfjDdvOc046gBFidMk5A,4992
29
29
  dp/agent/cli/templates/ui/api/projects.py,sha256=KOMRAPrfsbBKNBPotKHEFIKyBypQpox4lSO3ocnjA_M,5008
30
30
  dp/agent/cli/templates/ui/api/sessions.py,sha256=Mi_Sq4mfW163oZ2K0lRFiSgE7MpvVArmmSItDL2fBFo,2994
31
- dp/agent/cli/templates/ui/api/utils.py,sha256=6oTf1P105GEGcwDct-TB3v2pt2iQezTmywfniwrlOqM,4728
31
+ dp/agent/cli/templates/ui/api/utils.py,sha256=FLbFJpBQ5O9F4BoLUWv5PvGQG8IAOMWI6QM7albVKTo,4825
32
32
  dp/agent/cli/templates/ui/api/websocket.py,sha256=kPc9ZArxHSpvEZHl8So5vAH7rWaKNcfTk2O5_61wYUg,9013
33
33
  dp/agent/cli/templates/ui/config/__init__.py,sha256=F7JR_fnXNnZDHeqRCdEiWtVMy03_qFr8cOilIQ6XLUk,14
34
34
  dp/agent/cli/templates/ui/config/agent_config.py,sha256=WFpukdBuZ0t-QQKnhE7NQ6tYPl98FP6fmweRpNbOZQM,11255
@@ -43,13 +43,15 @@ dp/agent/cli/templates/ui/frontend/ui-static/assets/index-DfN2raU9.css,sha256=Ks
43
43
  dp/agent/cli/templates/ui/scripts/build_ui.py,sha256=zoK6agl1dt-InaYQyXSABxjtJj24gtCbm0KKVv0RTSA,1661
44
44
  dp/agent/cli/templates/ui/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  dp/agent/cli/templates/ui/server/app.py,sha256=ZX7ZA_qJyA-Ur7BODICzSn_S4FwGnRWcQP_qq2MZJhw,3803
46
- dp/agent/cli/templates/ui/server/connection.py,sha256=huet_WWxGjo_J9ozj-G8tEYSiEYTsgxOTgjEEJ6CubU,6457
46
+ dp/agent/cli/templates/ui/server/connection.py,sha256=DVlhx49gMjKVPxOpi7spAZiHZA5illXkyC3MhczXsQk,6614
47
47
  dp/agent/cli/templates/ui/server/file_watcher.py,sha256=t0e7IMgf4E1y0CxeboCqsVeZPBo1CGSw_qoO8wG6Wy4,2923
48
48
  dp/agent/cli/templates/ui/server/middleware.py,sha256=bNR5aFqKjxv2einQ0e26aq6tiUW4SMses3IHkrmHDCk,1474
49
49
  dp/agent/cli/templates/ui/server/models.py,sha256=obfMkFFB9qZZXDdwoC25u3tBlUC6Rz7sjeyuh7a_UWk,1680
50
50
  dp/agent/cli/templates/ui/server/session_manager.py,sha256=ZbNHGCFvswa-LKWn6c6RMHWDdfeOvY5L6A8CfrsF0OE,46536
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
+ dp/agent/client/__init__.py,sha256=yu7HYZwAkD7g5dL9JttLkGmspBcyOf-6OoCjci4oPDA,59
54
+ dp/agent/client/mcp_client.py,sha256=xp8GSM5effjjp34yGp5-HwldalyqT-CWb6PxlqkclCA,6313
53
55
  dp/agent/cloud/__init__.py,sha256=e16ymCZX2f-S8DyGB5jSK8gnQqVObRIsvtLXLALIKxQ,441
54
56
  dp/agent/cloud/main.py,sha256=5QIEjpZ1RxWnR8wyLf-vlgz1bn9oOnxCYn158LBaLN4,727
55
57
  dp/agent/cloud/mcp.py,sha256=tsAwC3doVMLYr6Oh8PxVqF-qCygYkDZJTIhoF_h8eGQ,4537
@@ -60,7 +62,7 @@ dp/agent/device/device/__init__.py,sha256=w7_1S16S1vWUq0RGl0GFgjq2vFkc5oNvy8cQTn
60
62
  dp/agent/device/device/device.py,sha256=9ZRIJth-4qMO-i-u_b_cO3d6a4eTbTQjPaxFsV_zEkc,9643
61
63
  dp/agent/device/device/types.py,sha256=JuxB-hjf1CjjvfBxCLwRAXVFlYS-nPEdiJpBWLFVCzo,1924
62
64
  dp/agent/server/__init__.py,sha256=rckaYd8pbYyB4ENEhgjXKeGMXjdnrgcJpdM1gu5u1Wc,508
63
- dp/agent/server/calculation_mcp_server.py,sha256=iRFOdgTxySMGk7ZaSseNssEp-A7zT5cW1Ym2_MIKnG4,12602
65
+ dp/agent/server/calculation_mcp_server.py,sha256=hsTxuguyqgq_4HSeJYS7VnW-jTvqf9DI0mpvRj0wK3w,13566
64
66
  dp/agent/server/preprocessor.py,sha256=XUWu7QOwo_sIDMYS2b1OTrM33EXEVH_73vk-ju1Ok8A,1264
65
67
  dp/agent/server/utils.py,sha256=ui3lca9EagcGqmYf8BKLsPARIzXxJ3jgN98yuEO3OSQ,1668
66
68
  dp/agent/server/executor/__init__.py,sha256=s95M5qKQk39Yi9qaVJZhk_nfj54quSf7EDghR3OCFUA,248
@@ -73,8 +75,8 @@ dp/agent/server/storage/bohrium_storage.py,sha256=EsKX4dWWvZTn2TEhZv4zsvihfDK0mm
73
75
  dp/agent/server/storage/http_storage.py,sha256=KiySq7g9-iJr12XQCKKyJLn8wJoDnSRpQAR5_qPJ1ZU,1471
74
76
  dp/agent/server/storage/local_storage.py,sha256=t1wfjByjXew9ws3PuUxWxmZQ0-Wt1a6t4wmj3fW62GI,1352
75
77
  dp/agent/server/storage/oss_storage.py,sha256=pgjmi7Gir3Y5wkMDCvU4fvSls15fXT7Ax-h9MYHFPK0,3359
76
- bohr_agent_sdk-0.1.106.dist-info/METADATA,sha256=gAYz_SBcnkqaV314ppLvyi9O8dekysp24LlvNhUGfdU,10219
77
- bohr_agent_sdk-0.1.106.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
- bohr_agent_sdk-0.1.106.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
79
- bohr_agent_sdk-0.1.106.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
80
- bohr_agent_sdk-0.1.106.dist-info/RECORD,,
78
+ bohr_agent_sdk-0.1.109.dist-info/METADATA,sha256=B9SUVukVXm_P3E8ZWP41ApkenlCwobucnf9DSN_F2Ds,11070
79
+ bohr_agent_sdk-0.1.109.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
+ bohr_agent_sdk-0.1.109.dist-info/entry_points.txt,sha256=5n5kneF5IbDQtoQ2WfF-QuBjDtsimJte9Rv9baSGgc0,86
81
+ bohr_agent_sdk-0.1.109.dist-info/top_level.txt,sha256=87xLUDhu_1nQHoGLwlhJ6XlO7OsjILh6i1nX6ljFzDo,3
82
+ bohr_agent_sdk-0.1.109.dist-info/RECORD,,
@@ -36,22 +36,9 @@ def get_user_identifier(access_key: str = None, app_key: str = None, session_id:
36
36
  if cached_user_id:
37
37
  return cached_user_id
38
38
 
39
- # If no cache, call OpenSDK
40
- if access_key and app_key:
41
- try:
42
- # Use OpenSDK to get user info
43
- client = OpenSDK(
44
- access_key=access_key,
45
- app_key=app_key
46
- )
47
- user_info = client.user.get_info()
48
- if user_info and user_info.get('code') == 0:
49
- data = user_info.get('data', {})
50
- bohrium_user_id = data.get('user_id')
51
- if bohrium_user_id:
52
- return bohrium_user_id
53
- except Exception as e:
54
- pass
39
+ # If no cache, return access_key directly (consistent with WebSocket logic)
40
+ if access_key:
41
+ return access_key
55
42
 
56
43
  # If has session_id, use it
57
44
  if session_id:
@@ -141,21 +141,22 @@ def safe_filename(filename: str) -> str:
141
141
  async def check_project_id_required(context_manager, user_identifier: str) -> bool:
142
142
  """
143
143
  Check if project_id is required and set
144
-
144
+
145
145
  Args:
146
146
  context_manager: WebSocket manager instance
147
- user_identifier: User identifier
148
-
147
+ user_identifier: User identifier (can be access_key or bohrium_user_id)
148
+
149
149
  Returns:
150
150
  True if project_id is set, False otherwise
151
151
  """
152
152
  # First check environment variable
153
153
  if os.environ.get('BOHR_PROJECT_ID'):
154
154
  return True
155
-
155
+
156
156
  # Check user's connection context
157
157
  for context in context_manager.active_connections.values():
158
- if context.get_user_identifier() == user_identifier:
158
+ # Match by bohrium_user_id or access_key
159
+ if context.get_user_identifier() == user_identifier or context.access_key == user_identifier:
159
160
  return bool(context.project_id)
160
-
161
+
161
162
  return False
@@ -86,21 +86,25 @@ class ConnectionContext:
86
86
  def get_user_identifier(self) -> str:
87
87
  """
88
88
  Get unique user identifier
89
- Prefer bohrium_user_id, fallback to generated user_id
90
-
89
+ Use access_key if available (for registered users), otherwise use generated user_id
90
+
91
91
  Returns:
92
92
  User identifier
93
93
  """
94
- return self.bohrium_user_id if self.bohrium_user_id else self.user_id
94
+ # 优先使用 access_key,保持与 HTTP API 的一致性
95
+ if self.access_key:
96
+ return self.access_key
97
+ # 临时用户使用生成的 user_id
98
+ return self.user_id
95
99
 
96
100
  def is_registered_user(self) -> bool:
97
101
  """
98
102
  Check if user is registered
99
-
103
+
100
104
  Returns:
101
- True if has bohrium_user_id
105
+ True if has access_key (registered Bohrium user)
102
106
  """
103
- return bool(self.bohrium_user_id)
107
+ return bool(self.access_key)
104
108
 
105
109
  def set_project_id(self, project_id: int):
106
110
  """
@@ -0,0 +1,3 @@
1
+ from .mcp_client import MCPClient
2
+
3
+ __all__ = ["MCPClient"]
@@ -0,0 +1,162 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ from contextlib import AsyncExitStack
5
+
6
+ from mcp import ClientSession, StdioServerParameters
7
+ from mcp.client.stdio import stdio_client
8
+ from mcp.client.sse import sse_client
9
+ from mcp.client.streamable_http import streamablehttp_client
10
+
11
+ from ..server.utils import get_logger
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class MCPClient:
16
+ def __init__(self, server, query_interval=4):
17
+ self.server = server
18
+ self.session = None
19
+ self.exit_stack = AsyncExitStack()
20
+ self.query_interval = query_interval
21
+
22
+ def _is_session_disconnected(self, session: ClientSession) -> bool:
23
+ """Checks if a session is disconnected or closed.
24
+
25
+ Args:
26
+ session: The ClientSession to check.
27
+
28
+ Returns:
29
+ True if the session is disconnected, False otherwise.
30
+ """
31
+ return session._read_stream._closed or session._write_stream._closed
32
+
33
+ async def get_session(self):
34
+ if self.session is not None:
35
+ # Check if the existing session is still connected
36
+ if not self._is_session_disconnected(self.session):
37
+ # Session is still good, return it
38
+ return self.session
39
+ else:
40
+ # Session is disconnected, clean it up
41
+ logger.info(f'Cleaning up disconnected session: {self.server}')
42
+ try:
43
+ await self.exit_stack.aclose()
44
+ except Exception as e:
45
+ logger.warning('Error during disconnected session cleanup: %s',
46
+ e)
47
+ finally:
48
+ self.session = None
49
+
50
+ is_python = self.server.endswith('.py')
51
+ is_js = self.server.endswith('.js')
52
+ is_sse = self.server.startswith('http') and "/sse" in self.server
53
+ is_streamablehttp = self.server.startswith('http') \
54
+ and "/sse" not in self.server
55
+ if not (is_python or is_js or is_sse or is_streamablehttp):
56
+ raise ValueError(
57
+ "Server script must be a .py or .js file or a http link")
58
+
59
+ if is_sse:
60
+ try:
61
+ logger.info(f"SSE: {self.server}")
62
+ streams = await self.exit_stack.enter_async_context(sse_client(
63
+ self.server))
64
+ self.session = await self.exit_stack.enter_async_context(
65
+ ClientSession(*streams))
66
+ await self.session.initialize()
67
+ return self.session
68
+ except Exception as e:
69
+ logger.error(
70
+ f"An error occurred while connecting to the server: {e}")
71
+ raise
72
+ if is_streamablehttp:
73
+ try:
74
+ logger.info(f"StreamableHTTP: {self.server}")
75
+ transports = await self.exit_stack.enter_async_context(
76
+ streamablehttp_client(self.server))
77
+ self.session = await self.exit_stack.enter_async_context(
78
+ ClientSession(*transports[:2]))
79
+ await self.session.initialize()
80
+ return self.session
81
+ except Exception as e:
82
+ logger.error(
83
+ f"An error occurred while connecting to the server: {e}")
84
+ raise
85
+
86
+ command = "python" if is_python else "node"
87
+ server_params = StdioServerParameters(
88
+ command=command,
89
+ args=[self.server],
90
+ env=os.environ.copy(),
91
+ )
92
+
93
+ try:
94
+ stdio_transport = await self.exit_stack.enter_async_context(
95
+ stdio_client(server_params))
96
+ stdio, write = stdio_transport
97
+ self.session = await self.exit_stack.enter_async_context(
98
+ ClientSession(stdio, write))
99
+ await self.session.initialize()
100
+ return self.session
101
+ except Exception as e:
102
+ logger.error(
103
+ f"An error occurred while connecting to the server: {e}")
104
+ raise
105
+
106
+ async def connect_to_server(self):
107
+ await self.get_session()
108
+ response = await self.session.list_tools()
109
+ tools = []
110
+ for tool in response.tools:
111
+ if tool.name.startswith("submit_") or tool.name in [
112
+ "query_job_status", "get_job_results", "terminate_job"]:
113
+ continue
114
+ tools.append(tool)
115
+ logger.info(
116
+ f"Connected to server with tools:{[tool.name for tool in tools]}")
117
+ return tools
118
+
119
+ async def call_tool(self, tool_name: str, arguments: dict,
120
+ async_mode=False):
121
+ await self.get_session()
122
+ if not async_mode:
123
+ result = await self.session.call_tool(tool_name, arguments)
124
+ return result
125
+
126
+ executor = arguments.get("executor")
127
+ storage = arguments.get("storage")
128
+ res = await self.session.call_tool("submit_" + tool_name, arguments)
129
+ if res.isError:
130
+ logger.error("Failed to submit %s: %s" % (
131
+ tool_name, res.content[0].text))
132
+ return res
133
+ job_id = json.loads(res.content[0].text)["job_id"]
134
+ job_info = res.content[0].job_info
135
+ logger.info("Job submitted (ID: %s)" % job_id)
136
+ if job_info.get("extra_info"):
137
+ logger.info(job_info["extra_info"])
138
+
139
+ while True:
140
+ res = await self.session.call_tool("query_job_status", {
141
+ "job_id": job_id, "executor": executor})
142
+ if res.isError:
143
+ logger.error(res.content[0].text)
144
+ else:
145
+ status = res.content[0].text
146
+ logger.info("Job %s status is %s" % (job_id, status))
147
+ if status != "Running":
148
+ break
149
+ await asyncio.sleep(self.query_interval)
150
+
151
+ res = await self.session.call_tool("get_job_results", {
152
+ "job_id": job_id, "executor": executor, "storage": storage})
153
+ if res.isError:
154
+ logger.error("Job %s failed: %s" % (job_id, res.content[0].text))
155
+ else:
156
+ logger.info("Job %s result is %s" % (job_id, res.content))
157
+ res.content[0].job_info = {**job_info,
158
+ **getattr(res.content[0], "job_info", {})}
159
+ return res
160
+
161
+ async def cleanup(self):
162
+ await self.exit_stack.aclose()
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import json
2
3
  import os
3
4
  from collections.abc import Callable
4
5
  from contextlib import contextmanager
@@ -64,6 +65,20 @@ def set_directory(workdir: str):
64
65
  os.chdir(cwd)
65
66
 
66
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
80
+
81
+
67
82
  def query_job_status(job_id: str, executor: Optional[dict] = None
68
83
  ) -> Literal["Running", "Succeeded", "Failed"]:
69
84
  """
@@ -75,6 +90,7 @@ def query_job_status(job_id: str, executor: Optional[dict] = None
75
90
  """
76
91
  trace_id, exec_id = job_id.split("/")
77
92
  with set_directory(trace_id):
93
+ executor = load_executor(executor)
78
94
  _, executor = init_executor(executor)
79
95
  status = executor.query_status(exec_id)
80
96
  logger.info("Job %s status is %s" % (job_id, status))
@@ -89,6 +105,7 @@ def terminate_job(job_id: str, executor: Optional[dict] = None):
89
105
  """
90
106
  trace_id, exec_id = job_id.split("/")
91
107
  with set_directory(trace_id):
108
+ executor = load_executor(executor)
92
109
  _, executor = init_executor(executor)
93
110
  executor.terminate(exec_id)
94
111
  logger.info("Job %s is terminated" % job_id)
@@ -138,17 +155,21 @@ def handle_output_artifacts(results, exec_id, storage):
138
155
  return results, output_artifacts
139
156
 
140
157
 
158
+ # MCP does not regard Any as serializable in Python 3.12
159
+ # use Optional[Any] to work around
141
160
  def get_job_results(job_id: str, executor: Optional[dict] = None,
142
- storage: Optional[dict] = None) -> Any:
161
+ storage: Optional[dict] = None) -> Optional[Any]:
143
162
  """
144
163
  Get results of a calculation job
145
164
  Args:
146
165
  job_id (str): The ID of the calculation job
147
166
  Returns:
148
- results (dict): results of the calculation job
167
+ results (Any): results of the calculation job
149
168
  """
150
169
  trace_id, exec_id = job_id.split("/")
151
170
  with set_directory(trace_id):
171
+ executor = load_executor(executor)
172
+ storage = load_storage(storage)
152
173
  _, executor = init_executor(executor)
153
174
  results = executor.get_results(exec_id)
154
175
  results, output_artifacts = handle_output_artifacts(
@@ -247,6 +268,12 @@ class CalculationMCPServer:
247
268
  if preprocess_func is not None:
248
269
  executor, storage, kwargs = preprocess_func(
249
270
  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)
250
277
  kwargs, input_artifacts = handle_input_artifacts(
251
278
  fn, kwargs, storage)
252
279
  executor_type, executor = init_executor(executor)