erdo 0.1.4__py3-none-any.whl → 0.1.6__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.

Potentially problematic release.


This version of erdo might be problematic. Click here for more details.

@@ -0,0 +1,266 @@
1
+ """Bot permission management for the Erdo SDK."""
2
+
3
+ import os
4
+ from typing import Any, Dict, Optional
5
+
6
+ import requests
7
+
8
+
9
+ class BotPermissions:
10
+ """Manage RBAC permissions for bots."""
11
+
12
+ def __init__(self, base_url: Optional[str] = None):
13
+ """Initialize bot permissions manager.
14
+
15
+ Args:
16
+ base_url: Erdo server base URL. Defaults to ERDO_SERVER_URL env var or localhost:4000
17
+ """
18
+ self.base_url = base_url or os.getenv(
19
+ "ERDO_SERVER_URL", "http://localhost:4000"
20
+ )
21
+ self.session = requests.Session()
22
+
23
+ # Set auth token if available
24
+ token = os.getenv("ERDO_AUTH_TOKEN")
25
+ if token:
26
+ self.session.headers.update({"Authorization": f"Bearer {token}"})
27
+
28
+ def set_public_access(
29
+ self, bot_id: str, is_public: bool = True, permission_level: str = "view"
30
+ ) -> bool:
31
+ """Set public access for a bot.
32
+
33
+ Args:
34
+ bot_id: Bot ID
35
+ is_public: Whether to make the bot public
36
+ permission_level: Permission level for public access (view, comment, edit, admin)
37
+
38
+ Returns:
39
+ True if successful, False otherwise
40
+ """
41
+ try:
42
+ url = f"{self.base_url}/rbac/bot/{bot_id}/public"
43
+ data = {
44
+ "isPublic": is_public,
45
+ "level": permission_level if is_public else None,
46
+ }
47
+ response = self.session.put(url, json=data)
48
+ return response.status_code == 200
49
+ except Exception as e:
50
+ print(f"Error setting public access: {e}")
51
+ return False
52
+
53
+ def set_user_permission(
54
+ self, bot_id: str, user_id: str, permission_level: str
55
+ ) -> bool:
56
+ """Set permissions for a specific user.
57
+
58
+ Args:
59
+ bot_id: Bot ID
60
+ user_id: User ID
61
+ permission_level: Permission level (view, comment, edit, admin, owner)
62
+
63
+ Returns:
64
+ True if successful, False otherwise
65
+ """
66
+ try:
67
+ url = f"{self.base_url}/rbac/bot/{bot_id}/user/{user_id}"
68
+ data = {"level": permission_level}
69
+ response = self.session.put(url, json=data)
70
+ return response.status_code == 200
71
+ except Exception as e:
72
+ print(f"Error setting user permission: {e}")
73
+ return False
74
+
75
+ def set_org_permission(
76
+ self, bot_id: str, org_id: str, permission_level: str
77
+ ) -> bool:
78
+ """Set permissions for an organization.
79
+
80
+ Args:
81
+ bot_id: Bot ID
82
+ org_id: Organization ID
83
+ permission_level: Permission level (view, comment, edit, admin, owner)
84
+
85
+ Returns:
86
+ True if successful, False otherwise
87
+ """
88
+ try:
89
+ url = f"{self.base_url}/rbac/bot/{bot_id}/org/{org_id}"
90
+ data = {"level": permission_level}
91
+ response = self.session.put(url, json=data)
92
+ return response.status_code == 200
93
+ except Exception as e:
94
+ print(f"Error setting org permission: {e}")
95
+ return False
96
+
97
+ def remove_user_permission(self, bot_id: str, user_id: str) -> bool:
98
+ """Remove permissions for a specific user.
99
+
100
+ Args:
101
+ bot_id: Bot ID
102
+ user_id: User ID
103
+
104
+ Returns:
105
+ True if successful, False otherwise
106
+ """
107
+ try:
108
+ url = f"{self.base_url}/rbac/bot/{bot_id}/user/{user_id}"
109
+ response = self.session.delete(url)
110
+ return response.status_code == 200
111
+ except Exception as e:
112
+ print(f"Error removing user permission: {e}")
113
+ return False
114
+
115
+ def remove_org_permission(self, bot_id: str, org_id: str) -> bool:
116
+ """Remove permissions for an organization.
117
+
118
+ Args:
119
+ bot_id: Bot ID
120
+ org_id: Organization ID
121
+
122
+ Returns:
123
+ True if successful, False otherwise
124
+ """
125
+ try:
126
+ url = f"{self.base_url}/rbac/bot/{bot_id}/org/{org_id}"
127
+ response = self.session.delete(url)
128
+ return response.status_code == 200
129
+ except Exception as e:
130
+ print(f"Error removing org permission: {e}")
131
+ return False
132
+
133
+ def get_permissions(self, bot_id: str) -> Optional[Dict[str, Any]]:
134
+ """Get all permissions for a bot.
135
+
136
+ Args:
137
+ bot_id: Bot ID
138
+
139
+ Returns:
140
+ Dictionary with permission details or None if error
141
+ """
142
+ try:
143
+ url = f"{self.base_url}/rbac/permissions/bot/{bot_id}"
144
+ response = self.session.get(url)
145
+ if response.status_code == 200:
146
+ return response.json()
147
+ return None
148
+ except Exception as e:
149
+ print(f"Error getting permissions: {e}")
150
+ return None
151
+
152
+ def check_access(self, bot_id: str, permission_level: str = "view") -> bool:
153
+ """Check if current user has access to a bot.
154
+
155
+ Args:
156
+ bot_id: Bot ID
157
+ permission_level: Required permission level
158
+
159
+ Returns:
160
+ True if user has access, False otherwise
161
+ """
162
+ try:
163
+ url = f"{self.base_url}/rbac/access/bot/{bot_id}/{permission_level}"
164
+ response = self.session.get(url)
165
+ if response.status_code == 200:
166
+ data = response.json()
167
+ return data.get("hasAccess", False)
168
+ return False
169
+ except Exception as e:
170
+ print(f"Error checking access: {e}")
171
+ return False
172
+
173
+ def invite_user(self, bot_id: str, email: str, permission_level: str) -> bool:
174
+ """Invite a user to access a bot.
175
+
176
+ Args:
177
+ bot_id: Bot ID
178
+ email: User's email address
179
+ permission_level: Permission level to grant
180
+
181
+ Returns:
182
+ True if successful, False otherwise
183
+ """
184
+ try:
185
+ url = f"{self.base_url}/rbac/resource/bot/{bot_id}/invite"
186
+ data = {"email": email, "permissionLevel": permission_level}
187
+ response = self.session.post(url, json=data)
188
+ return response.status_code == 200
189
+ except Exception as e:
190
+ print(f"Error inviting user: {e}")
191
+ return False
192
+
193
+
194
+ # Convenience functions for direct use
195
+ def set_bot_public(
196
+ bot_id: str, is_public: bool = True, permission_level: str = "view"
197
+ ) -> bool:
198
+ """Set public access for a bot.
199
+
200
+ Args:
201
+ bot_id: Bot ID
202
+ is_public: Whether to make the bot public
203
+ permission_level: Permission level for public access
204
+
205
+ Returns:
206
+ True if successful, False otherwise
207
+ """
208
+ permissions = BotPermissions()
209
+ return permissions.set_public_access(bot_id, is_public, permission_level)
210
+
211
+
212
+ def set_bot_user_permission(bot_id: str, user_id: str, permission_level: str) -> bool:
213
+ """Set permissions for a user on a bot.
214
+
215
+ Args:
216
+ bot_id: Bot ID
217
+ user_id: User ID
218
+ permission_level: Permission level
219
+
220
+ Returns:
221
+ True if successful, False otherwise
222
+ """
223
+ permissions = BotPermissions()
224
+ return permissions.set_user_permission(bot_id, user_id, permission_level)
225
+
226
+
227
+ def set_bot_org_permission(bot_id: str, org_id: str, permission_level: str) -> bool:
228
+ """Set permissions for an organization on a bot.
229
+
230
+ Args:
231
+ bot_id: Bot ID
232
+ org_id: Organization ID
233
+ permission_level: Permission level
234
+
235
+ Returns:
236
+ True if successful, False otherwise
237
+ """
238
+ permissions = BotPermissions()
239
+ return permissions.set_org_permission(bot_id, org_id, permission_level)
240
+
241
+
242
+ def get_bot_permissions(bot_id: str) -> Optional[Dict[str, Any]]:
243
+ """Get all permissions for a bot.
244
+
245
+ Args:
246
+ bot_id: Bot ID
247
+
248
+ Returns:
249
+ Dictionary with permission details or None if error
250
+ """
251
+ permissions = BotPermissions()
252
+ return permissions.get_permissions(bot_id)
253
+
254
+
255
+ def check_bot_access(bot_id: str, permission_level: str = "view") -> bool:
256
+ """Check if current user has access to a bot.
257
+
258
+ Args:
259
+ bot_id: Bot ID
260
+ permission_level: Required permission level
261
+
262
+ Returns:
263
+ True if user has access, False otherwise
264
+ """
265
+ permissions = BotPermissions()
266
+ return permissions.check_access(bot_id, permission_level)
@@ -0,0 +1,5 @@
1
+ """Configuration management for Erdo SDK."""
2
+
3
+ from .config import Config, get_config
4
+
5
+ __all__ = ["Config", "get_config"]
erdo/config/config.py ADDED
@@ -0,0 +1,140 @@
1
+ """Configuration management for Erdo SDK."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+
9
+ class Config:
10
+ """Configuration manager for Erdo SDK."""
11
+
12
+ def __init__(
13
+ self, endpoint: Optional[str] = None, auth_token: Optional[str] = None
14
+ ):
15
+ """Initialize configuration.
16
+
17
+ Args:
18
+ endpoint: API endpoint URL. If not provided, will try environment variable or config file.
19
+ auth_token: Authentication token. If not provided, will try environment variable or config file.
20
+ """
21
+ self._endpoint = endpoint
22
+ self._auth_token = auth_token
23
+ self._config_yaml = Path.home() / ".erdo" / "config.yaml"
24
+ self._config_json = Path.home() / ".erdo" / "config.json"
25
+
26
+ @property
27
+ def endpoint(self) -> str:
28
+ """Get the API endpoint."""
29
+ if self._endpoint:
30
+ return self._endpoint
31
+
32
+ # Try environment variable
33
+ env_endpoint = os.environ.get("ERDO_ENDPOINT")
34
+ if env_endpoint:
35
+ return env_endpoint
36
+
37
+ # Try config file
38
+ config = self._load_config_file()
39
+ if config and "endpoint" in config:
40
+ return config["endpoint"]
41
+
42
+ raise ValueError(
43
+ "No endpoint configured. Set ERDO_ENDPOINT environment variable or run 'erdo configure'"
44
+ )
45
+
46
+ @property
47
+ def auth_token(self) -> str:
48
+ """Get the authentication token."""
49
+ if self._auth_token:
50
+ return self._auth_token
51
+
52
+ # Try environment variable
53
+ env_token = os.environ.get("ERDO_AUTH_TOKEN")
54
+ if env_token:
55
+ return env_token
56
+
57
+ # Try config file
58
+ config = self._load_config_file()
59
+ if config and "auth_token" in config:
60
+ return config["auth_token"]
61
+
62
+ raise ValueError(
63
+ "No auth token configured. Set ERDO_AUTH_TOKEN environment variable or run 'erdo login'"
64
+ )
65
+
66
+ def _load_config_file(self) -> Optional[dict]:
67
+ """Load configuration from file (supports both YAML and JSON)."""
68
+ # Try YAML first (CLI default format)
69
+ if self._config_yaml.exists():
70
+ try:
71
+ import yaml
72
+
73
+ with open(self._config_yaml, "r") as f:
74
+ return yaml.safe_load(f)
75
+ except Exception:
76
+ try:
77
+ # Fallback to simple parsing if yaml not available
78
+ with open(self._config_yaml, "r") as f:
79
+ config = {}
80
+ for line in f:
81
+ if ": " in line:
82
+ key, value = line.strip().split(": ", 1)
83
+ config[key] = value
84
+ return config
85
+ except Exception:
86
+ pass
87
+
88
+ # Try JSON as fallback
89
+ if self._config_json.exists():
90
+ try:
91
+ with open(self._config_json, "r") as f:
92
+ return json.load(f)
93
+ except (json.JSONDecodeError, IOError):
94
+ pass
95
+
96
+ return None
97
+
98
+ def save(self):
99
+ """Save configuration to file (as JSON)."""
100
+ self._config_json.parent.mkdir(parents=True, exist_ok=True)
101
+
102
+ config = {}
103
+ if self._endpoint:
104
+ config["endpoint"] = self._endpoint
105
+ if self._auth_token:
106
+ config["auth_token"] = self._auth_token
107
+
108
+ with open(self._config_json, "w") as f:
109
+ json.dump(config, f, indent=2)
110
+
111
+ def set_endpoint(self, endpoint: str):
112
+ """Set the API endpoint."""
113
+ self._endpoint = endpoint
114
+
115
+ def set_auth_token(self, auth_token: str):
116
+ """Set the authentication token."""
117
+ self._auth_token = auth_token
118
+
119
+
120
+ # Global config instance
121
+ _config: Optional[Config] = None
122
+
123
+
124
+ def get_config() -> Config:
125
+ """Get the global configuration instance."""
126
+ global _config
127
+ if _config is None:
128
+ _config = Config()
129
+ return _config
130
+
131
+
132
+ def set_config(endpoint: Optional[str] = None, auth_token: Optional[str] = None):
133
+ """Set the global configuration.
134
+
135
+ Args:
136
+ endpoint: API endpoint URL
137
+ auth_token: Authentication token
138
+ """
139
+ global _config
140
+ _config = Config(endpoint=endpoint, auth_token=auth_token)
@@ -0,0 +1,10 @@
1
+ """Invoke module for running agents via the Erdo orchestrator."""
2
+
3
+ from .invoke import Invoke, InvokeResult, invoke_agent, invoke_by_key
4
+
5
+ __all__ = [
6
+ "Invoke",
7
+ "InvokeResult",
8
+ "invoke_agent",
9
+ "invoke_by_key",
10
+ ]
erdo/invoke/client.py ADDED
@@ -0,0 +1,213 @@
1
+ """API client for invoking bots via the backend orchestrator."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Generator, Optional, Union
5
+
6
+ import requests
7
+
8
+ from ..config import get_config
9
+
10
+
11
+ class SSEClient:
12
+ """Client for Server-Sent Events streaming."""
13
+
14
+ def __init__(self, response: requests.Response):
15
+ """Initialize SSE client with a response object."""
16
+ self.response = response
17
+ self.response.encoding = "utf-8"
18
+
19
+ def events(self) -> Generator[Dict[str, Any], None, None]:
20
+ """Yield events from the SSE stream."""
21
+ for line in self.response.iter_lines():
22
+ if line:
23
+ line = line.decode("utf-8") if isinstance(line, bytes) else line
24
+ if line.startswith("data: "):
25
+ data = line[6:] # Remove 'data: ' prefix
26
+ if data.strip():
27
+ try:
28
+ yield json.loads(data)
29
+ except json.JSONDecodeError:
30
+ # Some events might not be JSON
31
+ yield {"raw": data}
32
+
33
+
34
+ class InvokeClient:
35
+ """Client for invoking bots via the Erdo backend."""
36
+
37
+ def __init__(
38
+ self, endpoint: Optional[str] = None, auth_token: Optional[str] = None
39
+ ):
40
+ """Initialize the invoke client.
41
+
42
+ Args:
43
+ endpoint: API endpoint URL. If not provided, uses config.
44
+ auth_token: Authentication token. If not provided, uses config.
45
+ """
46
+ config = get_config()
47
+ self.endpoint = endpoint or config.endpoint
48
+ self.auth_token = auth_token or config.auth_token
49
+
50
+ def invoke_bot(
51
+ self,
52
+ bot_identifier: str,
53
+ parameters: Optional[Dict[str, Any]] = None,
54
+ dataset_ids: Optional[list] = None,
55
+ stream: bool = False,
56
+ ) -> Union[SSEClient, Dict[str, Any]]:
57
+ """Invoke a bot via the backend orchestrator.
58
+
59
+ Args:
60
+ bot_identifier: Bot ID or key (e.g., "erdo.data-analyzer")
61
+ parameters: Parameters to pass to the bot
62
+ dataset_ids: Optional dataset IDs to include
63
+ stream: Whether to return SSE client for streaming (default: False)
64
+
65
+ Returns:
66
+ SSEClient for streaming or final result dict for non-streaming
67
+
68
+ Raises:
69
+ requests.RequestException: If the API request fails.
70
+ """
71
+ url = f"{self.endpoint}/bots/{bot_identifier}/invoke"
72
+ headers = {
73
+ "Authorization": f"Bearer {self.auth_token}",
74
+ "Content-Type": "application/json",
75
+ "Accept": "text/event-stream", # Endpoint always returns SSE
76
+ }
77
+
78
+ # Build invoke parameters
79
+ invoke_params: Dict[str, Any] = {
80
+ "parameters": parameters or {},
81
+ }
82
+
83
+ if dataset_ids:
84
+ invoke_params["dataset_ids"] = dataset_ids
85
+
86
+ # Make the request - always stream to handle SSE
87
+ response = requests.post(url, json=invoke_params, headers=headers, stream=True)
88
+
89
+ if response.status_code != 200:
90
+ error_msg = f"API request failed with status {response.status_code}"
91
+ try:
92
+ error_details = response.text
93
+ error_msg = f"{error_msg}: {error_details}"
94
+ except Exception:
95
+ pass
96
+ raise requests.RequestException(error_msg)
97
+
98
+ sse_client = SSEClient(response)
99
+
100
+ if stream:
101
+ # Return SSE client for streaming
102
+ return sse_client
103
+ else:
104
+ # Consume all events and return final result
105
+ events: list[Dict[str, Any]] = []
106
+ final_result: Dict[str, Any] = {}
107
+
108
+ for event in sse_client.events():
109
+ events.append(event)
110
+
111
+ # Look for completion or result events
112
+ if "payload" in event:
113
+ payload = event["payload"]
114
+ if isinstance(payload, dict):
115
+ if "result" in payload:
116
+ final_result = payload["result"]
117
+ elif "invocation_id" in payload:
118
+ final_result["invocation_id"] = payload["invocation_id"]
119
+ elif "bot_name" in payload:
120
+ final_result["bot_name"] = payload["bot_name"]
121
+
122
+ # Check for invocation completed
123
+ if event.get("type") == "invocation_completed":
124
+ if "result" in event:
125
+ final_result = event["result"]
126
+ break
127
+
128
+ # Return collected data
129
+ return {
130
+ "events": events,
131
+ "result": final_result,
132
+ "event_count": len(events),
133
+ }
134
+
135
+ def invoke_bot_from_thread(
136
+ self,
137
+ bot_identifier: str,
138
+ thread_id: str,
139
+ message: str,
140
+ parameters: Optional[Dict[str, Any]] = None,
141
+ stream: bool = False,
142
+ ) -> Union[SSEClient, Dict[str, Any]]:
143
+ """Invoke a bot from within a thread context.
144
+
145
+ Args:
146
+ bot_identifier: Bot ID or key
147
+ thread_id: Thread ID to invoke from
148
+ message: User message
149
+ parameters: Additional parameters
150
+ stream: Whether to return SSE client for streaming (default: False)
151
+
152
+ Returns:
153
+ SSEClient for streaming or final result dict
154
+ """
155
+ url = f"{self.endpoint}/bots/{bot_identifier}/invoke-from-thread"
156
+ headers = {
157
+ "Authorization": f"Bearer {self.auth_token}",
158
+ "Content-Type": "application/json",
159
+ "Accept": "text/event-stream", # Endpoint always returns SSE
160
+ }
161
+
162
+ invoke_params = {
163
+ "thread_id": thread_id,
164
+ "message": message,
165
+ "parameters": parameters or {},
166
+ }
167
+
168
+ # Make the request - always stream to handle SSE
169
+ response = requests.post(url, json=invoke_params, headers=headers, stream=True)
170
+
171
+ if response.status_code != 200:
172
+ error_msg = f"API request failed with status {response.status_code}"
173
+ try:
174
+ error_details = response.text
175
+ error_msg = f"{error_msg}: {error_details}"
176
+ except Exception:
177
+ pass
178
+ raise requests.RequestException(error_msg)
179
+
180
+ sse_client = SSEClient(response)
181
+
182
+ if stream:
183
+ # Return SSE client for streaming
184
+ return sse_client
185
+ else:
186
+ # Consume all events and return final result
187
+ events: list[Dict[str, Any]] = []
188
+ final_result: Dict[str, Any] = {}
189
+
190
+ for event in sse_client.events():
191
+ events.append(event)
192
+
193
+ # Look for completion or result events
194
+ if "payload" in event:
195
+ payload = event["payload"]
196
+ if isinstance(payload, dict):
197
+ if "result" in payload:
198
+ final_result = payload["result"]
199
+ elif "invocation_id" in payload:
200
+ final_result["invocation_id"] = payload["invocation_id"]
201
+
202
+ # Check for invocation completed
203
+ if event.get("type") == "invocation_completed":
204
+ if "result" in event:
205
+ final_result = event["result"]
206
+ break
207
+
208
+ # Return collected data
209
+ return {
210
+ "events": events,
211
+ "result": final_result,
212
+ "event_count": len(events),
213
+ }