erdo 0.1.7__py3-none-any.whl → 0.1.9__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/invoke/invoke.py CHANGED
@@ -40,7 +40,7 @@ class Invoke:
40
40
  self,
41
41
  agent: Optional[Any] = None,
42
42
  parameters: Optional[Dict[str, Any]] = None,
43
- dataset_ids: Optional[list] = None,
43
+ dataset_slugs: Optional[list] = None,
44
44
  endpoint: Optional[str] = None,
45
45
  auth_token: Optional[str] = None,
46
46
  stream: bool = False,
@@ -51,7 +51,7 @@ class Invoke:
51
51
  Args:
52
52
  agent: Optional Agent instance to invoke immediately
53
53
  parameters: Parameters to pass to the agent
54
- dataset_ids: Dataset IDs to include
54
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
55
55
  endpoint: API endpoint URL
56
56
  auth_token: Authentication token
57
57
  stream: Whether to stream events
@@ -68,14 +68,17 @@ class Invoke:
68
68
  raise ValueError("Agent must have a 'key' attribute for invocation")
69
69
 
70
70
  self.result = self.invoke_by_key(
71
- bot_key, parameters=parameters, dataset_ids=dataset_ids, stream=stream
71
+ bot_key,
72
+ parameters=parameters,
73
+ dataset_slugs=dataset_slugs,
74
+ stream=stream,
72
75
  )
73
76
 
74
77
  def invoke_agent(
75
78
  self,
76
79
  agent: Any,
77
80
  parameters: Optional[Dict[str, Any]] = None,
78
- dataset_ids: Optional[list] = None,
81
+ dataset_slugs: Optional[list] = None,
79
82
  stream: bool = False,
80
83
  ) -> InvokeResult:
81
84
  """Invoke an agent instance.
@@ -83,7 +86,7 @@ class Invoke:
83
86
  Args:
84
87
  agent: Agent instance with a 'key' attribute
85
88
  parameters: Parameters to pass to the agent
86
- dataset_ids: Dataset IDs to include
89
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
87
90
  stream: Whether to stream events
88
91
 
89
92
  Returns:
@@ -93,75 +96,222 @@ class Invoke:
93
96
  if not bot_key:
94
97
  raise ValueError("Agent must have a 'key' attribute for invocation")
95
98
 
96
- return self.invoke_by_key(bot_key, parameters, dataset_ids, stream)
99
+ return self.invoke_by_key(
100
+ bot_key, parameters=parameters, dataset_slugs=dataset_slugs, stream=stream
101
+ )
97
102
 
98
103
  def invoke_by_key(
99
104
  self,
100
105
  bot_key: str,
106
+ messages: Optional[List[Dict[str, str]]] = None,
101
107
  parameters: Optional[Dict[str, Any]] = None,
102
- dataset_ids: Optional[list] = None,
108
+ dataset_slugs: Optional[list] = None,
109
+ mode: Optional[str] = None,
103
110
  stream: bool = False,
111
+ output_format: str = "events",
112
+ verbose: bool = False,
104
113
  ) -> InvokeResult:
105
114
  """Invoke a bot by its key.
106
115
 
107
116
  Args:
108
117
  bot_key: Bot key (e.g., "erdo.data-analyzer")
118
+ messages: Messages in format [{"role": "user", "content": "..."}]
109
119
  parameters: Parameters to pass to the bot
110
- dataset_ids: Dataset IDs to include
120
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
121
+ mode: Invocation mode: "live" (default), "replay" (cached), or "mock" (synthetic)
111
122
  stream: Whether to stream events
123
+ output_format: Output format: "events" (raw), "text" (formatted), "json" (summary)
124
+ verbose: Show detailed steps (only for text format)
112
125
 
113
126
  Returns:
114
127
  InvokeResult with the outcome
