bohr-agent-sdk 0.1.100__py3-none-any.whl → 0.1.102__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.
Files changed (49) hide show
  1. {bohr_agent_sdk-0.1.100.dist-info → bohr_agent_sdk-0.1.102.dist-info}/METADATA +6 -2
  2. bohr_agent_sdk-0.1.102.dist-info/RECORD +80 -0
  3. dp/agent/adapter/adk/client/calculation_mcp_tool.py +10 -2
  4. dp/agent/cli/cli.py +126 -25
  5. dp/agent/cli/templates/__init__.py +1 -0
  6. dp/agent/cli/templates/calculation/simple.py.template +15 -0
  7. dp/agent/cli/templates/device/tescan_device.py.template +158 -0
  8. dp/agent/cli/templates/main.py.template +67 -0
  9. dp/agent/cli/templates/ui/__init__.py +1 -0
  10. dp/agent/cli/templates/ui/api/__init__.py +1 -0
  11. dp/agent/cli/templates/ui/api/config.py +32 -0
  12. dp/agent/cli/templates/ui/api/constants.py +61 -0
  13. dp/agent/cli/templates/ui/api/debug.py +257 -0
  14. dp/agent/cli/templates/ui/api/files.py +469 -0
  15. dp/agent/cli/templates/ui/api/files_upload.py +115 -0
  16. dp/agent/cli/templates/ui/api/files_user.py +50 -0
  17. dp/agent/cli/templates/ui/api/messages.py +161 -0
  18. dp/agent/cli/templates/ui/api/projects.py +146 -0
  19. dp/agent/cli/templates/ui/api/sessions.py +93 -0
  20. dp/agent/cli/templates/ui/api/utils.py +161 -0
  21. dp/agent/cli/templates/ui/api/websocket.py +184 -0
  22. dp/agent/cli/templates/ui/config/__init__.py +1 -0
  23. dp/agent/cli/templates/ui/config/agent_config.py +257 -0
  24. dp/agent/cli/templates/ui/frontend/index.html +13 -0
  25. dp/agent/cli/templates/ui/frontend/package.json +46 -0
  26. dp/agent/cli/templates/ui/frontend/tsconfig.json +26 -0
  27. dp/agent/cli/templates/ui/frontend/tsconfig.node.json +10 -0
  28. dp/agent/cli/templates/ui/frontend/ui-static/assets/index-DdAmKhul.js +105 -0
  29. dp/agent/cli/templates/ui/frontend/ui-static/assets/index-DfN2raU9.css +1 -0
  30. dp/agent/cli/templates/ui/frontend/ui-static/index.html +14 -0
  31. dp/agent/cli/templates/ui/frontend/vite.config.ts +37 -0
  32. dp/agent/cli/templates/ui/scripts/build_ui.py +56 -0
  33. dp/agent/cli/templates/ui/server/__init__.py +0 -0
  34. dp/agent/cli/templates/ui/server/app.py +98 -0
  35. dp/agent/cli/templates/ui/server/connection.py +210 -0
  36. dp/agent/cli/templates/ui/server/file_watcher.py +85 -0
  37. dp/agent/cli/templates/ui/server/middleware.py +43 -0
  38. dp/agent/cli/templates/ui/server/models.py +53 -0
  39. dp/agent/cli/templates/ui/server/session_manager.py +1158 -0
  40. dp/agent/cli/templates/ui/server/user_files.py +85 -0
  41. dp/agent/cli/templates/ui/server/utils.py +50 -0
  42. dp/agent/cli/templates/ui/test_download.py +98 -0
  43. dp/agent/cli/templates/ui/ui_utils.py +260 -0
  44. dp/agent/cli/templates/ui/websocket-server.py +87 -0
  45. dp/agent/server/storage/http_storage.py +1 -1
  46. bohr_agent_sdk-0.1.100.dist-info/RECORD +0 -40
  47. {bohr_agent_sdk-0.1.100.dist-info → bohr_agent_sdk-0.1.102.dist-info}/WHEEL +0 -0
  48. {bohr_agent_sdk-0.1.100.dist-info → bohr_agent_sdk-0.1.102.dist-info}/entry_points.txt +0 -0
  49. {bohr_agent_sdk-0.1.100.dist-info → bohr_agent_sdk-0.1.102.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,161 @@
1
+ """
2
+ User-facing messages for API modules
3
+ Supports internationalization - Chinese and English messages
4
+ """
5
+
6
+ # Language detection helper
7
+ def get_message(messages_dict, lang='zh'):
8
+ """Get message in specified language, fallback to Chinese"""
9
+ return messages_dict.get(lang, messages_dict.get('zh'))
10
+
11
+ # Error messages
12
+ ERROR_MESSAGES = {
13
+ 'access_denied': {
14
+ 'zh': '访问被拒绝',
15
+ 'en': 'Access denied'
16
+ },
17
+ 'invalid_file_path': {
18
+ 'zh': '无效的文件路径',
19
+ 'en': 'Invalid file path'
20
+ },
21
+ 'file_not_found': {
22
+ 'zh': '文件未找到',
23
+ 'en': 'File not found'
24
+ },
25
+ 'folder_not_found': {
26
+ 'zh': '文件夹未找到',
27
+ 'en': 'Folder not found'
28
+ },
29
+ 'file_or_folder_not_found': {
30
+ 'zh': '文件或文件夹不存在',
31
+ 'en': 'File or folder does not exist'
32
+ },
33
+ 'decode_error': {
34
+ 'zh': '无法解码文件内容',
35
+ 'en': 'Unable to decode file content'
36
+ },
37
+ 'session_not_exist': {
38
+ 'zh': '会话不存在',
39
+ 'en': 'Session does not exist'
40
+ },
41
+ 'delete_session_failed': {
42
+ 'zh': '删除会话失败',
43
+ 'en': 'Failed to delete session'
44
+ },
45
+ 'invalid_project_id': {
46
+ 'zh': '无效的 Project ID: {project_id},必须是整数',
47
+ 'en': 'Invalid Project ID: {project_id}, must be an integer'
48
+ },
49
+ 'project_id_required': {
50
+ 'zh': '请先设置项目 ID 后再上传文件。',
51
+ 'en': 'Please set project ID before uploading files.'
52
+ },
53
+ 'unsupported_file_type': {
54
+ 'zh': '不支持的文件类型: {file_ext}',
55
+ 'en': 'Unsupported file type: {file_ext}'
56
+ },
57
+ 'file_too_large': {
58
+ 'zh': '文件 {filename} 超过大小限制 (10MB)',
59
+ 'en': 'File {filename} exceeds size limit (10MB)'
60
+ },
61
+ 'no_permission_project': {
62
+ 'zh': '您没有权限使用项目 ID: {project_id}。请从项目列表中选择您有权限的项目。',
63
+ 'en': 'You do not have permission to use project ID: {project_id}. Please select a project you have permission for.'
64
+ },
65
+ 'accesskey_not_found': {
66
+ 'zh': '未找到 AccessKey',
67
+ 'en': 'AccessKey not found'
68
+ },
69
+ 'appkey_not_found': {
70
+ 'zh': '未找到 AppKey',
71
+ 'en': 'AppKey not found'
72
+ },
73
+ 'accesskey_or_appkey_not_found': {
74
+ 'zh': '未找到 AccessKey 或 AppKey',
75
+ 'en': 'AccessKey or AppKey not found'
76
+ },
77
+ 'get_project_list_failed': {
78
+ 'zh': '获取项目列表失败',
79
+ 'en': 'Failed to get project list'
80
+ },
81
+ 'project_not_belong_to_user': {
82
+ 'zh': '该项目不属于当前用户',
83
+ 'en': 'This project does not belong to current user'
84
+ },
85
+ 'temp_user_no_session': {
86
+ 'zh': '临时用户没有历史会话',
87
+ 'en': 'Temporary users have no session history'
88
+ },
89
+ 'temp_user_cannot_export': {
90
+ 'zh': '临时用户没有会话可导出',
91
+ 'en': 'Temporary users have no sessions to export'
92
+ },
93
+ 'no_session_found': {
94
+ 'zh': '没有找到会话',
95
+ 'en': 'No sessions found'
96
+ },
97
+ 'clear_failed': {
98
+ 'zh': '清除失败: {error}',
99
+ 'en': 'Clear failed: {error}'
100
+ },
101
+ 'export_failed': {
102
+ 'zh': '导出失败: {error}',
103
+ 'en': 'Export failed: {error}'
104
+ },
105
+ 'no_active_session': {
106
+ 'zh': '没有活动的会话',
107
+ 'en': 'No active session'
108
+ },
109
+ 'session_service_not_initialized': {
110
+ 'zh': '会话服务未初始化',
111
+ 'en': 'Session service not initialized'
112
+ },
113
+ 'get_session_list_failed': {
114
+ 'zh': '获取会话列表失败',
115
+ 'en': 'Failed to get session list'
116
+ },
117
+ 'get_session_messages_failed': {
118
+ 'zh': '获取会话消息失败',
119
+ 'en': 'Failed to get session messages'
120
+ },
121
+ 'please_set_project_id': {
122
+ 'zh': '🔒 请先设置项目 ID',
123
+ 'en': '🔒 Please set project ID first'
124
+ }
125
+ }
126
+
127
+ # Success messages
128
+ SUCCESS_MESSAGES = {
129
+ 'project_id_set': {
130
+ 'zh': 'Project ID 已设置为: {project_id}',
131
+ 'en': 'Project ID set to: {project_id}'
132
+ },
133
+ 'project_id_set_from_env': {
134
+ 'zh': 'Project ID 已从环境变量设置为: {project_id} (开发模式)',
135
+ 'en': 'Project ID set from environment variable: {project_id} (development mode)'
136
+ },
137
+ 'delete_success': {
138
+ 'zh': '成功删除: {filename}',
139
+ 'en': 'Successfully deleted: {filename}'
140
+ },
141
+ 'session_cleared': {
142
+ 'zh': '历史会话已清除',
143
+ 'en': 'Session history cleared'
144
+ },
145
+ 'no_session_to_clear': {
146
+ 'zh': '没有找到历史会话',
147
+ 'en': 'No session history found'
148
+ }
149
+ }
150
+
151
+ # UI labels
152
+ UI_LABELS = {
153
+ 'workspace': {
154
+ 'zh': '工作空间',
155
+ 'en': 'Workspace'
156
+ },
157
+ 'websocket_server_running': {
158
+ 'zh': '{agent_name} WebSocket 服务器正在运行',
159
+ 'en': '{agent_name} WebSocket server is running'
160
+ }
161
+ }
@@ -0,0 +1,146 @@
1
+ # Projects API using bohrium-open-sdk
2
+ from typing import Dict, Any, List, Optional
3
+
4
+ from fastapi import APIRouter, Request
5
+ from bohrium_open_sdk import OpenSDK
6
+
7
+ from server.utils import get_ak_info_from_request
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ def make_client_from_request(request: Request) -> OpenSDK:
13
+ """Parse AccessKey/AppKey from request headers and create SDK client"""
14
+ access_key, app_key = get_ak_info_from_request(request.headers)
15
+ if not access_key:
16
+ raise ValueError("未找到 AccessKey")
17
+ if not app_key:
18
+ raise ValueError("未找到 AppKey")
19
+ return OpenSDK(access_key=access_key, app_key=app_key)
20
+
21
+
22
+ def normalize_project(item: Dict[str, Any]) -> Dict[str, Any]:
23
+ """
24
+ Normalize project structure for frontend:
25
+ - id: item['project_id'] or item['id']
26
+ - name: item['project_name'] or item['name']
27
+ Set default values for missing fields
28
+ """
29
+ pid = item.get("project_id", item.get("id"))
30
+ name = item.get("project_name", item.get("name"))
31
+ return {
32
+ "id": pid,
33
+ "name": name,
34
+ "creatorName": item.get("creatorName", ""),
35
+ "createTime": item.get("createTime", ""),
36
+ "projectRole": item.get("projectRole", 0),
37
+ }
38
+
39
+
40
+ async def verify_user_project(access_key: str, app_key: str, project_id: int) -> bool:
41
+ """
42
+ Use bohrium-open-sdk to verify if project_id belongs to current user
43
+ """
44
+ if not access_key or not app_key or not project_id:
45
+ return False
46
+
47
+ try:
48
+ client = OpenSDK(access_key=access_key, app_key=app_key)
49
+ res = client.user.list_project()
50
+ if res.get("code") != 0:
51
+ return False
52
+
53
+ items: List[Dict[str, Any]] = res.get("data", {}).get("items", []) or []
54
+ user_project_ids = [it.get("project_id", it.get("id")) for it in items]
55
+
56
+ # Compatible with string/integer ID
57
+ user_project_ids_int: List[int] = []
58
+ for pid in user_project_ids:
59
+ try:
60
+ user_project_ids_int.append(int(pid))
61
+ except Exception:
62
+ pass
63
+
64
+ return int(project_id) in user_project_ids_int
65
+
66
+ except Exception as e:
67
+ return False
68
+
69
+
70
+ @router.get("/api/projects")
71
+ async def get_projects(request: Request) -> Dict[str, Any]:
72
+ """Get user's project list using SDK"""
73
+ try:
74
+ client = make_client_from_request(request)
75
+ user_info = client.user.get_info()
76
+ # Response data contains: user_id, name, org_id
77
+ response = f"Get userid success,the user_id is: {user_info['data']['user_id']}"
78
+ print(response)
79
+ res = client.user.list_project()
80
+ if res.get("code") != 0:
81
+ return {
82
+ "success": False,
83
+ "error": res.get("error", "获取项目列表失败"),
84
+ "projects": [],
85
+ }
86
+
87
+ items: List[Dict[str, Any]] = res.get("data", {}).get("items", []) or []
88
+ projects = [normalize_project(it) for it in items]
89
+
90
+ return {"success": True, "projects": projects}
91
+
92
+ except ValueError as ve:
93
+ return {"success": False, "error": str(ve), "projects": []}
94
+ except Exception as e:
95
+ return {"success": False, "error": str(e), "projects": []}
96
+
97
+
98
+ @router.get("/api/projects/{project_id}/verify")
99
+ async def verify_project(project_id: int, request: Request) -> Dict[str, Any]:
100
+ """Verify if specific project_id belongs to current user"""
101
+ try:
102
+ access_key, app_key = get_ak_info_from_request(request.headers)
103
+
104
+ if not access_key or not app_key:
105
+ return {
106
+ "success": False,
107
+ "error": "未找到 AccessKey 或 AppKey",
108
+ "valid": False,
109
+ }
110
+
111
+ is_valid = await verify_user_project(access_key, app_key, project_id)
112
+
113
+ if not is_valid:
114
+ return {
115
+ "success": True,
116
+ "valid": False,
117
+ "error": "该项目不属于当前用户",
118
+ }
119
+
120
+ # Find specific project info and return
121
+ client = OpenSDK(access_key=access_key, app_key=app_key)
122
+ res = client.user.list_project()
123
+ if res.get("code") != 0:
124
+ return {
125
+ "success": False,
126
+ "error": res.get("error", "获取项目列表失败"),
127
+ "valid": False,
128
+ }
129
+
130
+ items: List[Dict[str, Any]] = res.get("data", {}).get("items", []) or []
131
+ project_info: Optional[Dict[str, Any]] = None
132
+ for it in items:
133
+ raw_id = it.get("project_id", it.get("id"))
134
+ try:
135
+ if int(raw_id) == int(project_id):
136
+ project_info = normalize_project(it)
137
+ break
138
+ except Exception:
139
+ continue
140
+
141
+ return {"success": True, "valid": True, "project": project_info or {"id": project_id}}
142
+
143
+ except ValueError as ve:
144
+ return {"success": False, "error": str(ve), "valid": False}
145
+ except Exception as e:
146
+ return {"success": False, "error": str(e), "valid": False}
@@ -0,0 +1,93 @@
1
+ # Session management API
2
+ import json
3
+ import shutil
4
+ from datetime import datetime
5
+ from fastapi import Request
6
+ from fastapi.responses import JSONResponse, Response
7
+
8
+ from server.utils import get_ak_info_from_request
9
+ from api.websocket import manager
10
+
11
+
12
+ async def clear_user_sessions(request: Request):
13
+ """Clear all historical sessions for current user"""
14
+ access_key, _ = get_ak_info_from_request(request.headers)
15
+
16
+ if not access_key:
17
+ return JSONResponse(
18
+ content={"error": "临时用户没有历史会话"},
19
+ status_code=400
20
+ )
21
+
22
+ try:
23
+ # Get user's session directory
24
+ ak_hash = manager.persistent_manager._get_ak_hash(access_key)
25
+ user_sessions_dir = manager.persistent_manager.ak_sessions_dir / ak_hash / "sessions"
26
+
27
+ if user_sessions_dir.exists():
28
+ # Delete all session files
29
+ shutil.rmtree(user_sessions_dir)
30
+ user_sessions_dir.mkdir(parents=True, exist_ok=True)
31
+
32
+ return JSONResponse(content={
33
+ "message": "历史会话已清除",
34
+ "status": "success"
35
+ })
36
+ else:
37
+ return JSONResponse(content={
38
+ "message": "没有找到历史会话",
39
+ "status": "success"
40
+ })
41
+
42
+ except Exception as e:
43
+ return JSONResponse(
44
+ content={"error": f"清除失败: {str(e)}"},
45
+ status_code=500
46
+ )
47
+
48
+
49
+ async def export_user_sessions(request: Request):
50
+ """Export all sessions for current user"""
51
+ access_key, _ = get_ak_info_from_request(request.headers)
52
+
53
+ if not access_key:
54
+ return JSONResponse(
55
+ content={"error": "临时用户没有会话可导出"},
56
+ status_code=400
57
+ )
58
+
59
+ try:
60
+ # Load all user sessions
61
+ sessions = await manager.persistent_manager.load_user_sessions(access_key)
62
+
63
+ if not sessions:
64
+ return JSONResponse(
65
+ content={"error": "没有找到会话"},
66
+ status_code=404
67
+ )
68
+
69
+ # Build export data
70
+ export_data = {
71
+ "export_time": datetime.now().isoformat(),
72
+ "user_type": "registered",
73
+ "sessions": []
74
+ }
75
+
76
+ for session in sessions.values():
77
+ session_data = manager.persistent_manager._serialize_session(session)
78
+ export_data["sessions"].append(session_data)
79
+
80
+ # Return JSON file
81
+ return Response(
82
+ content=json.dumps(export_data, indent=2, ensure_ascii=False),
83
+ media_type="application/json",
84
+ headers={
85
+ "Content-Disposition": f"attachment; filename=sessions_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
86
+ }
87
+ )
88
+
89
+ except Exception as e:
90
+ return JSONResponse(
91
+ content={"error": f"导出失败: {str(e)}"},
92
+ status_code=500
93
+ )
@@ -0,0 +1,161 @@
1
+ """
2
+ Common utilities for API modules
3
+ """
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional, Tuple
7
+ from fastapi import Request
8
+ from fastapi.responses import JSONResponse
9
+
10
+ from server.utils import get_ak_info_from_request
11
+ from server.user_files import UserFileManager
12
+ from api.messages import ERROR_MESSAGES, get_message
13
+
14
+
15
+ def get_user_identifier(access_key: Optional[str], app_key: Optional[str], session_id: Optional[str]) -> str:
16
+ """
17
+ Get unique user identifier based on access_key or session_id
18
+
19
+ Args:
20
+ access_key: User's access key
21
+ app_key: User's app key
22
+ session_id: Session ID for temporary users
23
+
24
+ Returns:
25
+ Unique user identifier string
26
+ """
27
+ if access_key:
28
+ return access_key
29
+ return session_id or "anonymous"
30
+
31
+
32
+ def extract_session_id_from_request(request: Request) -> Optional[str]:
33
+ """
34
+ Extract session_id from request cookies
35
+
36
+ Args:
37
+ request: FastAPI request object
38
+
39
+ Returns:
40
+ Session ID if found, None otherwise
41
+ """
42
+ cookie_header = request.headers.get("cookie", "")
43
+ if not cookie_header:
44
+ return None
45
+
46
+ from http.cookies import SimpleCookie
47
+ simple_cookie = SimpleCookie()
48
+ simple_cookie.load(cookie_header)
49
+
50
+ if "session_id" in simple_cookie:
51
+ return simple_cookie["session_id"].value
52
+ return None
53
+
54
+
55
+ def get_user_context_from_request(request: Request) -> Tuple[str, Optional[str], Optional[str]]:
56
+ """
57
+ Extract user context from request
58
+
59
+ Returns:
60
+ Tuple of (user_identifier, access_key, app_key)
61
+ """
62
+ access_key, app_key = get_ak_info_from_request(request.headers)
63
+ session_id = extract_session_id_from_request(request)
64
+ user_identifier = get_user_identifier(access_key, app_key, session_id)
65
+
66
+ return user_identifier, access_key, app_key
67
+
68
+
69
+ def validate_file_access(file_path: Path, user_files_dir: Path, lang='zh') -> Optional[JSONResponse]:
70
+ """
71
+ Validate if file path is within user directory and accessible
72
+
73
+ Args:
74
+ file_path: Path to validate
75
+ user_files_dir: User's files directory
76
+ lang: Language for error messages
77
+
78
+ Returns:
79
+ JSONResponse with error if validation fails, None if valid
80
+ """
81
+ try:
82
+ # Resolve paths to absolute
83
+ file_resolved = file_path.resolve()
84
+ user_files_dir_resolved = user_files_dir.resolve()
85
+
86
+ # Check if file is within user directory
87
+ if not str(file_resolved).startswith(str(user_files_dir_resolved)):
88
+ return JSONResponse(
89
+ content={"error": get_message(ERROR_MESSAGES['access_denied'], lang)},
90
+ status_code=403
91
+ )
92
+
93
+ # Check if file exists
94
+ if not file_resolved.exists():
95
+ error_key = 'folder_not_found' if file_path.is_dir() else 'file_not_found'
96
+ return JSONResponse(
97
+ content={"error": get_message(ERROR_MESSAGES[error_key], lang)},
98
+ status_code=404
99
+ )
100
+
101
+ return None
102
+
103
+ except Exception:
104
+ return JSONResponse(
105
+ content={"error": get_message(ERROR_MESSAGES['invalid_file_path'], lang)},
106
+ status_code=400
107
+ )
108
+
109
+
110
+ def process_file_path(file_path_str: str, user_files_dir: Path) -> Path:
111
+ """
112
+ Process file path string and convert to Path object
113
+
114
+ Args:
115
+ file_path_str: File path as string
116
+ user_files_dir: User's files directory
117
+
118
+ Returns:
119
+ Processed Path object
120
+ """
121
+ if file_path_str.startswith('/'):
122
+ return Path(file_path_str)
123
+ else:
124
+ # Relative path, based on user directory
125
+ return user_files_dir / file_path_str
126
+
127
+
128
+ def safe_filename(filename: str) -> str:
129
+ """
130
+ Generate safe filename by replacing dangerous characters
131
+
132
+ Args:
133
+ filename: Original filename
134
+
135
+ Returns:
136
+ Safe filename
137
+ """
138
+ return filename.replace('/', '_').replace('\\', '_').replace('..', '_')
139
+
140
+
141
+ async def check_project_id_required(context_manager, user_identifier: str) -> bool:
142
+ """
143
+ Check if project_id is required and set
144
+
145
+ Args:
146
+ context_manager: WebSocket manager instance
147
+ user_identifier: User identifier
148
+
149
+ Returns:
150
+ True if project_id is set, False otherwise
151
+ """
152
+ # First check environment variable
153
+ if os.environ.get('BOHR_PROJECT_ID'):
154
+ return True
155
+
156
+ # Check user's connection context
157
+ for context in context_manager.active_connections.values():
158
+ if context.get_user_identifier() == user_identifier:
159
+ return bool(context.project_id)
160
+
161
+ return False