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

erdo/sync/sync.py ADDED
@@ -0,0 +1,328 @@
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
+ sync = cls(endpoint=endpoint, auth_token=auth_token)
141
+ results = []
142
+
143
+ directory = Path(directory_path)
144
+
145
+ # Check for __init__.py with agents first
146
+ init_file = directory / "__init__.py"
147
+ if init_file.exists():
148
+ try:
149
+ init_results = cls.from_file(
150
+ str(init_file), endpoint=endpoint, auth_token=auth_token
151
+ )
152
+ if init_results:
153
+ return init_results
154
+ except:
155
+ pass # Fall back to directory scan
156
+
157
+ # Scan directory for agent files
158
+ for py_file in directory.glob("**/*.py"):
159
+ # Skip common non-agent files
160
+ if any(
161
+ skip in str(py_file)
162
+ for skip in ["__pycache__", "test_", "_test.py", ".venv", "venv"]
163
+ ):
164
+ continue
165
+
166
+ try:
167
+ # Check if file has agents
168
+ with open(py_file, "r") as f:
169
+ content = f.read()
170
+ if "agents = [" not in content:
171
+ continue
172
+
173
+ # Try to sync agents from this file
174
+ file_results = cls.from_file(
175
+ str(py_file), endpoint=endpoint, auth_token=auth_token
176
+ )
177
+ results.extend(file_results)
178
+
179
+ except Exception as e:
180
+ results.append(
181
+ SyncResult(success=False, error=f"Failed to process {py_file}: {e}")
182
+ )
183
+
184
+ return results
185
+
186
+ def _sync_agent_data(self, agent_data: Dict[str, Any]) -> SyncResult:
187
+ """Sync extracted agent data to the backend.
188
+
189
+ Args:
190
+ agent_data: Extracted agent data dictionary
191
+
192
+ Returns:
193
+ SyncResult with the outcome of the sync operation
194
+ """
195
+ try:
196
+ # Convert to API format
197
+ bot_request = self._convert_to_api_format(agent_data)
198
+
199
+ # Send to backend
200
+ bot_id = self.client.upsert_bot(bot_request)
201
+
202
+ return SyncResult(
203
+ success=True, bot_id=bot_id, bot_name=agent_data["bot"]["name"]
204
+ )
205
+
206
+ except Exception as e:
207
+ return SyncResult(
208
+ success=False,
209
+ bot_name=agent_data.get("bot", {}).get("name", "Unknown"),
210
+ error=str(e),
211
+ )
212
+
213
+ def _convert_to_api_format(self, agent_data: Dict[str, Any]) -> Dict[str, Any]:
214
+ """Convert extracted agent data to API format.
215
+
216
+ Args:
217
+ agent_data: Extracted agent data
218
+
219
+ Returns:
220
+ Bot request in API format
221
+ """
222
+ # Override source to "user" as per Go implementation
223
+ bot_data = agent_data["bot"].copy()
224
+ bot_data["source"] = "user"
225
+
226
+ # Convert steps to API format
227
+ api_steps = []
228
+ for step_with_handlers in agent_data.get("steps", []):
229
+ api_step = self._convert_step_to_api(step_with_handlers)
230
+ api_steps.append(api_step)
231
+
232
+ # Build the API request
233
+ # Convert parameter_definitions to dicts if they're objects
234
+ param_defs = agent_data.get("parameter_definitions", [])
235
+ if param_defs and hasattr(param_defs[0], "to_dict"):
236
+ param_defs = [
237
+ pd.to_dict() if hasattr(pd, "to_dict") else pd for pd in param_defs
238
+ ]
239
+
240
+ bot_request = {
241
+ "bot": bot_data,
242
+ "steps": api_steps,
243
+ "source": "user",
244
+ "parameter_definitions": param_defs,
245
+ }
246
+
247
+ # Include action result schemas if present
248
+ if "action_result_schemas" in agent_data:
249
+ bot_request["action_result_schemas"] = agent_data["action_result_schemas"]
250
+
251
+ return bot_request
252
+
253
+ def _convert_step_to_api(
254
+ self, step_with_handlers: Dict[str, Any]
255
+ ) -> Dict[str, Any]:
256
+ """Convert StepWithHandlers to API format.
257
+
258
+ Args:
259
+ step_with_handlers: Step with handlers dictionary
260
+
261
+ Returns:
262
+ Step in API format
263
+ """
264
+ # Ensure parameters is not None
265
+ step = step_with_handlers["step"].copy()
266
+ if step.get("parameters") is None:
267
+ step["parameters"] = {}
268
+
269
+ # Convert result handlers
270
+ api_handlers = []
271
+ for handler in step_with_handlers.get("result_handlers", []):
272
+ api_handler = handler.copy()
273
+
274
+ # Convert nested steps in handler
275
+ if "steps" in api_handler:
276
+ api_handler_steps = []
277
+ for nested_step in api_handler["steps"]:
278
+ api_nested = self._convert_step_to_api(nested_step)
279
+ api_handler_steps.append(api_nested)
280
+ api_handler["steps"] = api_handler_steps
281
+
282
+ api_handlers.append(api_handler)
283
+
284
+ return {"step": step, "result_handlers": api_handlers}
285
+
286
+
287
+ # Convenience functions
288
+ def sync_agent(
289
+ agent: Any, source_file_path: Optional[str] = None, **kwargs
290
+ ) -> SyncResult:
291
+ """Sync a single agent to the backend.
292
+
293
+ Args:
294
+ agent: Agent instance to sync
295
+ source_file_path: Optional path to the source file
296
+ **kwargs: Additional arguments (endpoint, auth_token)
297
+
298
+ Returns:
299
+ SyncResult with the outcome of the sync operation
300
+ """
301
+ sync = Sync(endpoint=kwargs.get("endpoint"), auth_token=kwargs.get("auth_token"))
302
+ return sync.sync_agent(agent, source_file_path)
303
+
304
+
305
+ def sync_agents_from_file(file_path: str, **kwargs) -> List[SyncResult]:
306
+ """Sync agents from a Python file.
307
+
308
+ Args:
309
+ file_path: Path to the Python file containing agents
310
+ **kwargs: Additional arguments (endpoint, auth_token)
311
+
312
+ Returns:
313
+ List of SyncResult objects
314
+ """
315
+ return Sync.from_file(file_path, **kwargs)
316
+
317
+
318
+ def sync_agents_from_directory(directory_path: str = ".", **kwargs) -> List[SyncResult]:
319
+ """Sync all agents from a directory.
320
+
321
+ Args:
322
+ directory_path: Path to directory containing agent files
323
+ **kwargs: Additional arguments (endpoint, auth_token)
324
+
325
+ Returns:
326
+ List of SyncResult objects
327
+ """
328
+ return Sync.from_directory(directory_path, **kwargs)