erdo 0.1.31__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 (48) hide show
  1. erdo/__init__.py +35 -0
  2. erdo/_generated/__init__.py +18 -0
  3. erdo/_generated/actions/__init__.py +34 -0
  4. erdo/_generated/actions/analysis.py +179 -0
  5. erdo/_generated/actions/bot.py +186 -0
  6. erdo/_generated/actions/codeexec.py +199 -0
  7. erdo/_generated/actions/llm.py +148 -0
  8. erdo/_generated/actions/memory.py +463 -0
  9. erdo/_generated/actions/pdfextractor.py +97 -0
  10. erdo/_generated/actions/resource_definitions.py +296 -0
  11. erdo/_generated/actions/sqlexec.py +90 -0
  12. erdo/_generated/actions/utils.py +475 -0
  13. erdo/_generated/actions/webparser.py +119 -0
  14. erdo/_generated/actions/websearch.py +85 -0
  15. erdo/_generated/condition/__init__.py +556 -0
  16. erdo/_generated/internal.py +51 -0
  17. erdo/_generated/internal_actions.py +91 -0
  18. erdo/_generated/parameters.py +17 -0
  19. erdo/_generated/secrets.py +17 -0
  20. erdo/_generated/template_functions.py +55 -0
  21. erdo/_generated/types.py +3907 -0
  22. erdo/actions/__init__.py +40 -0
  23. erdo/bot_permissions.py +266 -0
  24. erdo/cli_entry.py +73 -0
  25. erdo/conditions/__init__.py +11 -0
  26. erdo/config/__init__.py +5 -0
  27. erdo/config/config.py +140 -0
  28. erdo/formatting.py +279 -0
  29. erdo/install_cli.py +140 -0
  30. erdo/integrations.py +131 -0
  31. erdo/invoke/__init__.py +11 -0
  32. erdo/invoke/client.py +234 -0
  33. erdo/invoke/invoke.py +555 -0
  34. erdo/state.py +376 -0
  35. erdo/sync/__init__.py +17 -0
  36. erdo/sync/client.py +95 -0
  37. erdo/sync/extractor.py +492 -0
  38. erdo/sync/sync.py +327 -0
  39. erdo/template.py +136 -0
  40. erdo/test/__init__.py +41 -0
  41. erdo/test/evaluate.py +272 -0
  42. erdo/test/runner.py +263 -0
  43. erdo/types.py +1431 -0
  44. erdo-0.1.31.dist-info/METADATA +471 -0
  45. erdo-0.1.31.dist-info/RECORD +48 -0
  46. erdo-0.1.31.dist-info/WHEEL +4 -0
  47. erdo-0.1.31.dist-info/entry_points.txt +2 -0
  48. erdo-0.1.31.dist-info/licenses/LICENSE +22 -0
