erdo 0.1.8__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/__init__.py +5 -1
- erdo/config/__init__.py +2 -2
- erdo/formatting.py +279 -0
- erdo/invoke/__init__.py +2 -1
- erdo/invoke/client.py +23 -8
- erdo/invoke/invoke.py +255 -30
- erdo/test/__init__.py +41 -0
- erdo/test/evaluate.py +272 -0
- erdo/test/runner.py +263 -0
- erdo/types.py +0 -311
- {erdo-0.1.8.dist-info → erdo-0.1.9.dist-info}/METADATA +96 -9
- {erdo-0.1.8.dist-info → erdo-0.1.9.dist-info}/RECORD +15 -12
- erdo/py.typed +0 -1
- {erdo-0.1.8.dist-info → erdo-0.1.9.dist-info}/WHEEL +0 -0
- {erdo-0.1.8.dist-info → erdo-0.1.9.dist-info}/entry_points.txt +0 -0
- {erdo-0.1.8.dist-info → erdo-0.1.9.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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=
|
|
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=
|
|
159
|
-
events=
|
|
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(
|
|
467
|
+
return invoke.invoke_by_key(
|
|
468
|
+
bot_key, messages, parameters, dataset_ids, mode, stream, output_format, verbose
|
|
469
|
+
)
|
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
|
+
]
|