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/invoke/invoke.py ADDED
@@ -0,0 +1,555 @@
1
+ """Main invoke functionality for running agents via the orchestrator."""
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from erdo._generated.types import Result
9
+
10
+ from .client import InvokeClient
11
+
12
+
13
+ @dataclass
14
+ class InvokeResult:
15
+ """Result from a bot invocation.
16
+
17
+ Follows the executor pattern with clean separation:
18
+ - .result: The actual bot output (text/data from messages)
19
+ - .messages: List of message objects with role/content
20
+ - .events: Full list of all events (for debugging/analysis)
21
+ - .steps: Information about executed steps
22
+ """
23
+
24
+ success: bool
25
+ bot_id: Optional[str] = None
26
+ invocation_id: Optional[str] = None
27
+ result: Optional[Result] = None # The actual bot result (types.Result object)
28
+ messages: List[Dict[str, Any]] = field(default_factory=list) # Message objects
29
+ events: List[Dict[str, Any]] = field(default_factory=list) # All raw events
30
+ steps: List[Dict[str, Any]] = field(default_factory=list) # Step execution info
31
+ error: Optional[str] = None
32
+
33
+ def __str__(self) -> str:
34
+ if self.success:
35
+ result_preview = str(self.result)[:100] if self.result else "No result"
36
+ return f"āœ… Success (ID: {self.invocation_id})\nResult: {result_preview}"
37
+ else:
38
+ return f"āŒ Failed: {self.error}"
39
+
40
+
41
+ class Invoke:
42
+ """Main class for invoking agents."""
43
+
44
+ def __init__(
45
+ self,
46
+ agent: Optional[Any] = None,
47
+ parameters: Optional[Dict[str, Any]] = None,
48
+ dataset_slugs: Optional[list] = None,
49
+ endpoint: Optional[str] = None,
50
+ auth_token: Optional[str] = None,
51
+ stream: bool = False,
52
+ print_events: bool = False,
53
+ ):
54
+ """Initialize and optionally invoke an agent immediately.
55
+
56
+ Args:
57
+ agent: Optional Agent instance to invoke immediately
58
+ parameters: Parameters to pass to the agent
59
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
60
+ endpoint: API endpoint URL
61
+ auth_token: Authentication token
62
+ stream: Whether to stream events
63
+ print_events: Whether to print events as they arrive
64
+ """
65
+ self.client = InvokeClient(endpoint=endpoint, auth_token=auth_token)
66
+ self.print_events = print_events
67
+ self.result = None
68
+
69
+ # If an agent is provided, invoke it immediately
70
+ if agent:
71
+ bot_key = getattr(agent, "key", None)
72
+ if not bot_key:
73
+ raise ValueError("Agent must have a 'key' attribute for invocation")
74
+
75
+ self.result = self.invoke_by_key(
76
+ bot_key,
77
+ parameters=parameters,
78
+ dataset_slugs=dataset_slugs,
79
+ stream=stream,
80
+ )
81
+
82
+ def invoke_agent(
83
+ self,
84
+ agent: Any,
85
+ parameters: Optional[Dict[str, Any]] = None,
86
+ dataset_slugs: Optional[list] = None,
87
+ stream: bool = False,
88
+ ) -> InvokeResult:
89
+ """Invoke an agent instance.
90
+
91
+ Args:
92
+ agent: Agent instance with a 'key' attribute
93
+ parameters: Parameters to pass to the agent
94
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
95
+ stream: Whether to stream events
96
+
97
+ Returns:
98
+ InvokeResult with the outcome
99
+ """
100
+ bot_key = getattr(agent, "key", None)
101
+ if not bot_key:
102
+ raise ValueError("Agent must have a 'key' attribute for invocation")
103
+
104
+ return self.invoke_by_key(
105
+ bot_key, parameters=parameters, dataset_slugs=dataset_slugs, stream=stream
106
+ )
107
+
108
+ def invoke_by_key(
109
+ self,
110
+ bot_key: str,
111
+ messages: Optional[List[Dict[str, str]]] = None,
112
+ parameters: Optional[Dict[str, Any]] = None,
113
+ dataset_slugs: Optional[list] = None,
114
+ mode: Optional[Union[str, Dict[str, Any]]] = None,
115
+ manual_mocks: Optional[Dict[str, Dict[str, Any]]] = None,
116
+ stream: bool = False,
117
+ output_format: str = "events",
118
+ verbose: bool = False,
119
+ ) -> InvokeResult:
120
+ """Invoke a bot by its key.
121
+
122
+ Args:
123
+ bot_key: Bot key (e.g., "erdo.data-analyzer")
124
+ messages: Messages in format [{"role": "user", "content": "..."}]
125
+ parameters: Parameters to pass to the bot
126
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
127
+ mode: Invocation mode - string: "live|replay|manual" OR dict: {"mode": "replay", "refresh": true}
128
+ manual_mocks: Manual mock responses for mode="manual" (step_path -> mock response)
129
+ stream: Whether to stream events
130
+ output_format: Output format: "events" (raw), "text" (formatted), "json" (summary)
131
+ verbose: Show detailed steps (only for text format)
132
+
133
+ Returns:
134
+ InvokeResult with the outcome
135
+ """
136
+ try:
137
+ response = self.client.invoke_bot(
138
+ bot_key,
139
+ messages=messages,
140
+ parameters=parameters,
141
+ dataset_slugs=dataset_slugs,
142
+ mode=mode,
143
+ manual_mocks=manual_mocks,
144
+ stream=stream,
145
+ )
146
+
147
+ if stream:
148
+ # Process SSE events
149
+ events = []
150
+ invocation_id = None
151
+
152
+ # Type guard: response should be SSEClient when stream=True
153
+ if not isinstance(response, dict):
154
+ # For formatted output, print as we stream
155
+ if output_format in ["text", "json"] and not self.print_events:
156
+ bot_name_printed = False
157
+ completed_steps = set()
158
+ step_names = {} # Map to track step keys
159
+ printed_content_ids = (
160
+ set()
161
+ ) # Track which message content we've printed
162
+
163
+ for event in response.events():
164
+ if not event:
165
+ continue
166
+ events.append(event)
167
+
168
+ # Extract payload and metadata
169
+ payload = event.get("payload", {})
170
+ metadata = event.get("metadata", {})
171
+ status = (
172
+ payload.get("status")
173
+ if isinstance(payload, dict)
174
+ else None
175
+ )
176
+
177
+ # Extract invocation ID and bot name from payload
178
+ if isinstance(payload, dict):
179
+ if "invocation_id" in payload and not invocation_id:
180
+ invocation_id = payload["invocation_id"]
181
+ if "bot_name" in payload and not bot_name_printed:
182
+ bot_name = payload["bot_name"]
183
+ if output_format == "text":
184
+ print(f"Bot: {bot_name}")
185
+ print(
186
+ f"Invocation ID: {invocation_id or 'N/A'}"
187
+ )
188
+ print() # Empty line before steps start
189
+ bot_name_printed = True
190
+
191
+ # Track and display step info (ONLY in verbose mode)
192
+ # step_info events have both key and action_type in payload
193
+ if (
194
+ verbose
195
+ and output_format == "text"
196
+ and isinstance(payload, dict)
197
+ ):
198
+ if "key" in payload and "action_type" in payload:
199
+ # This is a step_info event - show "ā–ø" here if visible
200
+ step_key = payload["key"]
201
+ action_type = payload["action_type"]
202
+ step_names[step_key] = action_type
203
+
204
+ # Show step start on step_info event if visible
205
+ if metadata.get("user_visibility") == "visible":
206
+ print(f"ā–ø {step_key} ({action_type})")
207
+
208
+ # Handle step finished events (check status in payload)
209
+ # We look for the most recent step from step_names (ONLY in verbose mode)
210
+ if (
211
+ verbose
212
+ and output_format == "text"
213
+ and status == "step finished"
214
+ ):
215
+ if metadata.get("user_visibility") == "visible":
216
+ # Find the last step that we haven't marked complete yet
217
+ for step_key in reversed(list(step_names.keys())):
218
+ if step_key not in completed_steps:
219
+ print(f"āœ“ {step_key}")
220
+ completed_steps.add(step_key)
221
+ break
222
+
223
+ # Print message content as it streams
224
+ # ONLY print if:
225
+ # 1. user_visibility is "visible"
226
+ # 2. Has message_content_id (actual message content, not fragments)
227
+ # 3. content_type is "text" (not "json" or other types)
228
+ # 4. Haven't already printed chunks for this content_id
229
+ if (
230
+ output_format == "text"
231
+ and isinstance(payload, str)
232
+ and len(payload) > 0
233
+ and isinstance(metadata, dict)
234
+ and metadata.get("user_visibility") == "visible"
235
+ and metadata.get(
236
+ "message_content_id"
237
+ ) # Must be message content
238
+ and metadata.get("content_type")
239
+ == "text" # Only plain text, not JSON
240
+ ):
241
+ content_id = metadata.get("message_content_id")
242
+ # If this is a full message (long) and we've already printed chunks, skip
243
+ if (
244
+ len(payload) > 20
245
+ and content_id in printed_content_ids
246
+ ):
247
+ # This is a duplicate full message after streaming chunks
248
+ pass
249
+ else:
250
+ print(payload, end="", flush=True)
251
+ printed_content_ids.add(content_id)
252
+ else:
253
+ # For raw events or print_events mode, just collect
254
+ for event in response.events():
255
+ if not event:
256
+ continue
257
+ events.append(event)
258
+
259
+ if self.print_events:
260
+ self._print_event(event)
261
+
262
+ # Extract invocation ID from events
263
+ if "invocation_id" in event:
264
+ invocation_id = event["invocation_id"]
265
+
266
+ # Extract structured data from events
267
+ result_data = self._extract_result_data(events)
268
+
269
+ return InvokeResult(
270
+ success=True,
271
+ bot_id=bot_key,
272
+ invocation_id=invocation_id,
273
+ result=result_data["result"],
274
+ messages=result_data["messages"],
275
+ steps=result_data["steps"],
276
+ events=events,
277
+ )
278
+ else:
279
+ # Non-streaming response - response is a dict with 'events' key
280
+ response_dict = response if isinstance(response, dict) else {}
281
+ # Extract the events list from the response
282
+ events = response_dict.get("events", [])
283
+
284
+ # Extract structured data from events
285
+ result_data = self._extract_result_data(events)
286
+
287
+ return InvokeResult(
288
+ success=True,
289
+ bot_id=bot_key,
290
+ result=result_data["result"],
291
+ messages=result_data["messages"],
292
+ steps=result_data["steps"],
293
+ events=events,
294
+ )
295
+
296
+ except Exception as e:
297
+ return InvokeResult(success=False, bot_id=bot_key, error=str(e))
298
+
299
+ def _extract_result_data(self, events: List[Dict[str, Any]]) -> Dict[str, Any]:
300
+ """Extract structured data from events.
301
+
302
+ Args:
303
+ events: List of events from the invocation
304
+
305
+ Returns:
306
+ Dict with:
307
+ - result: The final Result object {status, parameters, output, message, error}
308
+ - messages: List of message objects {role, content}
309
+ - steps: List of step execution info {key, action, status}
310
+ """
311
+ messages = []
312
+ steps = []
313
+ steps_seen = set()
314
+ final_result = None
315
+
316
+ # Iterate through ALL events to collect messages and steps
317
+ for event in events:
318
+ if not event:
319
+ continue
320
+
321
+ payload = event.get("payload", {})
322
+ metadata = event.get("metadata", {})
323
+
324
+ # Handle None values explicitly
325
+ if payload is None:
326
+ payload = {}
327
+ if metadata is None:
328
+ metadata = {}
329
+
330
+ # Extract step information
331
+ if (
332
+ isinstance(payload, dict)
333
+ and "action_type" in payload
334
+ and "key" in payload
335
+ ):
336
+ step_key = payload["key"]
337
+ if step_key not in steps_seen:
338
+ steps_seen.add(step_key)
339
+ steps.append(
340
+ {
341
+ "key": step_key,
342
+ "action": payload["action_type"],
343
+ "status": "completed",
344
+ }
345
+ )
346
+
347
+ # Extract messages from ANY event with output.content
348
+ # This captures messages from all steps including sub-agents
349
+ # ONLY collect from output.content (final complete messages)
350
+ # NOT from streaming chunks (those have message_content_id)
351
+ if (
352
+ isinstance(payload, dict)
353
+ and "output" in payload
354
+ and metadata.get("user_visibility") == "visible"
355
+ ):
356
+ output = payload.get("output")
357
+ if isinstance(output, dict) and "content" in output:
358
+ content_array = output["content"]
359
+ if isinstance(content_array, list):
360
+ for item in content_array:
361
+ if (
362
+ isinstance(item, dict)
363
+ and item.get("content_type") == "text"
364
+ ):
365
+ # Extract as assistant message (or check for role in metadata)
366
+ role = metadata.get("role", "assistant")
367
+ messages.append(
368
+ {"role": role, "content": item.get("content", "")}
369
+ )
370
+
371
+ # Keep track of final result (last one with status + output)
372
+ if (
373
+ isinstance(payload, dict)
374
+ and "status" in payload
375
+ and "output" in payload
376
+ ):
377
+ # Create Result object from the payload
378
+ final_result = Result(
379
+ status=payload.get("status"),
380
+ parameters=payload.get("parameters"),
381
+ output=payload.get("output"),
382
+ message=payload.get("message"),
383
+ error=payload.get("error"),
384
+ )
385
+
386
+ return {
387
+ "result": final_result,
388
+ "messages": messages,
389
+ "steps": steps,
390
+ }
391
+
392
+ def _print_event(self, event: Dict[str, Any]):
393
+ """Print an event in a readable format."""
394
+ event_type = event.get("type", "unknown")
395
+
396
+ if event_type == "step_started":
397
+ step_name = event.get("step_name", "Unknown step")
398
+ print(f"šŸ”„ Step started: {step_name}")
399
+ elif event_type == "step_completed":
400
+ step_name = event.get("step_name", "Unknown step")
401
+ print(f"āœ… Step completed: {step_name}")
402
+ elif event_type == "llm_chunk":
403
+ content = event.get("content", "")
404
+ print(content, end="", flush=True)
405
+ elif event_type == "invocation_completed":
406
+ print("\n✨ Invocation completed")
407
+ elif event_type == "error":
408
+ error = event.get("error", "Unknown error")
409
+ print(f"āŒ Error: {error}")
410
+ else:
411
+ # Generic event printing
412
+ print(f"šŸ“” {event_type}: {json.dumps(event, indent=2)}")
413
+
414
+
415
+ # Convenience functions
416
+ def invoke(
417
+ bot_key: str,
418
+ messages: Optional[List[Dict[str, str]]] = None,
419
+ parameters: Optional[Dict[str, Any]] = None,
420
+ datasets: Optional[list] = None,
421
+ mode: Optional[Union[str, Dict[str, Any]]] = None,
422
+ manual_mocks: Optional[Dict[str, Dict[str, Any]]] = None,
423
+ stream: bool = False,
424
+ output_format: str = "events",
425
+ verbose: bool = False,
426
+ print_events: bool = False,
427
+ **kwargs,
428
+ ) -> InvokeResult:
429
+ """Invoke a bot with a clean API.
430
+
431
+ Args:
432
+ bot_key: Bot key (e.g., "erdo.data-analyzer")
433
+ messages: Messages in format [{"role": "user", "content": "..."}]
434
+ parameters: Parameters to pass to the bot
435
+ datasets: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
436
+ mode: Invocation mode - string: "live|replay|manual" OR dict: {"mode": "replay", "refresh": true}
437
+ manual_mocks: Manual mock responses for mode="manual" (step_path -> mock response)
438
+ stream: Whether to stream events
439
+ output_format: Output format: "events" (raw), "text" (formatted), "json" (summary)
440
+ verbose: Show detailed steps (only for text format)
441
+ print_events: Whether to print events
442
+ **kwargs: Additional arguments (endpoint, auth_token)
443
+
444
+ Returns:
445
+ InvokeResult with formatted result in response.result
446
+
447
+ Example:
448
+ >>> from erdo import invoke
449
+ >>>
450
+ >>> # Simple replay mode
451
+ >>> response = invoke("my_agent", messages=[...], mode="replay")
452
+ >>>
453
+ >>> # Replay mode with refresh (bypass cache)
454
+ >>> response = invoke("my_agent", messages=[...], mode={"mode": "replay", "refresh": True})
455
+ >>>
456
+ >>> # Manual mode with mocks
457
+ >>> response = invoke("my_agent", messages=[...], mode="manual",
458
+ ... manual_mocks={"llm.message": {"status": "success", "output": {"content": "Mocked"}}})
459
+ """
460
+ # Check ERDO_REFRESH environment variable
461
+ # If set and mode is "replay" (string), convert to dict with refresh=True
462
+ if os.environ.get("ERDO_REFRESH") == "1" and mode == "replay":
463
+ mode = {"mode": "replay", "refresh": True}
464
+
465
+ return invoke_by_key(
466
+ bot_key=bot_key,
467
+ messages=messages,
468
+ parameters=parameters,
469
+ dataset_slugs=datasets,
470
+ mode=mode,
471
+ manual_mocks=manual_mocks,
472
+ stream=stream,
473
+ output_format=output_format,
474
+ verbose=verbose,
475
+ print_events=print_events,
476
+ **kwargs,
477
+ )
478
+
479
+
480
+ def invoke_agent(
481
+ agent: Any,
482
+ parameters: Optional[Dict[str, Any]] = None,
483
+ dataset_slugs: Optional[list] = None,
484
+ stream: bool = False,
485
+ print_events: bool = False,
486
+ **kwargs,
487
+ ) -> InvokeResult:
488
+ """Invoke an agent instance.
489
+
490
+ Args:
491
+ agent: Agent instance with a 'key' attribute
492
+ parameters: Parameters to pass to the agent
493
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
494
+ stream: Whether to stream events
495
+ print_events: Whether to print events
496
+ **kwargs: Additional arguments (endpoint, auth_token)
497
+
498
+ Returns:
499
+ InvokeResult with the outcome
500
+ """
501
+ invoke = Invoke(
502
+ endpoint=kwargs.get("endpoint"),
503
+ auth_token=kwargs.get("auth_token"),
504
+ print_events=print_events,
505
+ )
506
+ return invoke.invoke_agent(agent, parameters, dataset_slugs, stream)
507
+
508
+
509
+ def invoke_by_key(
510
+ bot_key: str,
511
+ messages: Optional[List[Dict[str, str]]] = None,
512
+ parameters: Optional[Dict[str, Any]] = None,
513
+ dataset_slugs: Optional[list] = None,
514
+ mode: Optional[Union[str, Dict[str, Any]]] = None,
515
+ manual_mocks: Optional[Dict[str, Dict[str, Any]]] = None,
516
+ stream: bool = False,
517
+ output_format: str = "events",
518
+ verbose: bool = False,
519
+ print_events: bool = False,
520
+ **kwargs,
521
+ ) -> InvokeResult:
522
+ """Invoke a bot by its key.
523
+
524
+ Args:
525
+ bot_key: Bot key (e.g., "erdo.data-analyzer")
526
+ messages: Messages in format [{"role": "user", "content": "..."}]
527
+ parameters: Parameters to pass to the bot
528
+ dataset_slugs: Dataset slugs to include (e.g. ["my-dataset"] or ["org.my-dataset"])
529
+ mode: Invocation mode - string: "live|replay|manual" OR dict: {"mode": "replay", "refresh": true}
530
+ manual_mocks: Manual mock responses for mode="manual" (step_path -> mock response)
531
+ stream: Whether to stream events
532
+ output_format: Output format: "events" (raw), "text" (formatted), "json" (summary)
533
+ verbose: Show detailed steps (only for text format)
534
+ print_events: Whether to print events
535
+ **kwargs: Additional arguments (endpoint, auth_token)
536
+
537
+ Returns:
538
+ InvokeResult with the outcome
539
+ """
540
+ invoke = Invoke(
541
+ endpoint=kwargs.get("endpoint"),
542
+ auth_token=kwargs.get("auth_token"),
543
+ print_events=print_events,
544
+ )
545
+ return invoke.invoke_by_key(
546
+ bot_key,
547
+ messages,
548
+ parameters,
549
+ dataset_slugs,
550
+ mode,
551
+ manual_mocks,
552
+ stream,
553
+ output_format,
554
+ verbose,
555
+ )