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/__init__.py +13 -13
- erdo/_generated/actions/__init__.py +3 -1
- erdo/_generated/actions/analysis.py +116 -4
- erdo/_generated/actions/bot.py +61 -5
- erdo/_generated/actions/codeexec.py +53 -28
- erdo/_generated/actions/llm.py +44 -5
- erdo/_generated/actions/memory.py +252 -57
- erdo/_generated/actions/pdfextractor.py +97 -0
- erdo/_generated/actions/resource_definitions.py +114 -12
- erdo/_generated/actions/sqlexec.py +86 -0
- erdo/_generated/actions/utils.py +178 -56
- erdo/_generated/actions/webparser.py +15 -5
- erdo/_generated/actions/websearch.py +15 -5
- erdo/_generated/condition/__init__.py +137 -127
- erdo/_generated/internal_actions.py +14 -2
- erdo/_generated/types.py +92 -48
- erdo/actions/__init__.py +8 -8
- erdo/bot_permissions.py +266 -0
- erdo/config/__init__.py +5 -0
- erdo/config/config.py +140 -0
- erdo/invoke/__init__.py +10 -0
- erdo/invoke/client.py +213 -0
- erdo/invoke/invoke.py +244 -0
- erdo/sync/__init__.py +17 -0
- erdo/sync/client.py +95 -0
- erdo/sync/extractor.py +482 -0
- erdo/sync/sync.py +327 -0
- erdo/types.py +516 -18
- {erdo-0.1.4.dist-info → erdo-0.1.6.dist-info}/METADATA +4 -1
- erdo-0.1.6.dist-info/RECORD +45 -0
- erdo-0.1.4.dist-info/RECORD +0 -33
- {erdo-0.1.4.dist-info → erdo-0.1.6.dist-info}/WHEEL +0 -0
- {erdo-0.1.4.dist-info → erdo-0.1.6.dist-info}/entry_points.txt +0 -0
- {erdo-0.1.4.dist-info → erdo-0.1.6.dist-info}/licenses/LICENSE +0 -0
erdo/types.py
CHANGED
|
@@ -10,7 +10,7 @@ from enum import Enum
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union, cast
|
|
12
12
|
|
|
13
|
-
from pydantic import BaseModel, Field
|
|
13
|
+
from pydantic import BaseModel, Field, field_serializer
|
|
14
14
|
|
|
15
15
|
from ._generated.types import (
|
|
16
16
|
ExecutionModeType,
|
|
@@ -21,6 +21,14 @@ from ._generated.types import (
|
|
|
21
21
|
ParameterHydrationBehaviour,
|
|
22
22
|
Tool,
|
|
23
23
|
)
|
|
24
|
+
from .bot_permissions import (
|
|
25
|
+
BotPermissions,
|
|
26
|
+
check_bot_access,
|
|
27
|
+
get_bot_permissions,
|
|
28
|
+
set_bot_org_permission,
|
|
29
|
+
set_bot_public,
|
|
30
|
+
set_bot_user_permission,
|
|
31
|
+
)
|
|
24
32
|
from .template import TemplateString
|
|
25
33
|
|
|
26
34
|
# Type aliases for complex types that are json.RawMessage in Go
|
|
@@ -99,8 +107,115 @@ class PythonFile(BaseModel):
|
|
|
99
107
|
raise RuntimeError(f"Error reading Python file {file_path}: {e}")
|
|
100
108
|
|
|
101
109
|
|
|
110
|
+
def _extract_step_config_from_action(action: Any) -> Dict[str, Any]:
|
|
111
|
+
"""Extract step configuration from an action object.
|
|
112
|
+
|
|
113
|
+
Actions can include a step_metadata parameter that configures the Step properties.
|
|
114
|
+
This function extracts those properties and returns them as a dict for Step(**config).
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
utils.echo(
|
|
118
|
+
data={"result": "value"},
|
|
119
|
+
step_metadata=StepMetadata(
|
|
120
|
+
key="my_step",
|
|
121
|
+
output_behavior={"result": OutputBehaviorType.MERGE}
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
The step_metadata fields are extracted and used to configure the Step:
|
|
126
|
+
- key: Step identifier
|
|
127
|
+
- output_behavior: How output fields are merged into state
|
|
128
|
+
- execution_mode: Parallel/sequential/background/iterate
|
|
129
|
+
- depends_on: Step dependencies
|
|
130
|
+
- output_channels: Where output is sent
|
|
131
|
+
- visibility settings: User and bot visibility
|
|
132
|
+
- messages: Running/finished messages
|
|
133
|
+
- content types: Output/history/UI content types
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
action: Action object (from utils.echo, llm.message, etc.)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dict with 'action' and any extracted step_metadata fields
|
|
140
|
+
"""
|
|
141
|
+
step_config: Dict[str, Any] = {"action": action}
|
|
142
|
+
|
|
143
|
+
# Extract step_metadata if present on the action object
|
|
144
|
+
if hasattr(action, "step_metadata") and action.step_metadata is not None:
|
|
145
|
+
metadata = action.step_metadata
|
|
146
|
+
|
|
147
|
+
# Extract each metadata field if it's set (not None)
|
|
148
|
+
if hasattr(metadata, "key") and metadata.key:
|
|
149
|
+
step_config["key"] = metadata.key
|
|
150
|
+
if hasattr(metadata, "depends_on") and metadata.depends_on:
|
|
151
|
+
step_config["depends_on"] = metadata.depends_on
|
|
152
|
+
if hasattr(metadata, "execution_mode") and metadata.execution_mode:
|
|
153
|
+
step_config["execution_mode"] = metadata.execution_mode
|
|
154
|
+
if hasattr(metadata, "output_behavior") and metadata.output_behavior:
|
|
155
|
+
step_config["output_behavior"] = metadata.output_behavior
|
|
156
|
+
if hasattr(metadata, "output_channels") and metadata.output_channels:
|
|
157
|
+
step_config["output_channels"] = metadata.output_channels
|
|
158
|
+
if hasattr(metadata, "output_content_type") and metadata.output_content_type:
|
|
159
|
+
step_config["output_content_type"] = metadata.output_content_type
|
|
160
|
+
if hasattr(metadata, "history_content_type") and metadata.history_content_type:
|
|
161
|
+
step_config["history_content_type"] = metadata.history_content_type
|
|
162
|
+
if hasattr(metadata, "ui_content_type") and metadata.ui_content_type:
|
|
163
|
+
step_config["ui_content_type"] = metadata.ui_content_type
|
|
164
|
+
if (
|
|
165
|
+
hasattr(metadata, "user_output_visibility")
|
|
166
|
+
and metadata.user_output_visibility
|
|
167
|
+
):
|
|
168
|
+
step_config["user_output_visibility"] = metadata.user_output_visibility
|
|
169
|
+
if (
|
|
170
|
+
hasattr(metadata, "bot_output_visibility")
|
|
171
|
+
and metadata.bot_output_visibility
|
|
172
|
+
):
|
|
173
|
+
step_config["bot_output_visibility"] = metadata.bot_output_visibility
|
|
174
|
+
if hasattr(metadata, "running_message") and metadata.running_message:
|
|
175
|
+
step_config["running_message"] = metadata.running_message
|
|
176
|
+
if hasattr(metadata, "finished_message") and metadata.finished_message:
|
|
177
|
+
step_config["finished_message"] = metadata.finished_message
|
|
178
|
+
if (
|
|
179
|
+
hasattr(metadata, "parameter_hydration_behaviour")
|
|
180
|
+
and metadata.parameter_hydration_behaviour
|
|
181
|
+
):
|
|
182
|
+
step_config["parameter_hydration_behaviour"] = (
|
|
183
|
+
metadata.parameter_hydration_behaviour
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return step_config
|
|
187
|
+
|
|
188
|
+
|
|
102
189
|
class StepMetadata(BaseModel):
|
|
103
|
-
"""Metadata for workflow steps, containing configuration and execution parameters.
|
|
190
|
+
"""Metadata for workflow steps, containing configuration and execution parameters.
|
|
191
|
+
|
|
192
|
+
StepMetadata is used to configure step properties when creating steps via actions.
|
|
193
|
+
It's particularly useful in result handlers where you need to configure step behavior:
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
step.on(IsSuccess(),
|
|
197
|
+
utils.parse_json(
|
|
198
|
+
json_data="{{output}}",
|
|
199
|
+
step_metadata=StepMetadata(
|
|
200
|
+
key="parse_result",
|
|
201
|
+
output_behavior={"data": OutputBehaviorType.MERGE}
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
Fields:
|
|
207
|
+
key: Step identifier (used for referencing in templates and dependencies)
|
|
208
|
+
output_behavior: Controls how output fields are merged into state
|
|
209
|
+
- STEP_ONLY: Output only available via steps.step_key.field
|
|
210
|
+
- MERGE: Output fields merged into root state
|
|
211
|
+
- OVERWRITE: Output replaces entire state
|
|
212
|
+
execution_mode: How the step executes (sequential/parallel/background/iterate)
|
|
213
|
+
depends_on: Other steps this step depends on
|
|
214
|
+
output_channels: Where step output is sent (e.g., ["user", "bot"])
|
|
215
|
+
visibility: Control who sees the output (user/bot)
|
|
216
|
+
messages: Custom running/finished messages
|
|
217
|
+
content_types: Specify output/history/UI content types
|
|
218
|
+
"""
|
|
104
219
|
|
|
105
220
|
model_config = {"arbitrary_types_allowed": True}
|
|
106
221
|
|
|
@@ -120,9 +235,7 @@ class StepMetadata(BaseModel):
|
|
|
120
235
|
bot_output_visibility: OutputVisibility = OutputVisibility.HIDDEN
|
|
121
236
|
running_message: Optional[str] = None
|
|
122
237
|
finished_message: Optional[str] = None
|
|
123
|
-
parameter_hydration_behaviour: ParameterHydrationBehaviour =
|
|
124
|
-
ParameterHydrationBehaviour.NONE
|
|
125
|
-
)
|
|
238
|
+
parameter_hydration_behaviour: Optional[ParameterHydrationBehaviour] = None
|
|
126
239
|
|
|
127
240
|
|
|
128
241
|
class Step(StepMetadata):
|
|
@@ -260,7 +373,8 @@ class Step(StepMetadata):
|
|
|
260
373
|
return str(type(self.action).__name__).lower()
|
|
261
374
|
|
|
262
375
|
def get_depends_on_keys(self) -> Optional[List[str]]:
|
|
263
|
-
"""Get dependency keys safely without circular references,
|
|
376
|
+
"""Get dependency keys safely without circular references,
|
|
377
|
+
preserving None vs [] distinction."""
|
|
264
378
|
# CRITICAL: Preserve None vs [] distinction for 100% roundtrip parity
|
|
265
379
|
if self.depends_on is None:
|
|
266
380
|
return None # Explicitly return None for null dependencies
|
|
@@ -341,8 +455,16 @@ class Step(StepMetadata):
|
|
|
341
455
|
# If it's already a Step, use step= parameter
|
|
342
456
|
step_actions.append(Step(step=action))
|
|
343
457
|
else:
|
|
344
|
-
#
|
|
345
|
-
|
|
458
|
+
# Extract step_metadata from action if present and merge into Step
|
|
459
|
+
# This allows result handler steps to be configured via step_metadata parameter:
|
|
460
|
+
# step.on(condition,
|
|
461
|
+
# utils.echo(data=..., step_metadata=StepMetadata(
|
|
462
|
+
# key="my_step",
|
|
463
|
+
# output_behavior={"field": OutputBehaviorType.MERGE}
|
|
464
|
+
# ))
|
|
465
|
+
# )
|
|
466
|
+
step_config = _extract_step_config_from_action(action)
|
|
467
|
+
step_actions.append(Step(**step_config))
|
|
346
468
|
|
|
347
469
|
# Create a result handler and add it to this step
|
|
348
470
|
handler = ResultHandler(
|
|
@@ -374,6 +496,8 @@ class Step(StepMetadata):
|
|
|
374
496
|
result.pop("ui_content_type", None)
|
|
375
497
|
if result.get("history_content_type") in [None, ""]:
|
|
376
498
|
result.pop("history_content_type", None)
|
|
499
|
+
if result.get("parameter_hydration_behaviour") is None:
|
|
500
|
+
result.pop("parameter_hydration_behaviour", None)
|
|
377
501
|
|
|
378
502
|
# Add action information without circular references
|
|
379
503
|
result["action_type"] = self.get_action_type()
|
|
@@ -461,6 +585,12 @@ class Step(StepMetadata):
|
|
|
461
585
|
for key, value in result.items():
|
|
462
586
|
result[key] = convert_conditions(value)
|
|
463
587
|
|
|
588
|
+
# Rename output_behavior to output_behaviour for Go backend compatibility
|
|
589
|
+
# Python SDK uses American spelling (output_behavior) but Go backend expects
|
|
590
|
+
# British spelling (output_behaviour). This ensures correct serialization.
|
|
591
|
+
if "output_behavior" in result:
|
|
592
|
+
result["output_behaviour"] = result.pop("output_behavior")
|
|
593
|
+
|
|
464
594
|
# Recursively sort all dictionaries for deterministic serialization
|
|
465
595
|
def sort_dict_recursively(obj: Any) -> Any:
|
|
466
596
|
"""Recursively sort all dictionaries to ensure deterministic JSON output."""
|
|
@@ -498,7 +628,8 @@ class Step(StepMetadata):
|
|
|
498
628
|
for condition, _ in self._decorator_handlers:
|
|
499
629
|
# Create a simple handler that calls the function
|
|
500
630
|
# For now, we'll create a basic handler structure
|
|
501
|
-
# In a full implementation, you'd want to analyze the function and create
|
|
631
|
+
# In a full implementation, you'd want to analyze the function and create
|
|
632
|
+
# appropriate steps
|
|
502
633
|
handler = ResultHandler(
|
|
503
634
|
type=HandlerType.FINAL,
|
|
504
635
|
if_conditions=condition,
|
|
@@ -588,9 +719,11 @@ class ResultHandler(BaseModel):
|
|
|
588
719
|
if hasattr(step, "to_dict"):
|
|
589
720
|
step_dict = step.to_dict()
|
|
590
721
|
# DO NOT auto-generate keys for result handler steps
|
|
591
|
-
# Only include keys if explicitly set to preserve roundtrip
|
|
722
|
+
# Only include keys if explicitly set to preserve roundtrip
|
|
723
|
+
# parity
|
|
592
724
|
|
|
593
|
-
# Ensure bot_output_visibility is preserved (defaults to hidden for
|
|
725
|
+
# Ensure bot_output_visibility is preserved (defaults to hidden for
|
|
726
|
+
# result handler steps)
|
|
594
727
|
if "bot_output_visibility" not in step_dict:
|
|
595
728
|
step_dict["bot_output_visibility"] = "hidden"
|
|
596
729
|
|
|
@@ -615,6 +748,8 @@ class ResultHandler(BaseModel):
|
|
|
615
748
|
result.pop("ui_content_type", None)
|
|
616
749
|
if result.get("history_content_type") in [None, ""]:
|
|
617
750
|
result.pop("history_content_type", None)
|
|
751
|
+
if result.get("parameter_hydration_behaviour") is None:
|
|
752
|
+
result.pop("parameter_hydration_behaviour", None)
|
|
618
753
|
|
|
619
754
|
# Convert condition objects to dictionaries if needed
|
|
620
755
|
if self.if_conditions is not None and hasattr(self.if_conditions, "to_dict"):
|
|
@@ -781,9 +916,10 @@ class Agent(BaseModel):
|
|
|
781
916
|
"""Main agent class for defining AI workflows."""
|
|
782
917
|
|
|
783
918
|
name: str
|
|
919
|
+
key: Optional[str] = None
|
|
784
920
|
description: Optional[str] = None
|
|
785
921
|
persona: Optional[str] = None
|
|
786
|
-
visibility: str = "
|
|
922
|
+
visibility: str = "public"
|
|
787
923
|
running_message: Optional[str] = None
|
|
788
924
|
finished_message: Optional[str] = None
|
|
789
925
|
version: str = "1.0"
|
|
@@ -811,8 +947,11 @@ class Agent(BaseModel):
|
|
|
811
947
|
Returns:
|
|
812
948
|
Step: The created step with sensible defaults
|
|
813
949
|
"""
|
|
814
|
-
#
|
|
815
|
-
|
|
950
|
+
# Extract step_metadata from the action if present
|
|
951
|
+
extracted_config = _extract_step_config_from_action(action)
|
|
952
|
+
|
|
953
|
+
# Auto-generate key if not provided (either via param or step_metadata)
|
|
954
|
+
if key is None and "key" not in extracted_config:
|
|
816
955
|
key = f"step_{len(self.steps) + 1}"
|
|
817
956
|
|
|
818
957
|
# Set sensible defaults
|
|
@@ -828,10 +967,11 @@ class Agent(BaseModel):
|
|
|
828
967
|
"user_output_visibility": OutputVisibility.VISIBLE,
|
|
829
968
|
"bot_output_visibility": OutputVisibility.HIDDEN,
|
|
830
969
|
"output_content_type": OutputContentType.TEXT,
|
|
831
|
-
"parameter_hydration_behaviour": ParameterHydrationBehaviour.NONE,
|
|
832
970
|
}
|
|
833
971
|
|
|
834
|
-
#
|
|
972
|
+
# First merge extracted_config (from step_metadata), then kwargs
|
|
973
|
+
# This allows kwargs to override step_metadata if needed
|
|
974
|
+
step_config.update(extracted_config)
|
|
835
975
|
step_config.update(kwargs)
|
|
836
976
|
|
|
837
977
|
step = Step(**step_config)
|
|
@@ -850,7 +990,8 @@ class Agent(BaseModel):
|
|
|
850
990
|
|
|
851
991
|
Args:
|
|
852
992
|
step_metadata: Step configuration (key, depends_on, etc.)
|
|
853
|
-
**codeexec_params: Parameters for codeexec.execute (entrypoint, parameters,
|
|
993
|
+
**codeexec_params: Parameters for codeexec.execute (entrypoint, parameters,
|
|
994
|
+
resources, etc.)
|
|
854
995
|
|
|
855
996
|
Returns:
|
|
856
997
|
Callable: Decorator function that creates and returns a Step
|
|
@@ -890,7 +1031,6 @@ class Agent(BaseModel):
|
|
|
890
1031
|
"user_output_visibility": OutputVisibility.VISIBLE,
|
|
891
1032
|
"bot_output_visibility": OutputVisibility.HIDDEN,
|
|
892
1033
|
"output_content_type": OutputContentType.TEXT,
|
|
893
|
-
"parameter_hydration_behaviour": ParameterHydrationBehaviour.NONE,
|
|
894
1034
|
}
|
|
895
1035
|
|
|
896
1036
|
# Remove None values and extract action separately
|
|
@@ -932,6 +1072,7 @@ class Agent(BaseModel):
|
|
|
932
1072
|
export_data = {
|
|
933
1073
|
"bot": {
|
|
934
1074
|
"Name": self.name,
|
|
1075
|
+
"Key": self.key,
|
|
935
1076
|
"Description": self.description or "",
|
|
936
1077
|
"Visibility": self.visibility,
|
|
937
1078
|
"Persona": self.persona, # Export as string or null, not object
|
|
@@ -1123,6 +1264,349 @@ class Prompt(BaseModel):
|
|
|
1123
1264
|
return prompts
|
|
1124
1265
|
|
|
1125
1266
|
|
|
1267
|
+
# Test system classes for agent testing
|
|
1268
|
+
class ConditionDefinition(BaseModel):
|
|
1269
|
+
"""Condition definition for test expectations."""
|
|
1270
|
+
|
|
1271
|
+
type: str # "TextContains", "IsSuccess", etc.
|
|
1272
|
+
path: str # JSONPath or template path to value
|
|
1273
|
+
parameters: Dict[str, Any] = Field(default_factory=dict)
|
|
1274
|
+
|
|
1275
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1276
|
+
"""Convert to dict format expected by backend."""
|
|
1277
|
+
return {"type": self.type, "path": self.path, "parameters": self.parameters}
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
class APIConditionDefinition(BaseModel):
|
|
1281
|
+
"""API condition definition that matches backend expectations."""
|
|
1282
|
+
|
|
1283
|
+
type: str # "TextContains", "IsSuccess", etc.
|
|
1284
|
+
conditions: List[Any] = Field(default_factory=list) # For composite conditions
|
|
1285
|
+
leaf: Dict[str, Any] = Field(default_factory=dict) # Parameters for leaf conditions
|
|
1286
|
+
|
|
1287
|
+
@field_serializer("leaf")
|
|
1288
|
+
def serialize_leaf(self, leaf_data: Dict[str, Any]) -> str:
|
|
1289
|
+
"""Serialize leaf field as JSON string to match Go backend json.RawMessage expectation."""
|
|
1290
|
+
import json
|
|
1291
|
+
|
|
1292
|
+
if leaf_data:
|
|
1293
|
+
# Convert TemplateString objects to strings
|
|
1294
|
+
converted_leaf = {}
|
|
1295
|
+
for key, value in leaf_data.items():
|
|
1296
|
+
if hasattr(value, "template"): # This is a TemplateString
|
|
1297
|
+
converted_leaf[key] = value.template
|
|
1298
|
+
else:
|
|
1299
|
+
converted_leaf[key] = value
|
|
1300
|
+
return json.dumps(converted_leaf)
|
|
1301
|
+
else:
|
|
1302
|
+
# Even if leaf is empty, ensure it's a JSON string
|
|
1303
|
+
return "{}"
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
class TestExpectation(BaseModel):
|
|
1307
|
+
"""Expected outcome for test validation."""
|
|
1308
|
+
|
|
1309
|
+
type: (
|
|
1310
|
+
str # "output_contains", "step_success", "invocation_status", "condition", etc.
|
|
1311
|
+
)
|
|
1312
|
+
step_key: Optional[str] = None
|
|
1313
|
+
field: Optional[str] = None
|
|
1314
|
+
operator: str # "contains", "equals", "greater_than", etc.
|
|
1315
|
+
value: Any
|
|
1316
|
+
description: str
|
|
1317
|
+
condition: Optional[Union[ConditionDefinition, APIConditionDefinition]] = None
|
|
1318
|
+
|
|
1319
|
+
@classmethod
|
|
1320
|
+
def output_contains(
|
|
1321
|
+
cls, step_key: str, text: str, description: Optional[str] = None
|
|
1322
|
+
) -> "TestExpectation":
|
|
1323
|
+
"""Expect step output to contain specific text."""
|
|
1324
|
+
return cls(
|
|
1325
|
+
type="output_contains",
|
|
1326
|
+
step_key=step_key,
|
|
1327
|
+
operator="contains",
|
|
1328
|
+
value=text,
|
|
1329
|
+
description=description
|
|
1330
|
+
or f"Step '{step_key}' output should contain '{text}'",
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
@classmethod
|
|
1334
|
+
def step_succeeds(
|
|
1335
|
+
cls, step_key: str, description: Optional[str] = None
|
|
1336
|
+
) -> "TestExpectation":
|
|
1337
|
+
"""Expect step to complete successfully."""
|
|
1338
|
+
return cls(
|
|
1339
|
+
type="step_success",
|
|
1340
|
+
step_key=step_key,
|
|
1341
|
+
operator="equals",
|
|
1342
|
+
value=True,
|
|
1343
|
+
description=description or f"Step '{step_key}' should succeed",
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
@classmethod
|
|
1347
|
+
def invocation_status(
|
|
1348
|
+
cls, status: str, description: Optional[str] = None
|
|
1349
|
+
) -> "TestExpectation":
|
|
1350
|
+
"""Expect overall invocation to have specific status."""
|
|
1351
|
+
return cls(
|
|
1352
|
+
type="invocation_status",
|
|
1353
|
+
operator="equals",
|
|
1354
|
+
value=status,
|
|
1355
|
+
description=description or f"Invocation should have status '{status}'",
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
@classmethod
|
|
1359
|
+
def from_condition_type(
|
|
1360
|
+
cls,
|
|
1361
|
+
condition_type: str,
|
|
1362
|
+
path: str,
|
|
1363
|
+
parameters: Dict[str, Any],
|
|
1364
|
+
description: Optional[str] = None,
|
|
1365
|
+
) -> "TestExpectation":
|
|
1366
|
+
"""Create a condition-based expectation using backend condition system."""
|
|
1367
|
+
return cls(
|
|
1368
|
+
type="condition",
|
|
1369
|
+
operator="evaluate",
|
|
1370
|
+
value="true",
|
|
1371
|
+
description=description
|
|
1372
|
+
or f"Condition {condition_type} should evaluate to true",
|
|
1373
|
+
condition=ConditionDefinition(
|
|
1374
|
+
type=condition_type, path=path, parameters=parameters
|
|
1375
|
+
),
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
@classmethod
|
|
1379
|
+
def text_contains(
|
|
1380
|
+
cls, path: str, text: str, description: Optional[str] = None
|
|
1381
|
+
) -> "TestExpectation":
|
|
1382
|
+
"""Expect value at path to contain specific text."""
|
|
1383
|
+
return cls(
|
|
1384
|
+
type="condition",
|
|
1385
|
+
operator="evaluate",
|
|
1386
|
+
value="true",
|
|
1387
|
+
description=description or f"Value at '{path}' should contain '{text}'",
|
|
1388
|
+
condition=ConditionDefinition(
|
|
1389
|
+
type="TextContains", path=path, parameters={"text": text}
|
|
1390
|
+
),
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
@classmethod
|
|
1394
|
+
def is_success(
|
|
1395
|
+
cls, path: str = "$.Status", description: Optional[str] = None
|
|
1396
|
+
) -> "TestExpectation":
|
|
1397
|
+
"""Expect value at path to indicate success."""
|
|
1398
|
+
return cls(
|
|
1399
|
+
type="condition",
|
|
1400
|
+
operator="evaluate",
|
|
1401
|
+
value="true",
|
|
1402
|
+
description=description or "Operation should succeed",
|
|
1403
|
+
condition=ConditionDefinition(type="IsSuccess", path=path, parameters={}),
|
|
1404
|
+
)
|
|
1405
|
+
|
|
1406
|
+
@classmethod
|
|
1407
|
+
def from_condition(
|
|
1408
|
+
cls, condition_obj: Any, description: Optional[str] = None
|
|
1409
|
+
) -> "TestExpectation":
|
|
1410
|
+
"""Create expectation from condition object with parameter hydration support."""
|
|
1411
|
+
# Convert condition object to dict format
|
|
1412
|
+
condition_dict = condition_obj.to_dict()
|
|
1413
|
+
condition_type = condition_dict.get("type", "Unknown")
|
|
1414
|
+
|
|
1415
|
+
# Extract leaf parameters from the condition object - these support template strings
|
|
1416
|
+
leaf_params = {}
|
|
1417
|
+
if "leaf" in condition_dict:
|
|
1418
|
+
leaf_params = condition_dict["leaf"]
|
|
1419
|
+
|
|
1420
|
+
# Create APIConditionDefinition object that matches backend expectations
|
|
1421
|
+
api_condition = APIConditionDefinition(
|
|
1422
|
+
type=condition_type,
|
|
1423
|
+
conditions=[], # Empty for leaf conditions
|
|
1424
|
+
leaf=leaf_params, # Parameters go in leaf field for backend
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
return cls(
|
|
1428
|
+
type="condition",
|
|
1429
|
+
operator="evaluate",
|
|
1430
|
+
value="true",
|
|
1431
|
+
description=description
|
|
1432
|
+
or f"Condition {condition_type} should evaluate to true",
|
|
1433
|
+
condition=api_condition, # Use APIConditionDefinition object
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
class TestMessage(BaseModel):
|
|
1438
|
+
"""Test message for agent input."""
|
|
1439
|
+
|
|
1440
|
+
role: str # "user" or "assistant"
|
|
1441
|
+
content: str
|
|
1442
|
+
|
|
1443
|
+
@classmethod
|
|
1444
|
+
def user(cls, content: str) -> "TestMessage":
|
|
1445
|
+
"""Create a user message."""
|
|
1446
|
+
return cls(role="user", content=content)
|
|
1447
|
+
|
|
1448
|
+
@classmethod
|
|
1449
|
+
def assistant(cls, content: str) -> "TestMessage":
|
|
1450
|
+
"""Create an assistant message."""
|
|
1451
|
+
return cls(role="assistant", content=content)
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
class Test(BaseModel):
|
|
1455
|
+
"""Test definition for agent validation."""
|
|
1456
|
+
|
|
1457
|
+
name: str
|
|
1458
|
+
description: str
|
|
1459
|
+
agent: Optional[Any] = None # Reference to agent being tested
|
|
1460
|
+
messages: List[TestMessage] = Field(default_factory=list)
|
|
1461
|
+
session_messages: List[TestMessage] = Field(default_factory=list)
|
|
1462
|
+
dataset_ids: List[str] = Field(
|
|
1463
|
+
default_factory=list
|
|
1464
|
+
) # Reference real datasets by ID
|
|
1465
|
+
parameters: Dict[str, str] = Field(default_factory=dict) # Bot parameters
|
|
1466
|
+
expectations: List[TestExpectation] = Field(default_factory=list)
|
|
1467
|
+
resume_from_state_id: Optional[str] = None
|
|
1468
|
+
test_suite: Optional[str] = None # Name of the test suite this test belongs to
|
|
1469
|
+
|
|
1470
|
+
def __init__(
|
|
1471
|
+
self,
|
|
1472
|
+
name: Optional[str] = None,
|
|
1473
|
+
description: str = "",
|
|
1474
|
+
agent: Optional[Any] = None,
|
|
1475
|
+
expect: Optional[List[TestExpectation]] = None,
|
|
1476
|
+
**kwargs: Any,
|
|
1477
|
+
):
|
|
1478
|
+
"""Initialize a Test with improved syntax support.
|
|
1479
|
+
|
|
1480
|
+
Args:
|
|
1481
|
+
name: Test name (auto-generated from agent name if not provided)
|
|
1482
|
+
description: Test description
|
|
1483
|
+
agent: Agent to test
|
|
1484
|
+
expect: List of expectations (alternative to using .expect() method)
|
|
1485
|
+
**kwargs: Additional test configuration
|
|
1486
|
+
"""
|
|
1487
|
+
# Auto-generate name from agent if not provided
|
|
1488
|
+
if name is None and agent is not None:
|
|
1489
|
+
agent_name = getattr(agent, "name", "agent")
|
|
1490
|
+
name = f"{agent_name}_test"
|
|
1491
|
+
elif name is None:
|
|
1492
|
+
name = "test"
|
|
1493
|
+
|
|
1494
|
+
# Set expectations from constructor if provided
|
|
1495
|
+
if expect is not None:
|
|
1496
|
+
kwargs["expectations"] = expect
|
|
1497
|
+
|
|
1498
|
+
super().__init__(name=name, description=description, agent=agent, **kwargs)
|
|
1499
|
+
|
|
1500
|
+
def message(self, role: str, content: str) -> "Test":
|
|
1501
|
+
"""Add a message to the test conversation."""
|
|
1502
|
+
self.messages.append(TestMessage(role=role, content=content))
|
|
1503
|
+
return self
|
|
1504
|
+
|
|
1505
|
+
def user_message(self, content: str) -> "Test":
|
|
1506
|
+
"""Add a user message."""
|
|
1507
|
+
return self.message("user", content)
|
|
1508
|
+
|
|
1509
|
+
def assistant_message(self, content: str) -> "Test":
|
|
1510
|
+
"""Add an assistant message."""
|
|
1511
|
+
return self.message("assistant", content)
|
|
1512
|
+
|
|
1513
|
+
def dataset(self, dataset_id: str) -> "Test":
|
|
1514
|
+
"""Add a dataset to the test by ID."""
|
|
1515
|
+
self.dataset_ids.append(dataset_id)
|
|
1516
|
+
return self
|
|
1517
|
+
|
|
1518
|
+
def expect(self, expectation: TestExpectation) -> "Test":
|
|
1519
|
+
"""Add an expectation to the test."""
|
|
1520
|
+
self.expectations.append(expectation)
|
|
1521
|
+
return self
|
|
1522
|
+
|
|
1523
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1524
|
+
"""Convert to dict format expected by backend."""
|
|
1525
|
+
return {
|
|
1526
|
+
"name": self.name,
|
|
1527
|
+
"description": self.description,
|
|
1528
|
+
"test_data": {
|
|
1529
|
+
"messages": [msg.model_dump() for msg in self.messages],
|
|
1530
|
+
"session_messages": [msg.model_dump() for msg in self.session_messages],
|
|
1531
|
+
"dataset_ids": self.dataset_ids,
|
|
1532
|
+
"parameters": self.parameters,
|
|
1533
|
+
"resume_from_state_id": self.resume_from_state_id,
|
|
1534
|
+
},
|
|
1535
|
+
"expectations": [exp.model_dump() for exp in self.expectations],
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def Expect(condition: Any, description: Optional[str] = None) -> TestExpectation:
|
|
1540
|
+
"""Create a test expectation from a condition object with clean syntax.
|
|
1541
|
+
|
|
1542
|
+
Usage:
|
|
1543
|
+
Expect(IsSuccess())
|
|
1544
|
+
Expect(TextContains(text=TemplateString("{{output.content}}"), value="success"))
|
|
1545
|
+
"""
|
|
1546
|
+
return TestExpectation.from_condition(condition, description)
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
class TestSuite(BaseModel):
|
|
1550
|
+
"""Test suite for grouping related tests together."""
|
|
1551
|
+
|
|
1552
|
+
name: str
|
|
1553
|
+
description: str
|
|
1554
|
+
tests: List[Test] = Field(default_factory=list)
|
|
1555
|
+
tags: List[str] = Field(default_factory=list)
|
|
1556
|
+
|
|
1557
|
+
def __init__(
|
|
1558
|
+
self,
|
|
1559
|
+
name: str,
|
|
1560
|
+
description: str = "",
|
|
1561
|
+
tests: Optional[List[Test]] = None,
|
|
1562
|
+
tags: Optional[List[str]] = None,
|
|
1563
|
+
**kwargs: Any,
|
|
1564
|
+
):
|
|
1565
|
+
"""Initialize a TestSuite.
|
|
1566
|
+
|
|
1567
|
+
Args:
|
|
1568
|
+
name: Suite name
|
|
1569
|
+
description: Suite description
|
|
1570
|
+
tests: List of tests to include in the suite
|
|
1571
|
+
tags: Tags for categorizing the test suite
|
|
1572
|
+
**kwargs: Additional suite configuration
|
|
1573
|
+
"""
|
|
1574
|
+
super().__init__(
|
|
1575
|
+
name=name,
|
|
1576
|
+
description=description,
|
|
1577
|
+
tests=tests or [],
|
|
1578
|
+
tags=tags or [],
|
|
1579
|
+
**kwargs,
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
def add_test(self, test: Test) -> "TestSuite":
|
|
1583
|
+
"""Add a test to the suite."""
|
|
1584
|
+
self.tests.append(test)
|
|
1585
|
+
return self
|
|
1586
|
+
|
|
1587
|
+
def add_tests(self, *tests: Test) -> "TestSuite":
|
|
1588
|
+
"""Add multiple tests to the suite."""
|
|
1589
|
+
for test in tests:
|
|
1590
|
+
self.tests.append(test)
|
|
1591
|
+
return self
|
|
1592
|
+
|
|
1593
|
+
def tag(self, *tags: str) -> "TestSuite":
|
|
1594
|
+
"""Add tags to the test suite."""
|
|
1595
|
+
for tag in tags:
|
|
1596
|
+
if tag not in self.tags:
|
|
1597
|
+
self.tags.append(tag)
|
|
1598
|
+
return self
|
|
1599
|
+
|
|
1600
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1601
|
+
"""Convert to dict format expected by backend."""
|
|
1602
|
+
return {
|
|
1603
|
+
"name": self.name,
|
|
1604
|
+
"description": self.description,
|
|
1605
|
+
"tests": [test.to_dict() for test in self.tests],
|
|
1606
|
+
"tags": self.tags,
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
|
|
1126
1610
|
# Re-export commonly used types for convenience
|
|
1127
1611
|
__all__ = [
|
|
1128
1612
|
"Agent",
|
|
@@ -1137,6 +1621,20 @@ __all__ = [
|
|
|
1137
1621
|
"PythonFile",
|
|
1138
1622
|
"Prompt",
|
|
1139
1623
|
"TemplateString",
|
|
1624
|
+
# Test system
|
|
1625
|
+
"Test",
|
|
1626
|
+
"TestSuite",
|
|
1627
|
+
"TestExpectation",
|
|
1628
|
+
"TestMessage",
|
|
1629
|
+
"Expect",
|
|
1140
1630
|
# Complex types
|
|
1141
1631
|
"ExecutionCondition",
|
|
1632
|
+
"ConditionDefinition",
|
|
1633
|
+
# Bot permissions
|
|
1634
|
+
"BotPermissions",
|
|
1635
|
+
"set_bot_public",
|
|
1636
|
+
"set_bot_user_permission",
|
|
1637
|
+
"set_bot_org_permission",
|
|
1638
|
+
"get_bot_permissions",
|
|
1639
|
+
"check_bot_access",
|
|
1142
1640
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: erdo
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Python SDK for building workflow automation agents with Erdo
|
|
5
5
|
Project-URL: Homepage, https://erdo.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.erdo.ai
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
24
|
Requires-Python: >=3.9
|
|
25
25
|
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: requests>=2.31.0
|
|
26
27
|
Requires-Dist: typing-extensions>=4.0.0
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
@@ -31,6 +32,8 @@ Requires-Dist: isort>=5.0.0; extra == 'dev'
|
|
|
31
32
|
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
32
33
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
33
34
|
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: types-requests>=2.31.0; extra == 'dev'
|
|
34
37
|
Description-Content-Type: text/markdown
|
|
35
38
|
|
|
36
39
|
# Erdo Agent SDK
|