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/sync/extractor.py ADDED
@@ -0,0 +1,492 @@
1
+ """Runtime extraction for syncing agents to the backend."""
2
+
3
+ import ast
4
+ import importlib.util
5
+ import inspect
6
+ import json
7
+ import os
8
+ import sys
9
+ import textwrap
10
+ import warnings
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Union
13
+
14
+
15
+ class TemplateStringEncoder(json.JSONEncoder):
16
+ """Custom JSON encoder that handles TemplateString objects and enums."""
17
+
18
+ def default(self, obj):
19
+ if hasattr(obj, "to_dict") and callable(getattr(obj, "to_dict")):
20
+ return obj.to_dict()
21
+ elif str(type(obj).__name__) == "TemplateString":
22
+ if hasattr(obj, "to_template_string") and callable(
23
+ getattr(obj, "to_template_string")
24
+ ):
25
+ return obj.to_template_string()
26
+ elif hasattr(obj, "template"):
27
+ return obj.template
28
+ else:
29
+ return str(obj)
30
+ elif hasattr(obj, "__class__") and hasattr(obj.__class__, "__mro__"):
31
+ for base in obj.__class__.__mro__:
32
+ if "Enum" in str(base):
33
+ return obj.value if hasattr(obj, "value") else str(obj)
34
+ elif hasattr(obj, "_parent_path"):
35
+ return str(obj)
36
+ return super().default(obj)
37
+
38
+
39
+ def transform_string_value(value: str) -> str:
40
+ """Transform a string value by converting state.field to {{.Data.field}} templates."""
41
+ if not isinstance(value, str):
42
+ return value
43
+
44
+ import re
45
+
46
+ pattern = r"\bstate\.([a-zA-Z_][a-zA-Z0-9_.]*)"
47
+
48
+ def replace_state_ref(match):
49
+ field_path = match.group(1)
50
+ return "{{.Data." + field_path + "}}"
51
+
52
+ transformed = re.sub(pattern, replace_state_ref, value)
53
+ return transformed
54
+
55
+
56
+ def transform_dict_recursively(obj: Any) -> Any:
57
+ """Recursively transform a dictionary/object, converting state references to templates."""
58
+ if hasattr(obj, "_parent_path") and hasattr(obj, "_tracker"):
59
+ field_path = obj._parent_path
60
+ return "{{.Data." + field_path + "}}"
61
+ elif (
62
+ hasattr(obj, "to_dict")
63
+ and hasattr(obj, "filename")
64
+ and "PythonFile" in str(type(obj))
65
+ ):
66
+ return obj.to_dict()
67
+ elif isinstance(obj, dict):
68
+ return {k: transform_dict_recursively(v) for k, v in obj.items()}
69
+ elif isinstance(obj, list):
70
+ return [transform_dict_recursively(item) for item in obj]
71
+ elif isinstance(obj, str):
72
+ return transform_string_value(obj)
73
+ elif hasattr(obj, "__class__") and hasattr(obj.__class__, "__mro__"):
74
+ # Handle enums by converting to their value
75
+ for base in obj.__class__.__mro__:
76
+ if "Enum" in str(base):
77
+ return obj.value if hasattr(obj, "value") else str(obj)
78
+ return obj
79
+ else:
80
+ return obj
81
+
82
+
83
+ def convert_step_dict_to_step_with_handlers(step_dict: Dict) -> Dict:
84
+ """Convert a step dictionary to StepWithHandlers format recursively."""
85
+ step_data = dict(step_dict)
86
+ result_handlers_list = step_data.pop("result_handlers", [])
87
+ converted_result_handlers = []
88
+
89
+ for handler in result_handlers_list:
90
+ converted_handler = dict(handler)
91
+
92
+ if "steps" in converted_handler and isinstance(
93
+ converted_handler["steps"], list
94
+ ):
95
+ converted_steps = []
96
+ for step in converted_handler["steps"]:
97
+ if hasattr(step, "to_dict"):
98
+ converted_step = convert_step_to_step_with_handlers(step)
99
+ else:
100
+ converted_step = convert_step_dict_to_step_with_handlers(step)
101
+ converted_steps.append(converted_step)
102
+ converted_handler["steps"] = converted_steps
103
+
104
+ converted_result_handlers.append(converted_handler)
105
+
106
+ return {
107
+ "step": step_data,
108
+ "result_handlers": converted_result_handlers,
109
+ }
110
+
111
+
112
+ def get_all_python_files_in_directory(
113
+ source_file_path: str, exclude_patterns: Optional[List[str]] = None
114
+ ) -> List[Dict]:
115
+ """Get all Python files in the same directory as the source file."""
116
+ if exclude_patterns is None:
117
+ exclude_patterns = ["__pycache__", "*.pyc", "test_*", "*_test.py"]
118
+
119
+ import fnmatch
120
+
121
+ source_dir = Path(source_file_path).parent
122
+ files = []
123
+
124
+ for py_file in source_dir.glob("*.py"):
125
+ if py_file.samefile(source_file_path):
126
+ continue
127
+
128
+ if any(fnmatch.fnmatch(py_file.name, pattern) for pattern in exclude_patterns):
129
+ continue
130
+
131
+ try:
132
+ with open(py_file, "r", encoding="utf-8") as f:
133
+ content = f.read()
134
+ files.append({"filename": py_file.name, "content": content})
135
+ except Exception as e:
136
+ print(f"Warning: Could not read {py_file}: {e}", file=sys.stderr)
137
+
138
+ return files
139
+
140
+
141
+ def resolve_code_files_references(
142
+ code_files: List, source_file_path: str
143
+ ) -> List[Dict]:
144
+ """Resolve PythonFile references to actual file contents."""
145
+ if not code_files:
146
+ return []
147
+
148
+ resolved_files = []
149
+ source_dir = Path(source_file_path).parent
150
+
151
+ for file_ref in code_files:
152
+ if isinstance(file_ref, dict) and file_ref.get("_type") == "PythonFile":
153
+ filename_with_path = file_ref["filename"]
154
+ file_path = source_dir / filename_with_path
155
+
156
+ try:
157
+ with open(file_path, "r", encoding="utf-8") as f:
158
+ content = f.read()
159
+ base_filename = Path(filename_with_path).name
160
+ resolved_files.append({"filename": base_filename, "content": content})
161
+ except Exception as e:
162
+ print(
163
+ f"Warning: Could not resolve PythonFile {file_path}: {e}",
164
+ file=sys.stderr,
165
+ )
166
+ else:
167
+ resolved_files.append(file_ref)
168
+
169
+ return resolved_files
170
+
171
+
172
+ def extract_and_update_function_code(
173
+ step_obj: Any, step_dict: Dict, source_file_path: str
174
+ ):
175
+ """Extract function body from decorated codeexec.execute function and update step parameters."""
176
+ func_name = getattr(step_obj, "__name__", None)
177
+ if not func_name:
178
+ return
179
+
180
+ try:
181
+ with open(source_file_path, "r") as f:
182
+ source_content = f.read()
183
+
184
+ tree = ast.parse(source_content)
185
+ func_body = None
186
+
187
+ for node in ast.walk(tree):
188
+ if isinstance(node, ast.FunctionDef) and node.name == func_name:
189
+ source_lines = source_content.split("\n")
190
+ start_line = node.lineno
191
+ end_line = (
192
+ node.end_lineno
193
+ if hasattr(node, "end_lineno")
194
+ else len(source_lines)
195
+ )
196
+ body_lines = source_lines[start_line:end_line]
197
+ func_body = textwrap.dedent("\n".join(body_lines))
198
+ break
199
+
200
+ if func_body:
201
+ if "parameters" not in step_dict:
202
+ step_dict["parameters"] = {}
203
+
204
+ existing_code_files = step_dict["parameters"].get("code_files", [])
205
+ resolved_files = resolve_code_files_references(
206
+ existing_code_files, source_file_path
207
+ )
208
+ directory_files = get_all_python_files_in_directory(source_file_path)
209
+ resolved_files.extend(directory_files)
210
+
211
+ main_content = f"""# Function: {func_name}
212
+ import json
213
+ import sys
214
+ import os
215
+ from erdo.types import StepContext
216
+
217
+ def {func_name}(context):
218
+ \"\"\"Extracted function implementation.\"\"\"
219
+ {textwrap.indent(func_body, " ")}
220
+
221
+ if __name__ == "__main__":
222
+ parameters_json = os.environ.get('STEP_PARAMETERS', '{{}}')
223
+ parameters = json.loads(parameters_json)
224
+ secrets_json = os.environ.get('STEP_SECRETS', '{{}}')
225
+ secrets = json.loads(secrets_json)
226
+ context = StepContext(parameters=parameters, secrets=secrets)
227
+
228
+ try:
229
+ result = {func_name}(context)
230
+ if result:
231
+ print(json.dumps(result))
232
+ except Exception as e:
233
+ print(f"Error: {{e}}", file=sys.stderr)
234
+ sys.exit(1)
235
+ """
236
+
237
+ all_files = [{"filename": "main.py", "content": main_content}]
238
+ all_files.extend(resolved_files)
239
+
240
+ seen_filenames = set()
241
+ unique_files = []
242
+ for file_dict in all_files:
243
+ filename = file_dict.get("filename")
244
+ if filename and filename not in seen_filenames:
245
+ seen_filenames.add(filename)
246
+ unique_files.append(file_dict)
247
+
248
+ step_dict["parameters"]["code_files"] = unique_files
249
+
250
+ except Exception as e:
251
+ print(f"Warning: Failed to extract function {func_name}: {e}", file=sys.stderr)
252
+
253
+
254
+ def convert_step_to_step_with_handlers(
255
+ step_obj: Any, source_file_path: Optional[str] = None
256
+ ) -> Dict:
257
+ """Convert a Step object to StepWithHandlers format recursively."""
258
+ step_dict = step_obj.to_dict()
259
+ step_dict = transform_dict_recursively(step_dict)
260
+
261
+ if source_file_path and step_dict.get("parameters", {}).get("code_files"):
262
+ existing_code_files = step_dict["parameters"]["code_files"]
263
+ resolved_files = resolve_code_files_references(
264
+ existing_code_files, source_file_path
265
+ )
266
+ if resolved_files:
267
+ step_dict["parameters"]["code_files"] = resolved_files
268
+
269
+ if (
270
+ source_file_path
271
+ and hasattr(step_obj, "__name__")
272
+ and not any(
273
+ file_dict.get("content")
274
+ for file_dict in step_dict.get("parameters", {}).get("code_files", [])
275
+ )
276
+ ):
277
+ try:
278
+ extract_and_update_function_code(step_obj, step_dict, source_file_path)
279
+ except Exception:
280
+ pass
281
+
282
+ return convert_step_dict_to_step_with_handlers(step_dict)
283
+
284
+
285
+ def extract_action_result_schemas(module: Any) -> Dict:
286
+ """Extract action result schemas from parameter classes with _result attribute."""
287
+ result_schemas = {}
288
+
289
+ for name in dir(module):
290
+ obj = getattr(module, name)
291
+ if inspect.isclass(obj) and hasattr(obj, "_result"):
292
+ result_class = obj._result
293
+ if not inspect.isclass(result_class):
294
+ continue
295
+
296
+ action_name = None
297
+ if hasattr(obj, "model_fields"):
298
+ name_field = obj.model_fields.get("name")
299
+ if name_field and name_field.default:
300
+ action_name = name_field.default.split(".")[-1]
301
+ elif hasattr(obj, "__fields__"):
302
+ name_field = obj.__fields__.get("name")
303
+ if name_field and name_field.default:
304
+ action_name = name_field.default.split(".")[-1]
305
+
306
+ if action_name and result_class:
307
+ schema: Dict[str, Any] = {
308
+ "class_name": result_class.__name__,
309
+ "description": result_class.__doc__ or "",
310
+ "required_fields": [],
311
+ "optional_fields": [],
312
+ "field_types": {},
313
+ }
314
+
315
+ fields = {}
316
+ if hasattr(result_class, "model_fields"):
317
+ fields = result_class.model_fields
318
+ elif hasattr(result_class, "__fields__"):
319
+ fields = result_class.__fields__
320
+
321
+ for field_name, field_info in fields.items():
322
+ is_required = True
323
+ if hasattr(field_info, "is_required"):
324
+ is_required = field_info.is_required()
325
+ elif hasattr(field_info, "required"):
326
+ is_required = field_info.required
327
+ elif (
328
+ hasattr(field_info, "default")
329
+ and field_info.default is not None
330
+ ):
331
+ is_required = False
332
+
333
+ if is_required:
334
+ schema["required_fields"].append(field_name)
335
+ else:
336
+ schema["optional_fields"].append(field_name)
337
+
338
+ field_type = "any"
339
+ if hasattr(field_info, "annotation"):
340
+ annotation = field_info.annotation
341
+ if annotation == str:
342
+ field_type = "string"
343
+ elif annotation == int:
344
+ field_type = "number"
345
+ elif annotation == bool:
346
+ field_type = "boolean"
347
+ elif annotation == list:
348
+ field_type = "array"
349
+ elif annotation == dict:
350
+ field_type = "object"
351
+
352
+ schema["field_types"][field_name] = field_type
353
+
354
+ result_schemas[action_name] = schema
355
+
356
+ return result_schemas
357
+
358
+
359
+ def extract_single_agent_data(
360
+ agent: Any, file_path: str, module: Optional[Any] = None
361
+ ) -> Dict:
362
+ """Extract data from a single agent."""
363
+ steps = getattr(agent, "steps", [])
364
+ step_dicts = []
365
+
366
+ for step in steps:
367
+ step_with_handlers = convert_step_to_step_with_handlers(step, file_path)
368
+
369
+ if hasattr(step, "_module_files") and step._module_files:
370
+ step_dict = step_with_handlers["step"]
371
+ if "parameters" not in step_dict:
372
+ step_dict["parameters"] = {}
373
+
374
+ code_files = []
375
+ for fp, content in step._module_files.items():
376
+ code_files.append({"filename": fp, "content": content})
377
+
378
+ step_dict["parameters"]["code_files"] = code_files
379
+ if hasattr(step, "_entrypoint"):
380
+ step_dict["parameters"]["entrypoint"] = step._entrypoint
381
+
382
+ step_dicts.append(step_with_handlers)
383
+
384
+ action_result_schemas = {}
385
+ if module:
386
+ action_result_schemas = extract_action_result_schemas(module)
387
+
388
+ source_code = ""
389
+ if file_path and os.path.exists(file_path):
390
+ with open(file_path, "r") as f:
391
+ source_code = f.read()
392
+
393
+ return {
394
+ "bot": {
395
+ "name": agent.name,
396
+ "key": getattr(agent, "key", None), # Include the bot key
397
+ "description": agent.description or "",
398
+ "visibility": agent.visibility,
399
+ "persona": agent.persona,
400
+ "running_status": getattr(agent, "running_status", None),
401
+ "finished_status": getattr(agent, "finished_status", None),
402
+ "running_status_context": getattr(agent, "running_status_context", None),
403
+ "finished_status_context": getattr(agent, "finished_status_context", None),
404
+ "running_status_prompt": getattr(agent, "running_status_prompt", None),
405
+ "finished_status_prompt": getattr(agent, "finished_status_prompt", None),
406
+ "source": "python",
407
+ },
408
+ "parameter_definitions": agent.parameter_definitions or [],
409
+ "steps": step_dicts,
410
+ "file_path": file_path,
411
+ "source_code": source_code,
412
+ "action_result_schemas": action_result_schemas,
413
+ }
414
+
415
+
416
+ def extract_agent_from_instance(
417
+ agent: Any, source_file_path: Optional[str] = None
418
+ ) -> Dict:
419
+ """Extract agent data from an Agent instance."""
420
+ if source_file_path:
421
+ # Try to load the module to get action schemas
422
+ try:
423
+ spec = importlib.util.spec_from_file_location(
424
+ "agent_module", source_file_path
425
+ )
426
+ if not spec or not spec.loader:
427
+ raise ValueError(f"Could not load module spec from {source_file_path}")
428
+
429
+ module = importlib.util.module_from_spec(spec)
430
+ spec.loader.exec_module(module)
431
+ except Exception:
432
+ module = None
433
+ else:
434
+ module = None
435
+
436
+ return extract_single_agent_data(agent, source_file_path or "", module)
437
+
438
+
439
+ def extract_agents_from_file(file_path: str) -> Union[Dict, List[Dict]]:
440
+ """Extract agent(s) from a Python file."""
441
+ if not os.path.exists(file_path):
442
+ raise FileNotFoundError(f"File does not exist: {file_path}")
443
+
444
+ # Check if file has agents
445
+ with open(file_path, "r") as f:
446
+ source = f.read()
447
+
448
+ tree = ast.parse(source)
449
+ has_agents = False
450
+
451
+ for node in ast.walk(tree):
452
+ if isinstance(node, ast.Assign):
453
+ for target in node.targets:
454
+ if isinstance(target, ast.Name) and target.id == "agents":
455
+ has_agents = True
456
+ break
457
+
458
+ if not has_agents:
459
+ raise ValueError("No 'agents = [...]' assignment found in file")
460
+
461
+ # Load the module
462
+ file_dir = os.path.dirname(file_path)
463
+ if file_dir not in sys.path:
464
+ sys.path.insert(0, file_dir)
465
+
466
+ spec = importlib.util.spec_from_file_location("target_module", file_path)
467
+ if not spec or not spec.loader:
468
+ raise ValueError(f"Could not load module from {file_path}")
469
+
470
+ module = importlib.util.module_from_spec(spec)
471
+
472
+ with warnings.catch_warnings():
473
+ warnings.simplefilter("ignore")
474
+ spec.loader.exec_module(module)
475
+
476
+ if not hasattr(module, "agents"):
477
+ raise ValueError("No 'agents' list found in the module")
478
+
479
+ agents_list = getattr(module, "agents")
480
+ if not isinstance(agents_list, list) or len(agents_list) == 0:
481
+ raise ValueError("'agents' must be a non-empty list")
482
+
483
+ # Check if we're extracting all agents or just one
484
+ if len(agents_list) > 1 and file_path.endswith("__init__.py"):
485
+ result = []
486
+ for agent in agents_list:
487
+ agent_data = extract_single_agent_data(agent, file_path, module)
488
+ result.append(agent_data)
489
+ return result
490
+ else:
491
+ agent = agents_list[0]
492
+ return extract_single_agent_data(agent, file_path, module)