erdo/state.py ADDED
@@ -0,0 +1,376 @@
1
+ """
2
+ Erdo State Management
3
+
4
+ Provides a magic `state` object that allows clean Python syntax like:
5
+ - state.code
6
+ - state.dataset.id
7
+ - f"Analysis for: {state.code}"
8
+
9
+ The state object tracks field access for static analysis and template conversion.
10
+ """
11
+
12
+ import ast
13
+ from collections import defaultdict
14
+ from typing import Any, Dict, Optional, Set
15
+
16
+ # Import template functions list - no fallback, fail fast if missing
17
+ from ._generated.template_functions import ALL_TEMPLATE_FUNCTIONS
18
+
19
+
20
+ class StateFieldTracker:
21
+ """Tracks field access on the state object for template conversion."""
22
+
23
+ def __init__(self):
24
+ self.accessed_fields: Set[str] = set()
25
+ self.nested_access: Dict[str, Set[str]] = defaultdict(set)
26
+
27
+ def record_access(self, field_path: str) -> None:
28
+ """Record that a state field was accessed."""
29
+ self.accessed_fields.add(field_path)
30
+
31
+ # Track nested access (e.g., "dataset.id" -> nested_access["dataset"].add("id"))
32
+ parts = field_path.split(".")
33
+ if len(parts) > 1:
34
+ parent = parts[0]
35
+ child = ".".join(parts[1:])
36
+ self.nested_access[parent].add(child)
37
+
38
+
39
+ class StateMethodProxy:
40
+ """Proxy object for state method calls like state.toJSON(x)"""
41
+
42
+ def __init__(self, method_name: str, tracker: StateFieldTracker):
43
+ self._method_name = method_name
44
+ self._tracker = tracker
45
+
46
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
47
+ """Handle method calls like state.toJSON(state.security_issues)"""
48
+ # Record that this method was called
49
+ self._tracker.record_access(f"{self._method_name}(*args)")
50
+
51
+ # For import-time safety, return a safe placeholder
52
+ if self._method_name == "toJSON":
53
+ return f"{{{{toJSON {args[0] if args else ''}}}}}"
54
+ elif self._method_name == "len":
55
+ return f"{{{{len {args[0] if args else ''}}}}}"
56
+ else:
57
+ # Generic method call placeholder
58
+ return f"{{{{{self._method_name} {' '.join(str(arg) for arg in args)}}}}}"
59
+
60
+ def __str__(self) -> str:
61
+ return f"{{{{.{self._method_name}}}}}"
62
+
63
+ def __repr__(self) -> str:
64
+ return f"StateMethodProxy('{self._method_name}')"
65
+
66
+
67
+ class NestedStateProxy(str):
68
+ """Proxy object for nested state access like state.dataset.id"""
69
+
70
+ def __new__(cls, parent_path: str, tracker: StateFieldTracker):
71
+ # Create a string with the template representation
72
+ template_str = f"{{{{.Data.{parent_path}}}}}"
73
+ obj = str.__new__(cls, template_str)
74
+ obj._parent_path = parent_path
75
+ obj._tracker = tracker
76
+ return obj
77
+
78
+ def __getattr__(self, name: str) -> Any:
79
+ if name.startswith("_"):
80
+ return super().__getattribute__(name)
81
+ field_path = f"{self._parent_path}.{name}"
82
+ self._tracker.record_access(field_path)
83
+
84
+ # Return another proxy for further nesting
85
+ return NestedStateProxy(field_path, self._tracker)
86
+
87
+ def __str__(self) -> str:
88
+ """Convert to template string when used in f-strings."""
89
+ # Handle special references that need .Data prefix
90
+ if self._parent_path.startswith("steps.") or self._parent_path.startswith(
91
+ "system."
92
+ ):
93
+ return f"{{{{.Data.{self._parent_path}}}}}"
94
+ return f"{{{{{self._parent_path}}}}}"
95
+
96
+ def __repr__(self) -> str:
97
+ return f"NestedStateProxy('{self._parent_path}')"
98
+
99
+ def __reduce__(self):
100
+ """Support for pickling/serialization - return the template string."""
101
+ return (str, (f"{{{{.Data.{self._parent_path}}}}}",))
102
+
103
+ def __eq__(self, other: Any) -> bool:
104
+ """Handle equality comparisons gracefully."""
105
+ if isinstance(other, NestedStateProxy):
106
+ return self._parent_path == other._parent_path
107
+ return False
108
+
109
+ def __bool__(self):
110
+ """Handle boolean context gracefully."""
111
+ return True
112
+
113
+ def __hash__(self):
114
+ """Make proxy hashable for use in dicts/sets."""
115
+ return hash(self._parent_path)
116
+
117
+ def __iter__(self):
118
+ """Handle iteration attempts gracefully."""
119
+ return iter([])
120
+
121
+ def __len__(self):
122
+ """Handle len() calls gracefully."""
123
+ return 0
124
+
125
+ def __getitem__(self, key: Any) -> "NestedStateProxy":
126
+ """Handle indexing gracefully."""
127
+ return NestedStateProxy(f"{self._parent_path}[{key}]", self._tracker)
128
+
129
+ def __setattr__(self, name: str, value: Any):
130
+ """Override setattr to allow internal attributes while tracking field access."""
131
+ if name.startswith("_"):
132
+ super().__setattr__(name, value)
133
+ else:
134
+ # Record the assignment as a field access
135
+ field_path = (
136
+ f"{self._parent_path}.{name}" if hasattr(self, "_parent_path") else name
137
+ )
138
+ if hasattr(self, "_tracker"):
139
+ self._tracker.record_access(field_path)
140
+
141
+
142
+ class StateMagic:
143
+ """Magic state object that tracks field access and provides clean Python syntax."""
144
+
145
+ def __init__(self):
146
+ self._tracker = StateFieldTracker()
147
+ self._test_values: Dict[str, Any] = {}
148
+
149
+ def __getattr__(self, name: str) -> Any:
150
+ """Handle attribute access like state.code, state.dataset, etc."""
151
+ self._tracker.record_access(name)
152
+
153
+ # Handle method calls that should return callable proxies
154
+ if name in ALL_TEMPLATE_FUNCTIONS:
155
+ return StateMethodProxy(name, self._tracker)
156
+
157
+ # If we have a test value, check if it's a dict (nested object)
158
+ if name in self._test_values:
159
+ test_value = self._test_values[name]
160
+ if isinstance(test_value, dict):
161
+ # For nested objects, return a proxy that can handle further access
162
+ proxy = NestedStateProxy(name, self._tracker)
163
+ # Attach test data to the proxy for local testing
164
+ proxy._test_data = test_value
165
+ return proxy
166
+ # For non-dict test values, still return a proxy to allow chaining
167
+ # but it will return the string representation when accessed
168
+ return NestedStateProxy(name, self._tracker)
169
+
170
+ # Always return a NestedStateProxy to support nested access
171
+ # This allows state.organization.name to work correctly
172
+ return NestedStateProxy(name, self._tracker)
173
+
174
+ def __str__(self) -> str:
175
+ """When used in f-strings, this shouldn't happen (individual fields should be accessed)."""
176
+ return "{{state}}" # Fallback, though this should rarely be used
177
+
178
+ def __setattr__(self, name: str, value: Any):
179
+ """Override setattr to allow internal attributes while tracking field access."""
180
+ if name.startswith("_"):
181
+ super().__setattr__(name, value)
182
+ else:
183
+ # Record the assignment as a field access
184
+ self._tracker.record_access(name)
185
+ self._test_values[name] = value
186
+
187
+ def set_test_value(self, field_path: str, value: Any):
188
+ """Set a test value for local development/testing."""
189
+ parts = field_path.split(".")
190
+ current = self._test_values
191
+
192
+ for part in parts[:-1]:
193
+ if part not in current:
194
+ current[part] = {}
195
+ current = current[part]
196
+
197
+ current[parts[-1]] = value
198
+
199
+ def get_accessed_fields(self) -> Set[str]:
200
+ """Get all fields that have been accessed."""
201
+ return self._tracker.accessed_fields.copy()
202
+
203
+ def clear_tracking(self):
204
+ """Clear the field access tracking."""
205
+ self._tracker = StateFieldTracker()
206
+
207
+
208
+ # Global state object for use in agent definitions
209
+ state = StateMagic()
210
+
211
+
212
+ def extract_state_references_from_ast(source_code: str) -> Set[str]:
213
+ """Extract all state.* references from Python source code using AST parsing."""
214
+ try:
215
+ tree = ast.parse(source_code)
216
+ except SyntaxError:
217
+ return set()
218
+
219
+ state_refs = set()
220
+
221
+ class StateVisitor(ast.NodeVisitor):
222
+ def visit_Attribute(self, node):
223
+ """Visit attribute access like state.code, state.dataset.id"""
224
+ if isinstance(node.value, ast.Name) and node.value.id == "state":
225
+ # Simple case: state.field
226
+ state_refs.add(node.attr)
227
+ elif isinstance(node.value, ast.Attribute):
228
+ # Nested case: state.dataset.id
229
+ path = self._get_full_attribute_path(node)
230
+ if path and path.startswith("state."):
231
+ # Remove 'state.' prefix
232
+ field_path = path[6:]
233
+ state_refs.add(field_path)
234
+
235
+ self.generic_visit(node)
236
+
237
+ def visit_JoinedStr(self, node):
238
+ """Visit f-string expressions like f"Analysis for: {state.code}" """
239
+ for value in node.values:
240
+ if isinstance(value, ast.FormattedValue):
241
+ # Extract the expression inside the f-string
242
+ if isinstance(value.value, ast.Attribute):
243
+ path = self._get_full_attribute_path(value.value)
244
+ if path and path.startswith("state."):
245
+ field_path = path[6:]
246
+ state_refs.add(field_path)
247
+ elif (
248
+ isinstance(value.value, ast.Name) and value.value.id == "state"
249
+ ):
250
+ state_refs.add("state") # Direct state reference
251
+
252
+ self.generic_visit(node)
253
+
254
+ def _get_full_attribute_path(self, node):
255
+ """Get the full dotted path for an attribute access."""
256
+ if isinstance(node, ast.Attribute):
257
+ if isinstance(node.value, ast.Name):
258
+ return f"{node.value.id}.{node.attr}"
259
+ else:
260
+ parent_path = self._get_full_attribute_path(node.value)
261
+ if parent_path:
262
+ return f"{parent_path}.{node.attr}"
263
+ elif isinstance(node, ast.Name):
264
+ return node.id
265
+ return None
266
+
267
+ visitor = StateVisitor()
268
+ visitor.visit(tree)
269
+
270
+ return state_refs
271
+
272
+
273
+ def convert_fstring_to_template(source_code: str, state_refs: Set[str]) -> str:
274
+ """Convert f-strings with state references to Go template format."""
275
+
276
+ class FStringConverter(ast.NodeTransformer):
277
+ def visit_JoinedStr(self, node):
278
+ """Convert f-strings to regular strings with Go template syntax."""
279
+ parts = []
280
+ has_state_ref = False
281
+
282
+ for value in node.values:
283
+ if isinstance(value, ast.Constant):
284
+ # Regular string part
285
+ parts.append(value.value)
286
+ elif isinstance(value, ast.FormattedValue):
287
+ # Expression inside f-string
288
+ if isinstance(value.value, ast.Attribute):
289
+ path = self._get_full_attribute_path(value.value)
290
+ if path and path.startswith("state."):
291
+ # Convert state.field to {{field}}
292
+ field_path = path[6:]
293
+ parts.append(f"{{{{{field_path}}}}}")
294
+ has_state_ref = True
295
+ continue
296
+
297
+ # Non-state expression - convert back to string representation
298
+ # This is complex, so for now we'll leave it as is
299
+ # In practice, most f-strings in agent code should be simple state refs
300
+ parts.append(f"{{{ast.unparse(value.value)}}}")
301
+
302
+ if has_state_ref:
303
+ # Replace the f-string with a regular string
304
+ template_str = "".join(parts)
305
+ return ast.Constant(value=template_str)
306
+
307
+ return node
308
+
309
+ def _get_full_attribute_path(self, node):
310
+ """Get the full dotted path for an attribute access."""
311
+ if isinstance(node, ast.Attribute):
312
+ if isinstance(node.value, ast.Name):
313
+ return f"{node.value.id}.{node.attr}"
314
+ else:
315
+ parent_path = self._get_full_attribute_path(node.value)
316
+ if parent_path:
317
+ return f"{parent_path}.{node.attr}"
318
+ elif isinstance(node, ast.Name):
319
+ return node.id
320
+ return None
321
+
322
+ try:
323
+ tree = ast.parse(source_code)
324
+ converter = FStringConverter()
325
+ new_tree = converter.visit(tree)
326
+ return ast.unparse(new_tree)
327
+ except Exception:
328
+ # If conversion fails, return original
329
+ return source_code
330
+
331
+
332
+ def validate_state_fields(
333
+ state_refs: Set[str], available_fields: Optional[Set[str]] = None
334
+ ) -> Dict[str, str]:
335
+ """Validate that all referenced state fields are available.
336
+
337
+ Args:
338
+ state_refs: Set of state field references found in the code
339
+ available_fields: Optional set of known available fields. If None, no validation is performed.
340
+
341
+ Returns a dict of {invalid_field: error_message} for any issues.
342
+ """
343
+ errors: Dict[str, str] = {}
344
+
345
+ # If no available_fields provided, skip validation (user-defined state is flexible)
346
+ if available_fields is None:
347
+ return errors
348
+
349
+ for field_ref in state_refs:
350
+ if field_ref not in available_fields:
351
+ # Check if it's a nested field
352
+ parts = field_ref.split(".")
353
+ if len(parts) > 1:
354
+ parent = parts[0]
355
+ if parent not in available_fields:
356
+ errors[field_ref] = f"State field '{parent}' is not available"
357
+ # For nested fields, assume they're valid if parent exists
358
+ # Real validation would require schema knowledge
359
+ else:
360
+ errors[field_ref] = f"State field '{field_ref}' is not available"
361
+
362
+ return errors
363
+
364
+
365
+ def setup_test_state(**test_values):
366
+ """Setup test values for local development and testing.
367
+
368
+ Example:
369
+ setup_test_state(
370
+ code="print('hello')",
371
+ dataset={'id': 'test123', 'config': {'type': 'csv'}},
372
+ query="analyze this data"
373
+ )
374
+ """
375
+ for field_path, value in test_values.items():
376
+ state.set_test_value(field_path, value)
erdo/sync/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """Sync module for Erdo SDK - sync agents to the backend."""
2
+
3
+ from .sync import (
4
+ Sync,
5
+ SyncResult,
6
+ sync_agent,
7
+ sync_agents_from_directory,
8
+ sync_agents_from_file,
9
+ )
10
+
11
+ __all__ = [
12
+ "Sync",
13
+ "SyncResult",
14
+ "sync_agent",
15
+ "sync_agents_from_file",
16
+ "sync_agents_from_directory",
17
+ ]
erdo/sync/client.py ADDED
@@ -0,0 +1,95 @@
1
+ """API client for syncing agents to the backend."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional
5
+
6
+ import requests
7
+
8
+ from ..config import get_config
9
+ from .extractor import TemplateStringEncoder
10
+
11
+
12
+ class SyncClient:
13
+ """Client for syncing agents to the Erdo backend."""
14
+
15
+ def __init__(
16
+ self, endpoint: Optional[str] = None, auth_token: Optional[str] = None
17
+ ):
18
+ """Initialize the sync client.
19
+
20
+ Args:
21
+ endpoint: API endpoint URL. If not provided, uses config.
22
+ auth_token: Authentication token. If not provided, uses config.
23
+ """
24
+ config = get_config()
25
+ self.endpoint = endpoint or config.endpoint
26
+ self.auth_token = auth_token or config.auth_token
27
+
28
+ def upsert_bot(self, bot_request: Dict[str, Any]) -> str:
29
+ """Upsert a bot to the backend.
30
+
31
+ Args:
32
+ bot_request: The bot request data containing bot info, steps, etc.
33
+
34
+ Returns:
35
+ The bot ID of the upserted bot.
36
+
37
+ Raises:
38
+ requests.RequestException: If the API request fails.
39
+ ValueError: If the response is invalid.
40
+ """
41
+ url = f"{self.endpoint}/bot/upsert"
42
+ headers = {
43
+ "Content-Type": "application/json",
44
+ "Authorization": f"Bearer {self.auth_token}",
45
+ }
46
+
47
+ # Use custom encoder to handle special types
48
+ json_data = json.dumps(bot_request, cls=TemplateStringEncoder)
49
+ response = requests.post(url, data=json_data, headers=headers)
50
+
51
+ if response.status_code != 200:
52
+ error_msg = f"API request failed with status {response.status_code}"
53
+ try:
54
+ error_details = response.text
55
+ error_msg = f"{error_msg}: {error_details}"
56
+ except Exception:
57
+ pass
58
+ raise requests.RequestException(error_msg)
59
+
60
+ try:
61
+ result = response.json()
62
+ return result.get("bot_id", "")
63
+ except json.JSONDecodeError as e:
64
+ raise ValueError(f"Failed to decode response: {e}")
65
+
66
+ def sync_test(self, test_data: Dict[str, Any]) -> Dict[str, Any]:
67
+ """Sync a test to the backend.
68
+
69
+ Args:
70
+ test_data: The test data to sync.
71
+
72
+ Returns:
73
+ The response from the API.
74
+
75
+ Raises:
76
+ requests.RequestException: If the API request fails.
77
+ """
78
+ url = f"{self.endpoint}/test/sync"
79
+ headers = {
80
+ "Content-Type": "application/json",
81
+ "Authorization": f"Bearer {self.auth_token}",
82
+ }
83
+
84
+ response = requests.post(url, json=test_data, headers=headers)
85
+
86
+ if response.status_code != 200:
87
+ error_msg = f"API request failed with status {response.status_code}"
88
+ try:
89
+ error_details = response.text
90
+ error_msg = f"{error_msg}: {error_details}"
91
+ except Exception:
92
+ pass
93
+ raise requests.RequestException(error_msg)
94
+
95
+ return response.json()