115
128
  """
116
129
  try:
117
130
  response = self.client.invoke_bot(
118
- bot_key, parameters=parameters, dataset_ids=dataset_ids, stream=stream
131
+ bot_key,
132
+ messages=messages,
133
+ parameters=parameters,
134
+ dataset_slugs=dataset_slugs,
135
+ mode=mode,
136
+ stream=stream,
119
137
  )
120
138
 
121
139
  if stream:
122
140
  # Process SSE events
123
141
  events = []
124
- final_result = None
125
142
  invocation_id = None
126
143
 
127
144
  # Type guard: response should be SSEClient when stream=True
128
145
  if not isinstance(response, dict):
129
- for event in response.events():
130
- events.append(event)
131
-
132
- if self.print_events:
133
- self._print_event(event)
134
-
135
- # Extract invocation ID from events
136
- if "invocation_id" in event:
137
- invocation_id = event["invocation_id"]
138
-
139
- # Check for final result
140
- if event.get("type") == "invocation_completed":
141
- final_result = event.get("result")
142
- elif event.get("type") == "result":
143
- final_result = event.get("data")
146
+ # For formatted output, print as we stream
147
+ if output_format in ["text", "json"] and not self.print_events:
148
+ from ..formatting import parse_invocation_events
149
+
150
+ bot_name_printed = False
151
+ completed_steps = set()
152
+
153
+ for event in response.events():
154
+ events.append(event)
155
+
156
+ # Extract invocation ID and bot name from payload
157
+ payload = event.get("payload", {})
158
+ if isinstance(payload, dict):
159
+ if "invocation_id" in payload and not invocation_id:
160
+ invocation_id = payload["invocation_id"]
161
+ if "bot_name" in payload and not bot_name_printed:
162
+ bot_name = payload["bot_name"]
163
+ if output_format == "text":
164
+ print(f"Bot: {bot_name}")
165
+ print(
166
+ f"Invocation ID: {invocation_id or 'N/A'}"
167
+ )
168
+ if verbose:
169
+ print("\nSteps:")
170
+ bot_name_printed = True
171
+
172
+ # Print steps as they complete (verbose mode)
173
+ if (
174
+ verbose
175
+ and output_format == "text"
176
+ and payload.get("status") == "step finished"
177
+ ):
178
+ # Parse to get step info
179
+ summary = parse_invocation_events(
180
+ events, bot_key, invocation_id
181
+ )
182
+ for step in summary.steps:
183
+ if step.key and step.key not in completed_steps:
184
+ print(f" ✓ {step.key} ({step.action})")
185
+ completed_steps.add(step.key)
186
+
187
+ # Print message content as it streams
188
+ if (
189
+ output_format == "text"
190
+ and isinstance(payload, str)
191
+ and len(payload) > 0
192
+ ):
193
+ # Check if this is actual content (not JSON)
194
+ if not payload.startswith(
195
+ "{"
196
+ ) and not payload.startswith("["):
197
+ print(payload, end="", flush=True)
198
+ else:
199
+ # For raw events or print_events mode, just collect
200
+ for event in response.events():
201
+ events.append(event)
202
+
203
+ if self.print_events:
204
+ self._print_event(event)
205
+
206
+ # Extract invocation ID from events
207
+ if "invocation_id" in event:
208
+ invocation_id = event["invocation_id"]
209
+
210
+ # Format final result based on output_format
211
+ formatted_result = self._format_result(
212
+ events, bot_key, invocation_id, output_format, verbose
213
+ )
144
214
 
145
215
  return InvokeResult(
146
216
  success=True,
147
217
  bot_id=bot_key,
148
218
  invocation_id=invocation_id,
149
- result=final_result,
219
+ result=formatted_result,
150
220
  events=events,
151
221
  )
152
222
  else:
153
- # Non-streaming response - response is a dict
223
+ # Non-streaming response - response is a dict with 'events' key
154
224
  response_dict = response if isinstance(response, dict) else {}
225
+ # Extract the events list from the response
226
+ events = response_dict.get("events", [])
227
+
228
+ # Format result based on output_format
229
+ formatted_result = self._format_result(
230
+ events, bot_key, None, output_format, verbose
231
+ )
232
+
155
233
  return InvokeResult(
156
234
  success=True,
157
235
  bot_id=bot_key,
158
- result=response_dict,
159
- events=[response_dict],
236
+ result=formatted_result,
237
+ events=events,
160
238
  )
161
239
 
162
240
  except Exception as e:
163
241
  return InvokeResult(success=False, bot_id=bot_key, error=str(e))
164
242
 
243
+ def _format_result(
244
+ self,
245
+ events: List[Dict[str, Any]],
246
+ bot_key: str,
247
+ invocation_id: Optional[str],
248
+ output_format: str,
249
+ verbose: bool,
250
+ ) -> Any:
251
+ """Format the result based on output_format parameter.
252
+
253
+ Args:
254
+ events: List of events from the invocation
255
+ bot_key: Bot key
256
+ invocation_id: Invocation ID
257
+ output_format: "events", "text", or "json"
258
+ verbose: Show detailed steps (for text format)
259
+
260
+ Returns:
261
+ Formatted result based on output_format
262
+ """
263
+ if output_format == "events":
264
+ # Return raw events (backwards compatible)
265
+ return {"events": events}
266
+
267
+ # Import formatting helpers
268
+ from ..formatting import parse_invocation_events
269
+
270
+ # Parse events into structured summary
271
+ summary = parse_invocation_events(events, bot_key, invocation_id)
272
+
273
+ if output_format == "text":
274
+ # Format as human-readable text
275
+ lines = []
276
+ lines.append(f"Bot: {summary.bot_name or summary.bot_key or 'unknown'}")
277
+ lines.append(f"Invocation ID: {summary.invocation_id or 'N/A'}")
278
+
279
+ if verbose and summary.steps:
280
+ lines.append("")
281
+ lines.append("Steps:")
282
+ for step in summary.steps:
283
+ status_icon = "✓" if step.status == "completed" else "•"
284
+ lines.append(f" {status_icon} {step.key} ({step.action})")
285
+
286
+ if summary.result:
287
+ lines.append("")
288
+ lines.append("Result:")
289
+ try:
290
+ parsed = json.loads(summary.result)
291
+ lines.append(json.dumps(parsed, indent=2))
292
+ except (json.JSONDecodeError, TypeError):
293
+ lines.append(summary.result)
294
+
295
+ return "\n".join(lines)
296
+
297
+ elif output_format == "json":
298
+ # Return structured summary
299
+ return {
300
+ "bot_name": summary.bot_name,
301
+ "bot_key": summary.bot_key,
302
+ "invocation_id": summary.invocation_id,
303
+ "steps": [
304
+ {"key": s.key, "action": s.action, "status": s.status}
305
+ for s in summary.steps
306
+ ],
307
+ "result": summary.result,
308
+ "success": summary.success,
309
+ "error": summary.error,
310
+ }
311
+
312
+ # Default: return events
313
+ return {"events": events}
314
+
165
315
  def _print_event(self, event: Dict[str, Any]):
166
316
  """Print an event in a readable format."""
167
317
  event_type = event.get("type", "unknown")
@@ -186,6 +336,71 @@ class Invoke:
186
336
 
187
337
 
188
338
  # Convenience functions
339
+ def invoke(
340
+ bot_key: str,
341
+ messages: Optional[List[Dict[str, str]]] = None,
342
+ parameters: Optional[Dict[str, Any]] = None,
343
+ datasets: Optional[list] = None,
344
+ mode: Optional[str] = None,
345
+ stream: bool = False,
346
+ output_format: str = "events",
347
+ verbose: bool = False,
348
+ print_events: bool = False,
349
+ **kwargs,
350
+ ) -> InvokeResult:
351
+ """Invoke a bot with a clean API.
352
+
353
+ Args:
354
+ bot_key: Bot key (e.g., "erdo.data-analyzer")
355
+ messages: Messages in format [{"role": "user", "content": "..."}]
356
+ parameters: Parameters to pass to the bot
357
+ datasets: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
358
+ mode: Invocation mode: "live" (default), "replay" (cached), or "mock" (synthetic)
359
+ stream: Whether to stream events
360
+ output_format: Output format: "events" (raw), "text" (formatted), "json" (summary)
361
+ verbose: Show detailed steps (only for text format)
362
+ print_events: Whether to print events
363
+ **kwargs: Additional arguments (endpoint, auth_token)
364
+
365
+ Returns:
366
+ InvokeResult with formatted result in response.result
367
+
368
+ Example:
369
+ >>> from erdo import invoke
370
+ >>>
371
+ >>> # Default: raw events (backwards compatible)
372
+ >>> response = invoke("my_agent", messages=[...])
373
+ >>> print(response.result) # {"events": [...]}
374
+ >>>
375
+ >>> # Formatted text output
376
+ >>> response = invoke("my_agent", messages=[...], output_format="text")
377
+ >>> print(response.result)
378
+ Bot: my agent
379
+ Invocation ID: abc-123
380
+ Result:
381
+ The answer is 4
382
+ >>>
383
+ >>> # Formatted text with verbose steps
384
+ >>> response = invoke("my_agent", messages=[...], output_format="text", verbose=True)
385
+ >>>
386
+ >>> # JSON summary
387
+ >>> response = invoke("my_agent", messages=[...], output_format="json")
388
+ >>> print(response.result) # {"bot_name": ..., "steps": [...], "result": ...}
389
+ """
390
+ return invoke_by_key(
391
+ bot_key=bot_key,
392
+ messages=messages,
393
+ parameters=parameters,
394
+ dataset_slugs=datasets,
395
+ mode=mode,
396
+ stream=stream,
397
+ output_format=output_format,
398
+ verbose=verbose,
399
+ print_events=print_events,
400
+ **kwargs,
401
+ )
402
+
403
+
189
404
  def invoke_agent(
190
405
  agent: Any,
191
406
  parameters: Optional[Dict[str, Any]] = None,
@@ -217,9 +432,13 @@ def invoke_agent(
217
432
 
218
433
  def invoke_by_key(
219
434
  bot_key: str,
435
+ messages: Optional[List[Dict[str, str]]] = None,
220
436
  parameters: Optional[Dict[str, Any]] = None,
221
437
  dataset_ids: Optional[list] = None,
438
+ mode: Optional[str] = None,
222
439
  stream: bool = False,
440
+ output_format: str = "events",
441
+ verbose: bool = False,
223
442
  print_events: bool = False,
224
443
  **kwargs,
225
444
  ) -> InvokeResult:
@@ -227,9 +446,13 @@ def invoke_by_key(
227
446
 
228
447
  Args:
229
448
  bot_key: Bot key (e.g., "erdo.data-analyzer")
449
+ messages: Messages in format [{"role": "user", "content": "..."}]
230
450
  parameters: Parameters to pass to the bot
231
451
  dataset_ids: Dataset IDs to include
452
+ mode: Invocation mode: "live" (default), "replay" (cached), or "mock" (synthetic)
232
453
  stream: Whether to stream events
454
+ output_format: Output format: "events" (raw), "text" (formatted), "json" (summary)
455
+ verbose: Show detailed steps (only for text format)
233
456
  print_events: Whether to print events
234
457
  **kwargs: Additional arguments (endpoint, auth_token)
235
458
 
@@ -241,4 +464,6 @@ def invoke_by_key(
241
464
  auth_token=kwargs.get("auth_token"),
242
465
  print_events=print_events,
243
466
  )
244
- return invoke.invoke_by_key(bot_key, parameters, dataset_ids, stream)
467
+ return invoke.invoke_by_key(
468
+ bot_key, messages, parameters, dataset_ids, mode, stream, output_format, verbose
469
+ )
erdo/sync/extractor.py CHANGED
@@ -70,6 +70,12 @@ def transform_dict_recursively(obj: Any) -> Any:
70
70
  return [transform_dict_recursively(item) for item in obj]
71
71
  elif isinstance(obj, str):
72
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
73
79
  else:
74
80
  return obj
75
81
 
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
+ ]