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/sync.py ADDED
@@ -0,0 +1,327 @@
1
+ """Main sync functionality for syncing agents to the backend."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from .client import SyncClient
8
+ from .extractor import (
9
+ extract_agent_from_instance,
10
+ extract_agents_from_file,
11
+ )
12
+
13
+
14
+ @dataclass
15
+ class SyncResult:
16
+ """Result of a sync operation."""
17
+
18
+ success: bool
19
+ bot_id: Optional[str] = None
20
+ bot_name: Optional[str] = None
21
+ error: Optional[str] = None
22
+
23
+ def __str__(self) -> str:
24
+ if self.success:
25
+ return f"✅ Successfully synced {self.bot_name} (ID: {self.bot_id})"
26
+ else:
27
+ return f"❌ Failed to sync {self.bot_name}: {self.error}"
28
+
29
+
30
+ class Sync:
31
+ """Main sync class for syncing agents."""
32
+
33
+ def __init__(
34
+ self,
35
+ agent: Optional[Any] = None,
36
+ endpoint: Optional[str] = None,
37
+ auth_token: Optional[str] = None,
38
+ ):
39
+ """Initialize sync and optionally sync an agent immediately.
40
+
41
+ Args:
42
+ agent: Optional Agent instance to sync immediately
43
+ endpoint: API endpoint URL. If not provided, uses config.
44
+ auth_token: Authentication token. If not provided, uses config.
45
+ """
46
+ self.client = SyncClient(endpoint=endpoint, auth_token=auth_token)
47
+ self.result = None
48
+
49
+ # If an agent is provided, sync it immediately
50
+ if agent:
51
+ self.result = self.sync_agent(agent)
52
+
53
+ def sync_agent(
54
+ self, agent: Any, source_file_path: Optional[str] = None
55
+ ) -> SyncResult:
56
+ """Sync a single agent to the backend.
57
+
58
+ Args:
59
+ agent: Agent instance to sync
60
+ source_file_path: Optional path to the source file for better extraction
61
+
62
+ Returns:
63
+ SyncResult with the outcome of the sync operation
64
+ """
65
+ try:
66
+ # Extract agent data
67
+ agent_data = extract_agent_from_instance(agent, source_file_path)
68
+
69
+ # Convert to API format
70
+ bot_request = self._convert_to_api_format(agent_data)
71
+
72
+ # Send to backend
73
+ bot_id = self.client.upsert_bot(bot_request)
74
+
75
+ return SyncResult(success=True, bot_id=bot_id, bot_name=agent.name)
76
+
77
+ except Exception as e:
78
+ return SyncResult(
79
+ success=False, bot_name=getattr(agent, "name", "Unknown"), error=str(e)
80
+ )
81
+
82
+ @classmethod
83
+ def from_file(
84
+ cls,
85
+ file_path: str,
86
+ endpoint: Optional[str] = None,
87
+ auth_token: Optional[str] = None,
88
+ ) -> List[SyncResult]:
89
+ """Sync agents from a Python file.
90
+
91
+ Args:
92
+ file_path: Path to the Python file containing agents
93
+ endpoint: API endpoint URL. If not provided, uses config.
94
+ auth_token: Authentication token. If not provided, uses config.
95
+
96
+ Returns:
97
+ List of SyncResult objects for each agent found
98
+ """
99
+ sync = cls(endpoint=endpoint, auth_token=auth_token)
100
+
101
+ try:
102
+ # Extract agents from file
103
+ agent_data = extract_agents_from_file(file_path)
104
+
105
+ # Handle single agent or list of agents
106
+ if isinstance(agent_data, list):
107
+ results = []
108
+ for data in agent_data:
109
+ result = sync._sync_agent_data(data)
110
+ results.append(result)
111
+ return results
112
+ else:
113
+ result = sync._sync_agent_data(agent_data)
114
+ return [result]
115
+
116
+ except Exception as e:
117
+ return [
118
+ SyncResult(
119
+ success=False, error=f"Failed to extract agents from file: {e}"
120
+ )
121
+ ]
122
+
123
+ @classmethod
124
+ def from_directory(
125
+ cls,
126
+ directory_path: str = ".",
127
+ endpoint: Optional[str] = None,
128
+ auth_token: Optional[str] = None,
129
+ ) -> List[SyncResult]:
130
+ """Sync all agents from a directory.
131
+
132
+ Args:
133
+ directory_path: Path to directory containing agent files (default: current directory)
134
+ endpoint: API endpoint URL. If not provided, uses config.
135
+ auth_token: Authentication token. If not provided, uses config.
136
+
137
+ Returns:
138
+ List of SyncResult objects for each agent found
139
+ """
140
+ results = []
141
+
142
+ directory = Path(directory_path)
143
+
144
+ # Check for __init__.py with agents first
145
+ init_file = directory / "__init__.py"
146
+ if init_file.exists():
147
+ try:
148
+ init_results = cls.from_file(
149
+ str(init_file), endpoint=endpoint, auth_token=auth_token
150
+ )
151
+ if init_results:
152
+ return init_results
153
+ except Exception:
154
+ pass # Fall back to directory scan
155
+
156
+ # Scan directory for agent files
157
+ for py_file in directory.glob("**/*.py"):
158
+ # Skip common non-agent files
159
+ if any(
160
+ skip in str(py_file)
161
+ for skip in ["__pycache__", "test_", "_test.py", ".venv", "venv"]
162
+ ):
163
+ continue
164
+
165
+ try:
166
+ # Check if file has agents
167
+ with open(py_file, "r") as f:
168
+ content = f.read()
169
+ if "agents = [" not in content:
170
+ continue
171
+
172
+ # Try to sync agents from this file
173
+ file_results = cls.from_file(
174
+ str(py_file), endpoint=endpoint, auth_token=auth_token
175
+ )
176
+ results.extend(file_results)
177
+
178
+ except Exception as e:
179
+ results.append(
180
+ SyncResult(success=False, error=f"Failed to process {py_file}: {e}")
181
+ )
182
+
183
+ return results
184
+
185
+ def _sync_agent_data(self, agent_data: Dict[str, Any]) -> SyncResult:
186
+ """Sync extracted agent data to the backend.
187
+
188
+ Args:
189
+ agent_data: Extracted agent data dictionary
190
+
191
+ Returns:
192
+ SyncResult with the outcome of the sync operation
193
+ """
194
+ try:
195
+ # Convert to API format
196
+ bot_request = self._convert_to_api_format(agent_data)
197
+
198
+ # Send to backend
199
+ bot_id = self.client.upsert_bot(bot_request)
200
+
201
+ return SyncResult(
202
+ success=True, bot_id=bot_id, bot_name=agent_data["bot"]["name"]
203
+ )
204
+
205
+ except Exception as e:
206
+ return SyncResult(
207
+ success=False,
208
+ bot_name=agent_data.get("bot", {}).get("name", "Unknown"),
209
+ error=str(e),
210
+ )
211
+
212
+ def _convert_to_api_format(self, agent_data: Dict[str, Any]) -> Dict[str, Any]:
213
+ """Convert extracted agent data to API format.
214
+
215
+ Args:
216
+ agent_data: Extracted agent data
217
+
218
+ Returns:
219
+ Bot request in API format
220
+ """
221
+ # Override source to "user" as per Go implementation
222
+ bot_data = agent_data["bot"].copy()
223
+ bot_data["source"] = "user"
224
+
225
+ # Convert steps to API format
226
+ api_steps = []
227
+ for step_with_handlers in agent_data.get("steps", []):
228
+ api_step = self._convert_step_to_api(step_with_handlers)
229
+ api_steps.append(api_step)
230
+
231
+ # Build the API request
232
+ # Convert parameter_definitions to dicts if they're objects
233
+ param_defs = agent_data.get("parameter_definitions", [])
234
+ if param_defs and hasattr(param_defs[0], "to_dict"):
235
+ param_defs = [
236
+ pd.to_dict() if hasattr(pd, "to_dict") else pd for pd in param_defs
237
+ ]
238
+
239
+ bot_request = {
240
+ "bot": bot_data,
241
+ "steps": api_steps,
242
+ "source": "user",
243
+ "parameter_definitions": param_defs,
244
+ }
245
+
246
+ # Include action result schemas if present
247
+ if "action_result_schemas" in agent_data:
248
+ bot_request["action_result_schemas"] = agent_data["action_result_schemas"]
249
+
250
+ return bot_request
251
+
252
+ def _convert_step_to_api(
253
+ self, step_with_handlers: Dict[str, Any]
254
+ ) -> Dict[str, Any]:
255
+ """Convert StepWithHandlers to API format.
256
+
257
+ Args:
258
+ step_with_handlers: Step with handlers dictionary
259
+
260
+ Returns:
261
+ Step in API format
262
+ """
263
+ # Ensure parameters is not None
264
+ step = step_with_handlers["step"].copy()
265
+ if step.get("parameters") is None:
266
+ step["parameters"] = {}
267
+
268
+ # Convert result handlers
269
+ api_handlers = []
270
+ for handler in step_with_handlers.get("result_handlers", []):
271
+ api_handler = handler.copy()
272
+
273
+ # Convert nested steps in handler
274
+ if "steps" in api_handler:
275
+ api_handler_steps = []
276
+ for nested_step in api_handler["steps"]:
277
+ api_nested = self._convert_step_to_api(nested_step)
278
+ api_handler_steps.append(api_nested)
279
+ api_handler["steps"] = api_handler_steps
280
+
281
+ api_handlers.append(api_handler)
282
+
283
+ return {"step": step, "result_handlers": api_handlers}
284
+
285
+
286
+ # Convenience functions
287
+ def sync_agent(
288
+ agent: Any, source_file_path: Optional[str] = None, **kwargs
289
+ ) -> SyncResult:
290
+ """Sync a single agent to the backend.
291
+
292
+ Args:
293
+ agent: Agent instance to sync
294
+ source_file_path: Optional path to the source file
295
+ **kwargs: Additional arguments (endpoint, auth_token)
296
+
297
+ Returns:
298
+ SyncResult with the outcome of the sync operation
299
+ """
300
+ sync = Sync(endpoint=kwargs.get("endpoint"), auth_token=kwargs.get("auth_token"))
301
+ return sync.sync_agent(agent, source_file_path)
302
+
303
+
304
+ def sync_agents_from_file(file_path: str, **kwargs) -> List[SyncResult]:
305
+ """Sync agents from a Python file.
306
+
307
+ Args:
308
+ file_path: Path to the Python file containing agents
309
+ **kwargs: Additional arguments (endpoint, auth_token)
310
+
311
+ Returns:
312
+ List of SyncResult objects
313
+ """
314
+ return Sync.from_file(file_path, **kwargs)
315
+
316
+
317
+ def sync_agents_from_directory(directory_path: str = ".", **kwargs) -> List[SyncResult]:
318
+ """Sync all agents from a directory.
319
+
320
+ Args:
321
+ directory_path: Path to directory containing agent files
322
+ **kwargs: Additional arguments (endpoint, auth_token)
323
+
324
+ Returns:
325
+ List of SyncResult objects
326
+ """
327
+ return Sync.from_directory(directory_path, **kwargs)
erdo/template.py ADDED
@@ -0,0 +1,136 @@
1
+ """
2
+ Template string handling for export/import roundtrip compatibility.
3
+
4
+ This module provides utilities for handling template strings during the
5
+ export/import roundtrip process. Template strings are Go template expressions
6
+ that can't be executed as Python, so they need special handling.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING, Union
10
+
11
+ if TYPE_CHECKING:
12
+ from erdo.types import Prompt
13
+
14
+
15
+ class TemplateString:
16
+ """
17
+ A wrapper class for template strings during export/import roundtrip.
18
+
19
+ This class represents a Go template string (like {{.Data.field}}) in a way
20
+ that can be executed as Python code and then converted back to the original
21
+ template string during import.
22
+
23
+ It implements various duck-typing methods to be compatible with Pydantic
24
+ validation while preserving the template content for later extraction.
25
+ """
26
+
27
+ template: str
28
+
29
+ def __init__(self, template: Union[str, "Prompt"]):
30
+ """
31
+ Initialize a TemplateString with the template content.
32
+
33
+ Args:
34
+ template: The template string content (e.g., "{{.Data.field}}") or a Prompt object
35
+ """
36
+ # Convert Prompt objects to strings, ensure template is always a string
37
+ if hasattr(template, "content"):
38
+ # This is a Prompt object
39
+ self.template = str(template)
40
+ else:
41
+ self.template = str(template)
42
+
43
+ def __str__(self) -> str:
44
+ """Return the template string for display purposes."""
45
+ return self.template
46
+
47
+ def __repr__(self) -> str:
48
+ """Return a representation of the TemplateString."""
49
+ return f"TemplateString({self.template!r})"
50
+
51
+ def __eq__(self, other: object) -> bool:
52
+ """Check equality with another TemplateString."""
53
+ if isinstance(other, TemplateString):
54
+ return self.template == other.template
55
+ return False
56
+
57
+ def __hash__(self) -> int:
58
+ """Make TemplateString hashable."""
59
+ return hash(self.template)
60
+
61
+ # Duck typing methods to make it behave like a string for Pydantic validation
62
+ def __len__(self) -> int:
63
+ """Return length to behave like a string."""
64
+ return len(self.template)
65
+
66
+ def __contains__(self, item) -> bool:
67
+ """Support 'in' operator to behave like a string."""
68
+ return item in self.template
69
+
70
+ def __getitem__(self, key):
71
+ """Support indexing to behave like a string."""
72
+ return self.template[key]
73
+
74
+ def __iter__(self):
75
+ """Support iteration to behave like a string."""
76
+ return iter(self.template)
77
+
78
+ # List-like methods for cases where template strings represent arrays
79
+ def __getstate__(self):
80
+ """Support pickling."""
81
+ return {"template": self.template}
82
+
83
+ def __setstate__(self, state):
84
+ """Support unpickling."""
85
+ self.template = state["template"]
86
+
87
+ # Additional methods to help with Pydantic validation
88
+ @classmethod
89
+ def __get_validators__(cls):
90
+ """Pydantic v1 compatibility."""
91
+ yield cls.validate
92
+
93
+ @classmethod
94
+ def validate(cls, v, *args, **kwargs):
95
+ """Validate that the value is a string, TemplateString, or basic type."""
96
+ if isinstance(v, cls):
97
+ return v
98
+ # Allow strings and basic types that can be converted to strings
99
+ if isinstance(v, (str, int, float, bool)):
100
+ return v
101
+ # Allow Prompt objects (they have __str__ method and content attribute)
102
+ if hasattr(v, "__str__") and hasattr(v, "content"):
103
+ # This is likely a Prompt object
104
+ return str(v)
105
+ # Reject functions, lambdas, and other complex types
106
+ if callable(v):
107
+ raise ValueError(
108
+ f"Template fields cannot accept callable objects like {type(v).__name__}"
109
+ )
110
+ # For other types, try to convert to string
111
+ try:
112
+ str(v)
113
+ return v
114
+ except Exception:
115
+ raise ValueError(
116
+ f"Template fields cannot accept objects of type {type(v).__name__}"
117
+ )
118
+
119
+ @classmethod
120
+ def __get_pydantic_core_schema__(cls, source_type, handler):
121
+ """Pydantic v2 compatibility."""
122
+ from pydantic_core import core_schema
123
+
124
+ return core_schema.no_info_plain_validator_function(cls.validate)
125
+
126
+ def to_template_string(self) -> str:
127
+ """
128
+ Convert back to the original template string format.
129
+
130
+ This is used during the import process to convert the TemplateString
131
+ object back to the raw template string.
132
+
133
+ Returns:
134
+ The original template string
135
+ """
136
+ return self.template
erdo/test/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """Test evaluation helpers for erdo agent testing.
2
+
3
+ This module provides helper functions for writing clean assertions when testing agents.
4
+ Use these in regular Python scripts - no pytest needed!
5
+
6
+ Example:
7
+ >>> from erdo import invoke
8
+ >>> from erdo.test import text_contains, json_path_equals
9
+ >>>
10
+ >>> # Test in a regular Python script
11
+ >>> response = invoke("my_agent", messages=[...], mode="replay")
12
+ >>> assert text_contains(response.result, "expected text")
13
+ >>> assert json_path_equals(response.result, "status", "success")
14
+ >>>
15
+ >>> # Or use via CLI
16
+ >>> # ./erdo invoke my_agent --message "test" --mode replay
17
+ """
18
+
19
+ from .evaluate import (
20
+ has_dataset,
21
+ json_path_equals,
22
+ json_path_exists,
23
+ text_contains,
24
+ text_equals,
25
+ text_matches,
26
+ )
27
+ from .runner import discover_tests
28
+ from .runner import main as run_tests
29
+ from .runner import run_tests_parallel
30
+
31
+ __all__ = [
32
+ "text_contains",
33
+ "text_equals",
34
+ "text_matches",
35
+ "json_path_equals",
36
+ "json_path_exists",
37
+ "has_dataset",
38
+ "run_tests",
39
+ "run_tests_parallel",
40
+ "discover_tests",
41
+ ]