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.
- erdo/__init__.py +35 -0
- erdo/_generated/__init__.py +18 -0
- erdo/_generated/actions/__init__.py +34 -0
- erdo/_generated/actions/analysis.py +179 -0
- erdo/_generated/actions/bot.py +186 -0
- erdo/_generated/actions/codeexec.py +199 -0
- erdo/_generated/actions/llm.py +148 -0
- erdo/_generated/actions/memory.py +463 -0
- erdo/_generated/actions/pdfextractor.py +97 -0
- erdo/_generated/actions/resource_definitions.py +296 -0
- erdo/_generated/actions/sqlexec.py +90 -0
- erdo/_generated/actions/utils.py +475 -0
- erdo/_generated/actions/webparser.py +119 -0
- erdo/_generated/actions/websearch.py +85 -0
- erdo/_generated/condition/__init__.py +556 -0
- erdo/_generated/internal.py +51 -0
- erdo/_generated/internal_actions.py +91 -0
- erdo/_generated/parameters.py +17 -0
- erdo/_generated/secrets.py +17 -0
- erdo/_generated/template_functions.py +55 -0
- erdo/_generated/types.py +3907 -0
- erdo/actions/__init__.py +40 -0
- erdo/bot_permissions.py +266 -0
- erdo/cli_entry.py +73 -0
- erdo/conditions/__init__.py +11 -0
- erdo/config/__init__.py +5 -0
- erdo/config/config.py +140 -0
- erdo/formatting.py +279 -0
- erdo/install_cli.py +140 -0
- erdo/integrations.py +131 -0
- erdo/invoke/__init__.py +11 -0
- erdo/invoke/client.py +234 -0
- erdo/invoke/invoke.py +555 -0
- erdo/state.py +376 -0
- erdo/sync/__init__.py +17 -0
- erdo/sync/client.py +95 -0
- erdo/sync/extractor.py +492 -0
- erdo/sync/sync.py +327 -0
- erdo/template.py +136 -0
- erdo/test/__init__.py +41 -0
- erdo/test/evaluate.py +272 -0
- erdo/test/runner.py +263 -0
- erdo/types.py +1431 -0
- erdo-0.1.31.dist-info/METADATA +471 -0
- erdo-0.1.31.dist-info/RECORD +48 -0
- erdo-0.1.31.dist-info/WHEEL +4 -0
- erdo-0.1.31.dist-info/entry_points.txt +2 -0
- 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()
|