erdo 0.1.4__py3-none-any.whl → 0.1.5__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/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, preserving None vs [] distinction."""
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
- # If it's an action function, use action= parameter
345
- step_actions.append(Step(action=action))
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 appropriate steps
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 parity
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 result handler steps)
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 = "private"
922
+ visibility: str = "public"
787
923
  running_message: Optional[str] = None
788
924
  finished_message: Optional[str] = None
789
925
  version: str = "1.0"
@@ -828,7 +964,6 @@ class Agent(BaseModel):
828
964
  "user_output_visibility": OutputVisibility.VISIBLE,
829
965
  "bot_output_visibility": OutputVisibility.HIDDEN,
830
966
  "output_content_type": OutputContentType.TEXT,
831
- "parameter_hydration_behaviour": ParameterHydrationBehaviour.NONE,
832
967
  }
833
968
 
834
969
  # Override with any provided kwargs
@@ -850,7 +985,8 @@ class Agent(BaseModel):
850
985
 
851
986
  Args:
852
987
  step_metadata: Step configuration (key, depends_on, etc.)
853
- **codeexec_params: Parameters for codeexec.execute (entrypoint, parameters, resources, etc.)
988
+ **codeexec_params: Parameters for codeexec.execute (entrypoint, parameters,
989
+ resources, etc.)
854
990
 
855
991
  Returns:
856
992
  Callable: Decorator function that creates and returns a Step
@@ -890,7 +1026,6 @@ class Agent(BaseModel):
890
1026
  "user_output_visibility": OutputVisibility.VISIBLE,
891
1027
  "bot_output_visibility": OutputVisibility.HIDDEN,
892
1028
  "output_content_type": OutputContentType.TEXT,
893
- "parameter_hydration_behaviour": ParameterHydrationBehaviour.NONE,
894
1029
  }
895
1030
 
896
1031
  # Remove None values and extract action separately
@@ -932,6 +1067,7 @@ class Agent(BaseModel):
932
1067
  export_data = {
933
1068
  "bot": {
934
1069
  "Name": self.name,
1070
+ "Key": self.key,
935
1071
  "Description": self.description or "",
936
1072
  "Visibility": self.visibility,
937
1073
  "Persona": self.persona, # Export as string or null, not object
@@ -1123,6 +1259,349 @@ class Prompt(BaseModel):
1123
1259
  return prompts
1124
1260
 
1125
1261
 
1262
+ # Test system classes for agent testing
1263
+ class ConditionDefinition(BaseModel):
1264
+ """Condition definition for test expectations."""
1265
+
1266
+ type: str # "TextContains", "IsSuccess", etc.
1267
+ path: str # JSONPath or template path to value
1268
+ parameters: Dict[str, Any] = Field(default_factory=dict)
1269
+
1270
+ def to_dict(self) -> Dict[str, Any]:
1271
+ """Convert to dict format expected by backend."""
1272
+ return {"type": self.type, "path": self.path, "parameters": self.parameters}
1273
+
1274
+
1275
+ class APIConditionDefinition(BaseModel):
1276
+ """API condition definition that matches backend expectations."""
1277
+
1278
+ type: str # "TextContains", "IsSuccess", etc.
1279
+ conditions: List[Any] = Field(default_factory=list) # For composite conditions
1280
+ leaf: Dict[str, Any] = Field(default_factory=dict) # Parameters for leaf conditions
1281
+
1282
+ @field_serializer("leaf")
1283
+ def serialize_leaf(self, leaf_data: Dict[str, Any]) -> str:
1284
+ """Serialize leaf field as JSON string to match Go backend json.RawMessage expectation."""
1285
+ import json
1286
+
1287
+ if leaf_data:
1288
+ # Convert TemplateString objects to strings
1289
+ converted_leaf = {}
1290
+ for key, value in leaf_data.items():
1291
+ if hasattr(value, "template"): # This is a TemplateString
1292
+ converted_leaf[key] = value.template
1293
+ else:
1294
+ converted_leaf[key] = value
1295
+ return json.dumps(converted_leaf)
1296
+ else:
1297
+ # Even if leaf is empty, ensure it's a JSON string
1298
+ return "{}"
1299
+
1300
+
1301
+ class TestExpectation(BaseModel):
1302
+ """Expected outcome for test validation."""
1303
+
1304
+ type: (
1305
+ str # "output_contains", "step_success", "invocation_status", "condition", etc.
1306
+ )
1307
+ step_key: Optional[str] = None
1308
+ field: Optional[str] = None
1309
+ operator: str # "contains", "equals", "greater_than", etc.
1310
+ value: Any
1311
+ description: str
1312
+ condition: Optional[Union[ConditionDefinition, APIConditionDefinition]] = None
1313
+
1314
+ @classmethod
1315
+ def output_contains(
1316
+ cls, step_key: str, text: str, description: Optional[str] = None
1317
+ ) -> "TestExpectation":
1318
+ """Expect step output to contain specific text."""
1319
+ return cls(
1320
+ type="output_contains",
1321
+ step_key=step_key,
1322
+ operator="contains",
1323
+ value=text,
1324
+ description=description
1325
+ or f"Step '{step_key}' output should contain '{text}'",
1326
+ )
1327
+
1328
+ @classmethod
1329
+ def step_succeeds(
1330
+ cls, step_key: str, description: Optional[str] = None
1331
+ ) -> "TestExpectation":
1332
+ """Expect step to complete successfully."""
1333
+ return cls(
1334
+ type="step_success",
1335
+ step_key=step_key,
1336
+ operator="equals",
1337
+ value=True,
1338
+ description=description or f"Step '{step_key}' should succeed",
1339
+ )
1340
+
1341
+ @classmethod
1342
+ def invocation_status(
1343
+ cls, status: str, description: Optional[str] = None
1344
+ ) -> "TestExpectation":
1345
+ """Expect overall invocation to have specific status."""
1346
+ return cls(
1347
+ type="invocation_status",
1348
+ operator="equals",
1349
+ value=status,
1350
+ description=description or f"Invocation should have status '{status}'",
1351
+ )
1352
+
1353
+ @classmethod
1354
+ def from_condition_type(
1355
+ cls,
1356
+ condition_type: str,
1357
+ path: str,
1358
+ parameters: Dict[str, Any],
1359
+ description: Optional[str] = None,
1360
+ ) -> "TestExpectation":
1361
+ """Create a condition-based expectation using backend condition system."""
1362
+ return cls(
1363
+ type="condition",
1364
+ operator="evaluate",
1365
+ value="true",
1366
+ description=description
1367
+ or f"Condition {condition_type} should evaluate to true",
1368
+ condition=ConditionDefinition(
1369
+ type=condition_type, path=path, parameters=parameters
1370
+ ),
1371
+ )
1372
+
1373
+ @classmethod
1374
+ def text_contains(
1375
+ cls, path: str, text: str, description: Optional[str] = None
1376
+ ) -> "TestExpectation":
1377
+ """Expect value at path to contain specific text."""
1378
+ return cls(
1379
+ type="condition",
1380
+ operator="evaluate",
1381
+ value="true",
1382
+ description=description or f"Value at '{path}' should contain '{text}'",
1383
+ condition=ConditionDefinition(
1384
+ type="TextContains", path=path, parameters={"text": text}
1385
+ ),
1386
+ )
1387
+
1388
+ @classmethod
1389
+ def is_success(
1390
+ cls, path: str = "$.Status", description: Optional[str] = None
1391
+ ) -> "TestExpectation":
1392
+ """Expect value at path to indicate success."""
1393
+ return cls(
1394
+ type="condition",
1395
+ operator="evaluate",
1396
+ value="true",
1397
+ description=description or "Operation should succeed",
1398
+ condition=ConditionDefinition(type="IsSuccess", path=path, parameters={}),
1399
+ )
1400
+
1401
+ @classmethod
1402
+ def from_condition(
1403
+ cls, condition_obj: Any, description: Optional[str] = None
1404
+ ) -> "TestExpectation":
1405
+ """Create expectation from condition object with parameter hydration support."""
1406
+ # Convert condition object to dict format
1407
+ condition_dict = condition_obj.to_dict()
1408
+ condition_type = condition_dict.get("type", "Unknown")
1409
+
1410
+ # Extract leaf parameters from the condition object - these support template strings
1411
+ leaf_params = {}
1412
+ if "leaf" in condition_dict:
1413
+ leaf_params = condition_dict["leaf"]
1414
+
1415
+ # Create APIConditionDefinition object that matches backend expectations
1416
+ api_condition = APIConditionDefinition(
1417
+ type=condition_type,
1418
+ conditions=[], # Empty for leaf conditions
1419
+ leaf=leaf_params, # Parameters go in leaf field for backend
1420
+ )
1421
+
1422
+ return cls(
1423
+ type="condition",
1424
+ operator="evaluate",
1425
+ value="true",
1426
+ description=description
1427
+ or f"Condition {condition_type} should evaluate to true",
1428
+ condition=api_condition, # Use APIConditionDefinition object
1429
+ )
1430
+
1431
+
1432
+ class TestMessage(BaseModel):
1433
+ """Test message for agent input."""
1434
+
1435
+ role: str # "user" or "assistant"
1436
+ content: str
1437
+
1438
+ @classmethod
1439
+ def user(cls, content: str) -> "TestMessage":
1440
+ """Create a user message."""
1441
+ return cls(role="user", content=content)
1442
+
1443
+ @classmethod
1444
+ def assistant(cls, content: str) -> "TestMessage":
1445
+ """Create an assistant message."""
1446
+ return cls(role="assistant", content=content)
1447
+
1448
+
1449
+ class Test(BaseModel):
1450
+ """Test definition for agent validation."""
1451
+
1452
+ name: str
1453
+ description: str
1454
+ agent: Optional[Any] = None # Reference to agent being tested
1455
+ messages: List[TestMessage] = Field(default_factory=list)
1456
+ session_messages: List[TestMessage] = Field(default_factory=list)
1457
+ dataset_ids: List[str] = Field(
1458
+ default_factory=list
1459
+ ) # Reference real datasets by ID
1460
+ parameters: Dict[str, str] = Field(default_factory=dict) # Bot parameters
1461
+ expectations: List[TestExpectation] = Field(default_factory=list)
1462
+ resume_from_state_id: Optional[str] = None
1463
+ test_suite: Optional[str] = None # Name of the test suite this test belongs to
1464
+
1465
+ def __init__(
1466
+ self,
1467
+ name: Optional[str] = None,
1468
+ description: str = "",
1469
+ agent: Optional[Any] = None,
1470
+ expect: Optional[List[TestExpectation]] = None,
1471
+ **kwargs: Any,
1472
+ ):
1473
+ """Initialize a Test with improved syntax support.
1474
+
1475
+ Args:
1476
+ name: Test name (auto-generated from agent name if not provided)
1477
+ description: Test description
1478
+ agent: Agent to test
1479
+ expect: List of expectations (alternative to using .expect() method)
1480
+ **kwargs: Additional test configuration
1481
+ """
1482
+ # Auto-generate name from agent if not provided
1483
+ if name is None and agent is not None:
1484
+ agent_name = getattr(agent, "name", "agent")
1485
+ name = f"{agent_name}_test"
1486
+ elif name is None:
1487
+ name = "test"
1488
+
1489
+ # Set expectations from constructor if provided
1490
+ if expect is not None:
1491
+ kwargs["expectations"] = expect
1492
+
1493
+ super().__init__(name=name, description=description, agent=agent, **kwargs)
1494
+
1495
+ def message(self, role: str, content: str) -> "Test":
1496
+ """Add a message to the test conversation."""
1497
+ self.messages.append(TestMessage(role=role, content=content))
1498
+ return self
1499
+
1500
+ def user_message(self, content: str) -> "Test":
1501
+ """Add a user message."""
1502
+ return self.message("user", content)
1503
+
1504
+ def assistant_message(self, content: str) -> "Test":
1505
+ """Add an assistant message."""
1506
+ return self.message("assistant", content)
1507
+
1508
+ def dataset(self, dataset_id: str) -> "Test":
1509
+ """Add a dataset to the test by ID."""
1510
+ self.dataset_ids.append(dataset_id)
1511
+ return self
1512
+
1513
+ def expect(self, expectation: TestExpectation) -> "Test":
1514
+ """Add an expectation to the test."""
1515
+ self.expectations.append(expectation)
1516
+ return self
1517
+
1518
+ def to_dict(self) -> Dict[str, Any]:
1519
+ """Convert to dict format expected by backend."""
1520
+ return {
1521
+ "name": self.name,
1522
+ "description": self.description,
1523
+ "test_data": {
1524
+ "messages": [msg.model_dump() for msg in self.messages],
1525
+ "session_messages": [msg.model_dump() for msg in self.session_messages],
1526
+ "dataset_ids": self.dataset_ids,
1527
+ "parameters": self.parameters,
1528
+ "resume_from_state_id": self.resume_from_state_id,
1529
+ },
1530
+ "expectations": [exp.model_dump() for exp in self.expectations],
1531
+ }
1532
+
1533
+
1534
+ def Expect(condition: Any, description: Optional[str] = None) -> TestExpectation:
1535
+ """Create a test expectation from a condition object with clean syntax.
1536
+
1537
+ Usage:
1538
+ Expect(IsSuccess())
1539
+ Expect(TextContains(text=TemplateString("{{output.content}}"), value="success"))
1540
+ """
1541
+ return TestExpectation.from_condition(condition, description)
1542
+
1543
+
1544
+ class TestSuite(BaseModel):
1545
+ """Test suite for grouping related tests together."""
1546
+
1547
+ name: str
1548
+ description: str
1549
+ tests: List[Test] = Field(default_factory=list)
1550
+ tags: List[str] = Field(default_factory=list)
1551
+
1552
+ def __init__(
1553
+ self,
1554
+ name: str,
1555
+ description: str = "",
1556
+ tests: Optional[List[Test]] = None,
1557
+ tags: Optional[List[str]] = None,
1558
+ **kwargs: Any,
1559
+ ):
1560
+ """Initialize a TestSuite.
1561
+
1562
+ Args:
1563
+ name: Suite name
1564
+ description: Suite description
1565
+ tests: List of tests to include in the suite
1566
+ tags: Tags for categorizing the test suite
1567
+ **kwargs: Additional suite configuration
1568
+ """
1569
+ super().__init__(
1570
+ name=name,
1571
+ description=description,
1572
+ tests=tests or [],
1573
+ tags=tags or [],
1574
+ **kwargs,
1575
+ )
1576
+
1577
+ def add_test(self, test: Test) -> "TestSuite":
1578
+ """Add a test to the suite."""
1579
+ self.tests.append(test)
1580
+ return self
1581
+
1582
+ def add_tests(self, *tests: Test) -> "TestSuite":
1583
+ """Add multiple tests to the suite."""
1584
+ for test in tests:
1585
+ self.tests.append(test)
1586
+ return self
1587
+
1588
+ def tag(self, *tags: str) -> "TestSuite":
1589
+ """Add tags to the test suite."""
1590
+ for tag in tags:
1591
+ if tag not in self.tags:
1592
+ self.tags.append(tag)
1593
+ return self
1594
+
1595
+ def to_dict(self) -> Dict[str, Any]:
1596
+ """Convert to dict format expected by backend."""
1597
+ return {
1598
+ "name": self.name,
1599
+ "description": self.description,
1600
+ "tests": [test.to_dict() for test in self.tests],
1601
+ "tags": self.tags,
1602
+ }
1603
+
1604
+
1126
1605
  # Re-export commonly used types for convenience
1127
1606
  __all__ = [
1128
1607
  "Agent",
@@ -1137,6 +1616,20 @@ __all__ = [
1137
1616
  "PythonFile",
1138
1617
  "Prompt",
1139
1618
  "TemplateString",
1619
+ # Test system
1620
+ "Test",
1621
+ "TestSuite",
1622
+ "TestExpectation",
1623
+ "TestMessage",
1624
+ "Expect",
1140
1625
  # Complex types
1141
1626
  "ExecutionCondition",
1627
+ "ConditionDefinition",
1628
+ # Bot permissions
1629
+ "BotPermissions",
1630
+ "set_bot_public",
1631
+ "set_bot_user_permission",
1632
+ "set_bot_org_permission",
1633
+ "get_bot_permissions",
1634
+ "check_bot_access",
1142
1635
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: erdo
3
- Version: 0.1.4
3
+ Version: 0.1.5
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'