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.

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)