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.
- erdo/__init__.py +35 -0
- erdo/_generated/__init__.py +18 -0
- erdo/_generated/actions/__init__.py +34 -0
- erdo/_generated/actions/analysis.py +179 -0
- erdo/_generated/actions/bot.py +186 -0
- erdo/_generated/actions/codeexec.py +199 -0
- erdo/_generated/actions/llm.py +148 -0
- erdo/_generated/actions/memory.py +463 -0
- erdo/_generated/actions/pdfextractor.py +97 -0
- erdo/_generated/actions/resource_definitions.py +296 -0
- erdo/_generated/actions/sqlexec.py +90 -0
- erdo/_generated/actions/utils.py +475 -0
- erdo/_generated/actions/webparser.py +119 -0
- erdo/_generated/actions/websearch.py +85 -0
- erdo/_generated/condition/__init__.py +556 -0
- erdo/_generated/internal.py +51 -0
- erdo/_generated/internal_actions.py +91 -0
- erdo/_generated/parameters.py +17 -0
- erdo/_generated/secrets.py +17 -0
- erdo/_generated/template_functions.py +55 -0
- erdo/_generated/types.py +3907 -0
- erdo/actions/__init__.py +40 -0
- erdo/bot_permissions.py +266 -0
- erdo/cli_entry.py +73 -0
- erdo/conditions/__init__.py +11 -0
- erdo/config/__init__.py +5 -0
- erdo/config/config.py +140 -0
- erdo/formatting.py +279 -0
- erdo/install_cli.py +140 -0
- erdo/integrations.py +131 -0
- erdo/invoke/__init__.py +11 -0
- erdo/invoke/client.py +234 -0
- erdo/invoke/invoke.py +555 -0
- erdo/state.py +376 -0
- erdo/sync/__init__.py +17 -0
- erdo/sync/client.py +95 -0
- erdo/sync/extractor.py +492 -0
- erdo/sync/sync.py +327 -0
- erdo/template.py +136 -0
- erdo/test/__init__.py +41 -0
- erdo/test/evaluate.py +272 -0
- erdo/test/runner.py +263 -0
- erdo/types.py +1431 -0
- erdo-0.1.31.dist-info/METADATA +471 -0
- erdo-0.1.31.dist-info/RECORD +48 -0
- erdo-0.1.31.dist-info/WHEEL +4 -0
- erdo-0.1.31.dist-info/entry_points.txt +2 -0
- erdo-0.1.31.dist-info/licenses/LICENSE +22 -0
erdo/types.py
ADDED
|
@@ -0,0 +1,1431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Erdo Agent SDK - Core Types
|
|
3
|
+
|
|
4
|
+
This file contains the main SDK classes for building AI agents.
|
|
5
|
+
Auto-generated types are imported from the generated module.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union, cast
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field, field_serializer, model_serializer
|
|
14
|
+
|
|
15
|
+
from ._generated.types import (
|
|
16
|
+
BotResource,
|
|
17
|
+
Dataset,
|
|
18
|
+
DatasetType,
|
|
19
|
+
ExecutionModeType,
|
|
20
|
+
HandlerType,
|
|
21
|
+
OutputContentType,
|
|
22
|
+
OutputVisibility,
|
|
23
|
+
ParameterDefinition,
|
|
24
|
+
ParameterHydrationBehaviour,
|
|
25
|
+
Tool,
|
|
26
|
+
)
|
|
27
|
+
from .bot_permissions import (
|
|
28
|
+
BotPermissions,
|
|
29
|
+
check_bot_access,
|
|
30
|
+
get_bot_permissions,
|
|
31
|
+
set_bot_org_permission,
|
|
32
|
+
set_bot_public,
|
|
33
|
+
set_bot_user_permission,
|
|
34
|
+
)
|
|
35
|
+
from .template import TemplateString
|
|
36
|
+
|
|
37
|
+
# Type aliases for complex types that are json.RawMessage in Go
|
|
38
|
+
ExecutionCondition = Dict[str, Any] # Complex execution condition configuration
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MessageRole(str, Enum):
|
|
42
|
+
"""Role for step output in message history."""
|
|
43
|
+
|
|
44
|
+
USER = "user"
|
|
45
|
+
ASSISTANT = "assistant"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Protocols for better type safety
|
|
49
|
+
class ActionProtocol(Protocol):
|
|
50
|
+
"""Protocol for action objects that can be converted to parameters."""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
|
|
54
|
+
def model_dump(self) -> Dict[str, Any]: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StepLike(Protocol):
|
|
58
|
+
"""Protocol for step-like objects."""
|
|
59
|
+
|
|
60
|
+
key: Optional[str]
|
|
61
|
+
id: Optional[str]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ConditionProtocol(Protocol):
|
|
65
|
+
"""Protocol for condition objects."""
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> Dict[str, Any]: ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ExecutionModeProtocol(Protocol):
|
|
71
|
+
"""Protocol for execution mode objects."""
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> Dict[str, Any]: ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PythonFile(BaseModel):
|
|
77
|
+
"""Reference to a Python file for code execution.
|
|
78
|
+
|
|
79
|
+
Can be used in code_files parameter to reference local Python files.
|
|
80
|
+
When used, the file content will be automatically loaded during export.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
filename: str = Field(
|
|
84
|
+
..., description="Path to the file (e.g., 'analyze_file_files/analyze.py')"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
88
|
+
"""Convert to dict format for serialization."""
|
|
89
|
+
return {
|
|
90
|
+
"filename": self.filename,
|
|
91
|
+
"_type": "PythonFile", # Marker to identify this as a file reference
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@model_serializer
|
|
95
|
+
def _serialize_model(self) -> Dict[str, Any]:
|
|
96
|
+
"""Pydantic v2 serializer - always use our custom format."""
|
|
97
|
+
return self.to_dict()
|
|
98
|
+
|
|
99
|
+
def resolve_content(self, base_path: Optional[str] = None) -> Dict[str, str]:
|
|
100
|
+
"""Resolve the file content for inclusion in code_files.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
base_path: Base directory path to resolve relative paths
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Dict with filename and content
|
|
107
|
+
"""
|
|
108
|
+
if base_path is None:
|
|
109
|
+
base_path = os.getcwd()
|
|
110
|
+
|
|
111
|
+
file_path = Path(base_path) / self.filename
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
115
|
+
content = f.read()
|
|
116
|
+
# Return just the base filename, not the full path
|
|
117
|
+
base_filename = Path(self.filename).name
|
|
118
|
+
return {"filename": base_filename, "content": content}
|
|
119
|
+
except FileNotFoundError:
|
|
120
|
+
raise FileNotFoundError(f"Python file not found: {file_path}")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise RuntimeError(f"Error reading Python file {file_path}: {e}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _extract_step_config_from_action(action: Any) -> Dict[str, Any]:
|
|
126
|
+
"""Extract step configuration from an action object.
|
|
127
|
+
|
|
128
|
+
Actions can include a step_metadata parameter that configures the Step properties.
|
|
129
|
+
This function extracts those properties and returns them as a dict for Step(**config).
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
utils.echo(
|
|
133
|
+
data={"result": "value"},
|
|
134
|
+
step_metadata=StepMetadata(
|
|
135
|
+
key="my_step",
|
|
136
|
+
output_behavior={"result": OutputBehaviorType.MERGE}
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
The step_metadata fields are extracted and used to configure the Step:
|
|
141
|
+
- key: Step identifier
|
|
142
|
+
- output_behavior: How output fields are merged into state
|
|
143
|
+
- execution_mode: Parallel/sequential/background/iterate
|
|
144
|
+
- depends_on: Step dependencies
|
|
145
|
+
- output_channels: Where output is sent
|
|
146
|
+
- visibility settings: User and bot visibility
|
|
147
|
+
- messages: Running/finished messages
|
|
148
|
+
- content types: Output/history/UI content types
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
action: Action object (from utils.echo, llm.message, etc.)
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Dict with 'action' and any extracted step_metadata fields
|
|
155
|
+
"""
|
|
156
|
+
step_config: Dict[str, Any] = {"action": action}
|
|
157
|
+
|
|
158
|
+
# Extract step_metadata if present on the action object
|
|
159
|
+
if hasattr(action, "step_metadata") and action.step_metadata is not None:
|
|
160
|
+
metadata = action.step_metadata
|
|
161
|
+
|
|
162
|
+
# Extract each metadata field if it's set (not None)
|
|
163
|
+
if hasattr(metadata, "key") and metadata.key:
|
|
164
|
+
step_config["key"] = metadata.key
|
|
165
|
+
if hasattr(metadata, "depends_on") and metadata.depends_on:
|
|
166
|
+
step_config["depends_on"] = metadata.depends_on
|
|
167
|
+
if hasattr(metadata, "execution_mode") and metadata.execution_mode:
|
|
168
|
+
step_config["execution_mode"] = metadata.execution_mode
|
|
169
|
+
if hasattr(metadata, "output_behavior") and metadata.output_behavior:
|
|
170
|
+
step_config["output_behavior"] = metadata.output_behavior
|
|
171
|
+
if hasattr(metadata, "output_channels") and metadata.output_channels:
|
|
172
|
+
step_config["output_channels"] = metadata.output_channels
|
|
173
|
+
if hasattr(metadata, "output_content_type") and metadata.output_content_type:
|
|
174
|
+
step_config["output_content_type"] = metadata.output_content_type
|
|
175
|
+
if hasattr(metadata, "history_content_type") and metadata.history_content_type:
|
|
176
|
+
step_config["history_content_type"] = metadata.history_content_type
|
|
177
|
+
if hasattr(metadata, "history_role") and metadata.history_role:
|
|
178
|
+
step_config["history_role"] = metadata.history_role
|
|
179
|
+
if hasattr(metadata, "ui_content_type") and metadata.ui_content_type:
|
|
180
|
+
step_config["ui_content_type"] = metadata.ui_content_type
|
|
181
|
+
if (
|
|
182
|
+
hasattr(metadata, "user_output_visibility")
|
|
183
|
+
and metadata.user_output_visibility
|
|
184
|
+
):
|
|
185
|
+
step_config["user_output_visibility"] = metadata.user_output_visibility
|
|
186
|
+
if (
|
|
187
|
+
hasattr(metadata, "bot_output_visibility")
|
|
188
|
+
and metadata.bot_output_visibility
|
|
189
|
+
):
|
|
190
|
+
step_config["bot_output_visibility"] = metadata.bot_output_visibility
|
|
191
|
+
if hasattr(metadata, "running_status") and metadata.running_status:
|
|
192
|
+
step_config["running_status"] = metadata.running_status
|
|
193
|
+
if hasattr(metadata, "finished_status") and metadata.finished_status:
|
|
194
|
+
step_config["finished_status"] = metadata.finished_status
|
|
195
|
+
if (
|
|
196
|
+
hasattr(metadata, "parameter_hydration_behaviour")
|
|
197
|
+
and metadata.parameter_hydration_behaviour
|
|
198
|
+
):
|
|
199
|
+
step_config["parameter_hydration_behaviour"] = (
|
|
200
|
+
metadata.parameter_hydration_behaviour
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return step_config
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class StepMetadata(BaseModel):
|
|
207
|
+
"""Metadata for workflow steps, containing configuration and execution parameters.
|
|
208
|
+
|
|
209
|
+
StepMetadata is used to configure step properties when creating steps via actions.
|
|
210
|
+
It's particularly useful in result handlers where you need to configure step behavior:
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
step.on(IsSuccess(),
|
|
214
|
+
utils.parse_json(
|
|
215
|
+
json_data="{{output}}",
|
|
216
|
+
step_metadata=StepMetadata(
|
|
217
|
+
key="parse_result",
|
|
218
|
+
output_behavior={"data": OutputBehaviorType.MERGE}
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
Fields:
|
|
224
|
+
key: Step identifier (used for referencing in templates and dependencies)
|
|
225
|
+
output_behavior: Controls how output fields are merged into state
|
|
226
|
+
- STEP_ONLY: Output only available via steps.step_key.field
|
|
227
|
+
- MERGE: Output fields merged into root state
|
|
228
|
+
- OVERWRITE: Output replaces entire state
|
|
229
|
+
execution_mode: How the step executes (sequential/parallel/background/iterate)
|
|
230
|
+
depends_on: Other steps this step depends on
|
|
231
|
+
output_channels: Where step output is sent (e.g., ["user", "bot"])
|
|
232
|
+
visibility: Control who sees the output (user/bot)
|
|
233
|
+
messages: Custom running/finished messages
|
|
234
|
+
content_types: Specify output/history/UI content types
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
238
|
+
|
|
239
|
+
key: Optional[str] = None
|
|
240
|
+
depends_on: Union[List[Union[Any, str]], None] = (
|
|
241
|
+
None # Can be Step objects or strings
|
|
242
|
+
)
|
|
243
|
+
execution_mode: Optional[Union[Any, Dict[str, Any]]] = (
|
|
244
|
+
None # Can be ExecutionMode object or dict
|
|
245
|
+
)
|
|
246
|
+
output_behavior: Optional[Dict[str, Any]] = None
|
|
247
|
+
output_channels: List[str] = Field(default_factory=list)
|
|
248
|
+
output_content_type: OutputContentType = OutputContentType.TEXT
|
|
249
|
+
history_content_type: Optional[str] = None
|
|
250
|
+
history_role: Optional[MessageRole] = None
|
|
251
|
+
ui_content_type: Optional[str] = None
|
|
252
|
+
user_output_visibility: OutputVisibility = OutputVisibility.VISIBLE
|
|
253
|
+
bot_output_visibility: OutputVisibility = OutputVisibility.HIDDEN
|
|
254
|
+
running_status: Optional[Union[str, TemplateString]] = None
|
|
255
|
+
finished_status: Optional[Union[str, TemplateString]] = None
|
|
256
|
+
parameter_hydration_behaviour: Optional[ParameterHydrationBehaviour] = None
|
|
257
|
+
|
|
258
|
+
@field_serializer(
|
|
259
|
+
"running_status", "finished_status", mode="wrap", when_used="always"
|
|
260
|
+
)
|
|
261
|
+
def serialize_message_fields(
|
|
262
|
+
self, value: Optional[Union[str, TemplateString]], _info
|
|
263
|
+
) -> Optional[str]:
|
|
264
|
+
"""Convert TemplateString to str during serialization."""
|
|
265
|
+
if isinstance(value, TemplateString):
|
|
266
|
+
return str(value)
|
|
267
|
+
return value
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Step(StepMetadata):
|
|
271
|
+
"""A single step in an agent workflow."""
|
|
272
|
+
|
|
273
|
+
agent: Optional[Any] = None # Reference to the agent this step belongs to
|
|
274
|
+
result_handler: Optional[Any] = (
|
|
275
|
+
None # Reference to the result handler this step belongs to
|
|
276
|
+
)
|
|
277
|
+
action: Union[Any, Dict[str, Any]] # Function call like codeexec.execute(...)
|
|
278
|
+
parameters: Dict[str, Any] = Field(default_factory=dict)
|
|
279
|
+
result_handlers: List["ResultHandler"] = Field(default_factory=list)
|
|
280
|
+
|
|
281
|
+
def __init__(self, action: Any = None, step: Optional["Step"] = None, **data: Any):
|
|
282
|
+
# Validate that only one of action or step is provided
|
|
283
|
+
if action is not None and step is not None:
|
|
284
|
+
raise ValueError("Cannot specify both 'action' and 'step' parameters")
|
|
285
|
+
|
|
286
|
+
if action is None and step is None:
|
|
287
|
+
raise ValueError("Must specify either 'action' or 'step' parameter")
|
|
288
|
+
|
|
289
|
+
if step is not None:
|
|
290
|
+
# Copy all fields from the provided step
|
|
291
|
+
# Type check is redundant due to type annotation, but kept for runtime safety
|
|
292
|
+
if not hasattr(step, "action"):
|
|
293
|
+
raise ValueError("'step' parameter must be a Step object")
|
|
294
|
+
|
|
295
|
+
# Copy fields directly to preserve object types (especially action)
|
|
296
|
+
# Don't use model_dump() as it serializes objects to dicts
|
|
297
|
+
data.update(
|
|
298
|
+
{
|
|
299
|
+
"action": step.action,
|
|
300
|
+
"key": step.key,
|
|
301
|
+
"parameters": step.parameters,
|
|
302
|
+
"depends_on": step.depends_on,
|
|
303
|
+
"execution_mode": step.execution_mode,
|
|
304
|
+
"output_behavior": step.output_behavior,
|
|
305
|
+
"result_handlers": step.result_handlers,
|
|
306
|
+
"output_channels": step.output_channels,
|
|
307
|
+
"output_content_type": step.output_content_type,
|
|
308
|
+
"history_content_type": step.history_content_type,
|
|
309
|
+
"history_role": step.history_role,
|
|
310
|
+
"ui_content_type": step.ui_content_type,
|
|
311
|
+
"user_output_visibility": step.user_output_visibility,
|
|
312
|
+
"bot_output_visibility": step.bot_output_visibility,
|
|
313
|
+
"running_status": step.running_status,
|
|
314
|
+
"finished_status": step.finished_status,
|
|
315
|
+
"parameter_hydration_behaviour": step.parameter_hydration_behaviour,
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
# Standard action-based step
|
|
320
|
+
data["action"] = action
|
|
321
|
+
|
|
322
|
+
# Handle step_metadata - can be provided as kwarg or on action object
|
|
323
|
+
step_metadata = None
|
|
324
|
+
if "step_metadata" in data and data["step_metadata"] is not None:
|
|
325
|
+
step_metadata = data.pop("step_metadata")
|
|
326
|
+
elif (
|
|
327
|
+
hasattr(action, "step_metadata")
|
|
328
|
+
and getattr(action, "step_metadata", None) is not None
|
|
329
|
+
):
|
|
330
|
+
step_metadata = getattr(action, "step_metadata")
|
|
331
|
+
|
|
332
|
+
if step_metadata is not None:
|
|
333
|
+
# Extract all fields from StepMetadata
|
|
334
|
+
if hasattr(step_metadata, "model_dump"):
|
|
335
|
+
metadata_dict = step_metadata.model_dump(exclude_none=True)
|
|
336
|
+
# Apply metadata fields, but don't overwrite explicitly provided fields
|
|
337
|
+
for key, value in metadata_dict.items():
|
|
338
|
+
if key not in data:
|
|
339
|
+
data[key] = value
|
|
340
|
+
elif isinstance(step_metadata, dict):
|
|
341
|
+
# If it's already a dict, apply it
|
|
342
|
+
for key, value in step_metadata.items():
|
|
343
|
+
if key not in data and value is not None:
|
|
344
|
+
data[key] = value
|
|
345
|
+
|
|
346
|
+
super().__init__(**data)
|
|
347
|
+
# For enhanced syntax support - use private field not tracked by pydantic
|
|
348
|
+
self._decorator_handlers: List[Tuple[Any, Callable[..., Any]]] = []
|
|
349
|
+
|
|
350
|
+
# Validate depends_on to ensure no empty strings
|
|
351
|
+
if self.depends_on is not None:
|
|
352
|
+
for dep in self.depends_on:
|
|
353
|
+
if isinstance(dep, str) and dep == "":
|
|
354
|
+
raise ValueError(
|
|
355
|
+
f"Step '{self.key or '(unnamed)'}' has an empty string in depends_on. "
|
|
356
|
+
"Dependencies must be non-empty step keys or Step objects. "
|
|
357
|
+
"Remove the empty string or set depends_on=[] for no dependencies."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Automatically register with agent if provided and this is not a nested step
|
|
361
|
+
if self.agent is not None and self.result_handler is None:
|
|
362
|
+
self.agent.add_step(self)
|
|
363
|
+
|
|
364
|
+
def extract_action_parameters(self) -> Dict[str, Any]:
|
|
365
|
+
"""Extract parameters from the action object safely."""
|
|
366
|
+
if not self.action:
|
|
367
|
+
return {}
|
|
368
|
+
|
|
369
|
+
# Handle nested Step objects (for result handlers)
|
|
370
|
+
if isinstance(self.action, Step):
|
|
371
|
+
return self.action.extract_action_parameters()
|
|
372
|
+
|
|
373
|
+
# If action is a Pydantic model, get its dict representation
|
|
374
|
+
if hasattr(self.action, "model_dump") and callable(
|
|
375
|
+
getattr(self.action, "model_dump")
|
|
376
|
+
):
|
|
377
|
+
try:
|
|
378
|
+
action_obj = cast(ActionProtocol, self.action)
|
|
379
|
+
params = action_obj.model_dump()
|
|
380
|
+
# Remove the redundant 'name' field since it's already known from action type
|
|
381
|
+
params.pop("name", None)
|
|
382
|
+
# Remove step_metadata - it's for Step config, not action parameters
|
|
383
|
+
params.pop("step_metadata", None)
|
|
384
|
+
# Map Python SDK field names to backend field names
|
|
385
|
+
if "json_data" in params:
|
|
386
|
+
params["json"] = params.pop("json_data")
|
|
387
|
+
# Remove None values for optional parameters (matches action function behavior)
|
|
388
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
389
|
+
# Sort parameters for deterministic output
|
|
390
|
+
return dict(sorted(params.items()))
|
|
391
|
+
except Exception:
|
|
392
|
+
return {}
|
|
393
|
+
elif hasattr(self.action, "dict") and callable(getattr(self.action, "dict")):
|
|
394
|
+
try:
|
|
395
|
+
# Legacy pydantic v1 support
|
|
396
|
+
action_dict_method = getattr(self.action, "dict")
|
|
397
|
+
params = action_dict_method()
|
|
398
|
+
# Remove the redundant 'name' field since it's already known from action type
|
|
399
|
+
params.pop("name", None)
|
|
400
|
+
# Remove step_metadata - it's for Step config, not action parameters
|
|
401
|
+
params.pop("step_metadata", None)
|
|
402
|
+
# Map Python SDK field names to backend field names
|
|
403
|
+
if "json_data" in params:
|
|
404
|
+
params["json"] = params.pop("json_data")
|
|
405
|
+
# Remove None values for optional parameters (matches action function behavior)
|
|
406
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
407
|
+
# Sort parameters for deterministic output
|
|
408
|
+
return dict(sorted(params.items()))
|
|
409
|
+
except Exception:
|
|
410
|
+
return {}
|
|
411
|
+
elif isinstance(self.action, dict):
|
|
412
|
+
# Map Python SDK field names to backend field names
|
|
413
|
+
dict_params: Dict[str, Any] = dict(self.action)
|
|
414
|
+
# Remove the redundant 'name' field since it's already known from action type
|
|
415
|
+
dict_params.pop("name", None)
|
|
416
|
+
if "json_data" in dict_params:
|
|
417
|
+
dict_params["json"] = dict_params.pop("json_data")
|
|
418
|
+
# Sort parameters for deterministic output
|
|
419
|
+
return dict(sorted(dict_params.items()))
|
|
420
|
+
else:
|
|
421
|
+
return {}
|
|
422
|
+
|
|
423
|
+
def get_action_type(self) -> str:
|
|
424
|
+
"""Get the action type string safely."""
|
|
425
|
+
if not self.action:
|
|
426
|
+
raise ValueError("Action is not set")
|
|
427
|
+
|
|
428
|
+
# Handle nested Step objects (for result handlers)
|
|
429
|
+
if isinstance(self.action, Step):
|
|
430
|
+
return self.action.get_action_type()
|
|
431
|
+
|
|
432
|
+
# Action objects should have a name attribute with the action type
|
|
433
|
+
if hasattr(self.action, "name"):
|
|
434
|
+
name_attr = getattr(self.action, "name")
|
|
435
|
+
if isinstance(name_attr, str):
|
|
436
|
+
return name_attr
|
|
437
|
+
elif hasattr(name_attr, "__str__"):
|
|
438
|
+
return str(name_attr)
|
|
439
|
+
|
|
440
|
+
# Fallback for unexpected cases
|
|
441
|
+
return str(type(self.action).__name__).lower()
|
|
442
|
+
|
|
443
|
+
def get_depends_on_keys(self) -> Optional[List[str]]:
|
|
444
|
+
"""Get dependency keys safely without circular references,
|
|
445
|
+
preserving None vs [] distinction."""
|
|
446
|
+
# CRITICAL: Preserve None vs [] distinction for 100% roundtrip parity
|
|
447
|
+
if self.depends_on is None:
|
|
448
|
+
return None # Explicitly return None for null dependencies
|
|
449
|
+
|
|
450
|
+
if not self.depends_on: # Empty list case
|
|
451
|
+
return []
|
|
452
|
+
|
|
453
|
+
# Depends_on is guaranteed to be a non-empty list at this point
|
|
454
|
+
depends_list = self.depends_on
|
|
455
|
+
|
|
456
|
+
result: List[str] = []
|
|
457
|
+
for dep in depends_list:
|
|
458
|
+
if isinstance(dep, str):
|
|
459
|
+
# Already a string identifier
|
|
460
|
+
result.append(dep)
|
|
461
|
+
elif hasattr(dep, "key") and getattr(dep, "key", None):
|
|
462
|
+
# Prefer the step key if available
|
|
463
|
+
key_val = getattr(dep, "key")
|
|
464
|
+
result.append(str(key_val) if key_val is not None else "")
|
|
465
|
+
elif hasattr(dep, "id") and getattr(dep, "id", None):
|
|
466
|
+
# Fall back to step ID if no key
|
|
467
|
+
id_val = getattr(dep, "id")
|
|
468
|
+
result.append(str(id_val) if id_val is not None else "")
|
|
469
|
+
else:
|
|
470
|
+
# Fail if we can't get a valid identifier
|
|
471
|
+
raise ValueError(f"Step dependency has no key or id: {dep}")
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def output(self) -> "StepOutput":
|
|
476
|
+
"""Get typed output reference for this step."""
|
|
477
|
+
return StepOutput(step_key=self.key or f"step_{id(self)}")
|
|
478
|
+
|
|
479
|
+
def when(
|
|
480
|
+
self, condition: Any
|
|
481
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
482
|
+
"""Decorator for adding result handlers with conditions.
|
|
483
|
+
|
|
484
|
+
Usage:
|
|
485
|
+
@step.when(IsSuccess() & GreaterThan("confidence", 0.8))
|
|
486
|
+
def handle_high_confidence(result):
|
|
487
|
+
return store_analysis(result)
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
491
|
+
# Store the handler function and condition for later processing
|
|
492
|
+
self._decorator_handlers.append((condition, func))
|
|
493
|
+
return func
|
|
494
|
+
|
|
495
|
+
return decorator
|
|
496
|
+
|
|
497
|
+
def on(
|
|
498
|
+
self,
|
|
499
|
+
condition: Any,
|
|
500
|
+
*actions: Any,
|
|
501
|
+
handler_type: HandlerType = HandlerType.INTERMEDIATE,
|
|
502
|
+
) -> "Step":
|
|
503
|
+
"""Add a result handler with condition and action(s).
|
|
504
|
+
|
|
505
|
+
Usage:
|
|
506
|
+
# Single action
|
|
507
|
+
step.on(IsSuccess(), utils.store_analysis(data=state.analyze_step.output))
|
|
508
|
+
|
|
509
|
+
# Multiple actions (variadic arguments)
|
|
510
|
+
step.on(And(IsError(), LessThan(number='r"{{coalesce "code_retry_loops?" 0}}"', value="2")),
|
|
511
|
+
send_status(status="retrying", message="Code execution failed, attempting to fix..."),
|
|
512
|
+
utils.echo(data={"code_retry_loops": 'r"{{incrementCounter "code_retry_loops"}}'}),
|
|
513
|
+
raise_action(status="go to step", message="code", parameters={...})
|
|
514
|
+
)
|
|
515
|
+
"""
|
|
516
|
+
if not actions:
|
|
517
|
+
raise ValueError("Must specify at least one action")
|
|
518
|
+
|
|
519
|
+
# Create steps for each action, handling Step objects correctly
|
|
520
|
+
step_actions: List[Step] = []
|
|
521
|
+
for action in actions:
|
|
522
|
+
if isinstance(action, Step):
|
|
523
|
+
# If it's already a Step, use step= parameter
|
|
524
|
+
step_actions.append(Step(step=action))
|
|
525
|
+
else:
|
|
526
|
+
# Extract step_metadata from action if present and merge into Step
|
|
527
|
+
# This allows result handler steps to be configured via step_metadata parameter:
|
|
528
|
+
# step.on(condition,
|
|
529
|
+
# utils.echo(data=..., step_metadata=StepMetadata(
|
|
530
|
+
# key="my_step",
|
|
531
|
+
# output_behavior={"field": OutputBehaviorType.MERGE}
|
|
532
|
+
# ))
|
|
533
|
+
# )
|
|
534
|
+
step_config = _extract_step_config_from_action(action)
|
|
535
|
+
step_actions.append(Step(**step_config))
|
|
536
|
+
|
|
537
|
+
# Create a result handler and add it to this step
|
|
538
|
+
handler = ResultHandler(
|
|
539
|
+
type=handler_type,
|
|
540
|
+
if_conditions=condition,
|
|
541
|
+
steps=step_actions,
|
|
542
|
+
)
|
|
543
|
+
self.result_handlers.append(handler)
|
|
544
|
+
return self
|
|
545
|
+
|
|
546
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
547
|
+
"""Convert to dict format expected by backend."""
|
|
548
|
+
# Process decorator handlers before serialization
|
|
549
|
+
self._process_decorator_handlers()
|
|
550
|
+
|
|
551
|
+
# Use model_dump but exclude problematic circular reference fields
|
|
552
|
+
result = self.model_dump(
|
|
553
|
+
exclude={
|
|
554
|
+
"agent",
|
|
555
|
+
"result_handler",
|
|
556
|
+
"_decorator_handlers",
|
|
557
|
+
"action",
|
|
558
|
+
"depends_on",
|
|
559
|
+
},
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Filter out empty/None optional fields to match export behavior
|
|
563
|
+
if result.get("ui_content_type") in [None, ""]:
|
|
564
|
+
result.pop("ui_content_type", None)
|
|
565
|
+
if result.get("history_content_type") in [None, ""]:
|
|
566
|
+
result.pop("history_content_type", None)
|
|
567
|
+
if result.get("history_role") in [None, ""]:
|
|
568
|
+
result.pop("history_role", None)
|
|
569
|
+
if result.get("parameter_hydration_behaviour") is None:
|
|
570
|
+
result.pop("parameter_hydration_behaviour", None)
|
|
571
|
+
|
|
572
|
+
# Add action information without circular references
|
|
573
|
+
result["action_type"] = self.get_action_type()
|
|
574
|
+
result["parameters"] = self.extract_action_parameters()
|
|
575
|
+
|
|
576
|
+
# Add depends_on only if there are actual dependencies
|
|
577
|
+
if self.depends_on:
|
|
578
|
+
# Convert Step objects to their keys
|
|
579
|
+
deps = []
|
|
580
|
+
for dep in self.depends_on:
|
|
581
|
+
if isinstance(dep, Step):
|
|
582
|
+
# If it's a Step object, use its key
|
|
583
|
+
if dep.key:
|
|
584
|
+
deps.append(dep.key)
|
|
585
|
+
elif isinstance(dep, str):
|
|
586
|
+
# If it's already a string (step key), use it directly
|
|
587
|
+
deps.append(dep)
|
|
588
|
+
result["depends_on"] = deps
|
|
589
|
+
|
|
590
|
+
# Always include result_handlers, even if they were excluded by exclude_unset
|
|
591
|
+
# This ensures that result handlers added via .on() method are included
|
|
592
|
+
result["result_handlers"] = []
|
|
593
|
+
if self.result_handlers:
|
|
594
|
+
converted_handlers: List[Any] = []
|
|
595
|
+
for handler in self.result_handlers:
|
|
596
|
+
if hasattr(handler, "to_dict"):
|
|
597
|
+
converted_handlers.append(handler.to_dict())
|
|
598
|
+
elif isinstance(handler, dict):
|
|
599
|
+
# Already a dict, convert conditions if needed
|
|
600
|
+
handler_dict = cast(Dict[str, Any], handler)
|
|
601
|
+
if handler_dict.get("if_conditions") and hasattr(
|
|
602
|
+
handler_dict["if_conditions"], "to_dict"
|
|
603
|
+
):
|
|
604
|
+
handler_dict["if_conditions"] = handler_dict[
|
|
605
|
+
"if_conditions"
|
|
606
|
+
].to_dict()
|
|
607
|
+
converted_handlers.append(handler_dict)
|
|
608
|
+
else:
|
|
609
|
+
converted_handlers.append(handler)
|
|
610
|
+
result["result_handlers"] = converted_handlers
|
|
611
|
+
|
|
612
|
+
# Convert ExecutionMode objects to dictionaries
|
|
613
|
+
if self.execution_mode is not None:
|
|
614
|
+
if hasattr(self.execution_mode, "to_dict") and callable(
|
|
615
|
+
getattr(self.execution_mode, "to_dict")
|
|
616
|
+
):
|
|
617
|
+
exec_mode_obj = cast(ExecutionModeProtocol, self.execution_mode)
|
|
618
|
+
result["execution_mode"] = exec_mode_obj.to_dict()
|
|
619
|
+
elif isinstance(self.execution_mode, dict):
|
|
620
|
+
result["execution_mode"] = self.execution_mode
|
|
621
|
+
else:
|
|
622
|
+
# Handle string mode types or other formats
|
|
623
|
+
result["execution_mode"] = self.execution_mode
|
|
624
|
+
|
|
625
|
+
# Recursively convert any other condition objects to dictionaries
|
|
626
|
+
def convert_conditions(obj: Any) -> Any:
|
|
627
|
+
"""Recursively convert condition objects to dictionaries."""
|
|
628
|
+
|
|
629
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
630
|
+
return obj
|
|
631
|
+
elif isinstance(obj, Enum):
|
|
632
|
+
# Convert enum values to their string values for JSON serialization
|
|
633
|
+
return obj.value
|
|
634
|
+
elif hasattr(obj, "to_dict"):
|
|
635
|
+
return obj.to_dict()
|
|
636
|
+
elif isinstance(obj, list):
|
|
637
|
+
return [convert_conditions(item) for item in obj]
|
|
638
|
+
elif isinstance(obj, dict):
|
|
639
|
+
# Handle enum keys in dictionaries
|
|
640
|
+
converted_dict: Dict[str, Any] = {}
|
|
641
|
+
obj_dict = cast(Dict[Any, Any], obj)
|
|
642
|
+
for key, value in obj_dict.items():
|
|
643
|
+
# Convert enum keys to strings
|
|
644
|
+
str_key: str
|
|
645
|
+
if isinstance(key, Enum):
|
|
646
|
+
str_key = str(key.value)
|
|
647
|
+
else:
|
|
648
|
+
str_key = str(key)
|
|
649
|
+
converted_dict[str_key] = convert_conditions(value)
|
|
650
|
+
return converted_dict
|
|
651
|
+
else:
|
|
652
|
+
return obj
|
|
653
|
+
|
|
654
|
+
# Apply condition conversion to all fields
|
|
655
|
+
for key, value in result.items():
|
|
656
|
+
result[key] = convert_conditions(value)
|
|
657
|
+
|
|
658
|
+
# Rename output_behavior to output_behaviour for Go backend compatibility
|
|
659
|
+
# Python SDK uses American spelling (output_behavior) but Go backend expects
|
|
660
|
+
# British spelling (output_behaviour). This ensures correct serialization.
|
|
661
|
+
if "output_behavior" in result:
|
|
662
|
+
result["output_behaviour"] = result.pop("output_behavior")
|
|
663
|
+
|
|
664
|
+
# Recursively sort all dictionaries for deterministic serialization
|
|
665
|
+
def sort_dict_recursively(obj: Any) -> Any:
|
|
666
|
+
"""Recursively sort all dictionaries to ensure deterministic JSON output."""
|
|
667
|
+
if isinstance(obj, dict):
|
|
668
|
+
# Convert keys to strings for sorting to handle mixed types (enums + strings)
|
|
669
|
+
def sort_key(item: Tuple[Any, Any]) -> str:
|
|
670
|
+
k, v = item
|
|
671
|
+
if hasattr(k, "value"): # Enum
|
|
672
|
+
return str(k.value)
|
|
673
|
+
return str(k)
|
|
674
|
+
|
|
675
|
+
obj_dict = cast(Dict[Any, Any], obj)
|
|
676
|
+
return dict(
|
|
677
|
+
sorted(
|
|
678
|
+
((k, sort_dict_recursively(v)) for k, v in obj_dict.items()),
|
|
679
|
+
key=sort_key,
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
elif isinstance(obj, list):
|
|
683
|
+
return [sort_dict_recursively(item) for item in obj]
|
|
684
|
+
else:
|
|
685
|
+
return obj
|
|
686
|
+
|
|
687
|
+
result = sort_dict_recursively(result)
|
|
688
|
+
return cast(Dict[str, Any], result)
|
|
689
|
+
|
|
690
|
+
def _process_decorator_handlers(self) -> None:
|
|
691
|
+
"""Process decorator handlers and convert them to ResultHandler objects."""
|
|
692
|
+
if not hasattr(self, "_decorator_handlers") or not self._decorator_handlers:
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
# Get current result_handlers list or create new one
|
|
696
|
+
current_handlers = list(self.result_handlers) if self.result_handlers else []
|
|
697
|
+
|
|
698
|
+
for condition, _ in self._decorator_handlers:
|
|
699
|
+
# Create a simple handler that calls the function
|
|
700
|
+
# For now, we'll create a basic handler structure
|
|
701
|
+
# In a full implementation, you'd want to analyze the function and create
|
|
702
|
+
# appropriate steps
|
|
703
|
+
handler = ResultHandler(
|
|
704
|
+
type=HandlerType.FINAL,
|
|
705
|
+
if_conditions=condition,
|
|
706
|
+
output_content_type=OutputContentType.TEXT,
|
|
707
|
+
steps=[], # Would need to convert function to steps
|
|
708
|
+
)
|
|
709
|
+
current_handlers.append(handler)
|
|
710
|
+
|
|
711
|
+
# Update the result_handlers field
|
|
712
|
+
self.result_handlers = current_handlers
|
|
713
|
+
|
|
714
|
+
# Clear processed handlers
|
|
715
|
+
self._decorator_handlers.clear()
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class StepOutput(BaseModel):
|
|
719
|
+
"""Represents the output of a step for type-safe access."""
|
|
720
|
+
|
|
721
|
+
step_key: str
|
|
722
|
+
|
|
723
|
+
def __init__(self, **data: Any):
|
|
724
|
+
super().__init__(**data)
|
|
725
|
+
# Use object attribute instead of Pydantic field
|
|
726
|
+
object.__setattr__(self, "_expected_fields", {})
|
|
727
|
+
|
|
728
|
+
def __getitem__(self, key: str) -> str:
|
|
729
|
+
"""Allow bracket notation for accessing output fields."""
|
|
730
|
+
# Track that this field is being accessed for validation
|
|
731
|
+
if hasattr(self, "_expected_fields"):
|
|
732
|
+
expected_fields = getattr(self, "_expected_fields", {})
|
|
733
|
+
expected_fields[key] = (
|
|
734
|
+
None # Will be populated with actual type during execution
|
|
735
|
+
)
|
|
736
|
+
return f"{{{{{self.step_key}.{key}}}}}"
|
|
737
|
+
|
|
738
|
+
def __getattr__(self, name: str) -> str:
|
|
739
|
+
"""Allow dot notation for accessing output fields."""
|
|
740
|
+
# Avoid infinite recursion for Pydantic internal attributes
|
|
741
|
+
if name.startswith("_") or name in {"step_key", "model_fields", "model_config"}:
|
|
742
|
+
raise AttributeError(
|
|
743
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Track that this field is being accessed for validation
|
|
747
|
+
if hasattr(self, "_expected_fields"):
|
|
748
|
+
expected_fields = getattr(self, "_expected_fields", {})
|
|
749
|
+
expected_fields[name] = (
|
|
750
|
+
None # Will be populated with actual type during execution
|
|
751
|
+
)
|
|
752
|
+
return f"{{{{{self.step_key}.{name}}}}}"
|
|
753
|
+
|
|
754
|
+
def get_expected_fields(self) -> Dict[str, Any]:
|
|
755
|
+
"""Get the fields that have been accessed on this step output."""
|
|
756
|
+
return getattr(self, "_expected_fields", {})
|
|
757
|
+
|
|
758
|
+
def validate_field_access(self, available_fields: Dict[str, Any]) -> List[str]:
|
|
759
|
+
"""Validate that all accessed fields are available in the step result."""
|
|
760
|
+
errors = []
|
|
761
|
+
expected = self.get_expected_fields()
|
|
762
|
+
|
|
763
|
+
for field_name in expected.keys():
|
|
764
|
+
if field_name not in available_fields:
|
|
765
|
+
errors.append(
|
|
766
|
+
f"Step '{self.step_key}' output field '{field_name}' is not available"
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
return errors
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
class ResultHandler(BaseModel):
|
|
773
|
+
"""Result handler definition that matches the Go backend structure."""
|
|
774
|
+
|
|
775
|
+
type: HandlerType = HandlerType.FINAL
|
|
776
|
+
if_conditions: Optional[Any] = None # Can be condition objects or None
|
|
777
|
+
output_content_type: OutputContentType = OutputContentType.TEXT
|
|
778
|
+
history_content_type: Optional[str] = None
|
|
779
|
+
ui_content_type: Optional[str] = None
|
|
780
|
+
steps: List[Step] = Field(default_factory=list)
|
|
781
|
+
|
|
782
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
783
|
+
"""Convert to dict format expected by backend."""
|
|
784
|
+
# Convert Step objects in steps list to dictionaries BEFORE model_dump()
|
|
785
|
+
# because model_dump() will convert Step objects to dicts but won't call to_dict()
|
|
786
|
+
converted_steps = []
|
|
787
|
+
if self.steps:
|
|
788
|
+
for step in self.steps:
|
|
789
|
+
if hasattr(step, "to_dict"):
|
|
790
|
+
step_dict = step.to_dict()
|
|
791
|
+
# DO NOT auto-generate keys for result handler steps
|
|
792
|
+
# Only include keys if explicitly set to preserve roundtrip
|
|
793
|
+
# parity
|
|
794
|
+
|
|
795
|
+
# Ensure bot_output_visibility is preserved (defaults to hidden for
|
|
796
|
+
# result handler steps)
|
|
797
|
+
if "bot_output_visibility" not in step_dict:
|
|
798
|
+
step_dict["bot_output_visibility"] = "hidden"
|
|
799
|
+
|
|
800
|
+
converted_steps.append(step_dict)
|
|
801
|
+
else:
|
|
802
|
+
# Convert step to dict if it doesn't have to_dict method
|
|
803
|
+
if isinstance(step, dict):
|
|
804
|
+
converted_steps.append(step)
|
|
805
|
+
else:
|
|
806
|
+
# Fallback: convert to dict using model_dump if available
|
|
807
|
+
if hasattr(step, "model_dump"):
|
|
808
|
+
converted_steps.append(step.model_dump())
|
|
809
|
+
else:
|
|
810
|
+
converted_steps.append({})
|
|
811
|
+
|
|
812
|
+
# Use model_dump but exclude steps, then add our properly converted steps
|
|
813
|
+
result = self.model_dump(exclude={"steps"})
|
|
814
|
+
result["steps"] = converted_steps
|
|
815
|
+
|
|
816
|
+
# Filter out empty/None optional fields to match export behavior
|
|
817
|
+
if result.get("ui_content_type") in [None, ""]:
|
|
818
|
+
result.pop("ui_content_type", None)
|
|
819
|
+
if result.get("history_content_type") in [None, ""]:
|
|
820
|
+
result.pop("history_content_type", None)
|
|
821
|
+
if result.get("parameter_hydration_behaviour") is None:
|
|
822
|
+
result.pop("parameter_hydration_behaviour", None)
|
|
823
|
+
|
|
824
|
+
# Convert condition objects to dictionaries if needed
|
|
825
|
+
if self.if_conditions is not None and hasattr(self.if_conditions, "to_dict"):
|
|
826
|
+
result["if_conditions"] = self.if_conditions.to_dict()
|
|
827
|
+
|
|
828
|
+
# Convert enum values to their string values (both keys and values)
|
|
829
|
+
converted_result = {}
|
|
830
|
+
for key, value in result.items():
|
|
831
|
+
# Convert enum keys to strings
|
|
832
|
+
if isinstance(key, Enum):
|
|
833
|
+
key = key.value
|
|
834
|
+
# Convert enum values to strings
|
|
835
|
+
if isinstance(value, Enum):
|
|
836
|
+
value = value.value
|
|
837
|
+
converted_result[key] = value
|
|
838
|
+
result = converted_result
|
|
839
|
+
|
|
840
|
+
return result
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
class SecretsDict(Dict[str, Any]):
|
|
844
|
+
"""A dictionary wrapper that provides .get() method for secrets access."""
|
|
845
|
+
|
|
846
|
+
def __init__(self, secrets_data: Optional[Dict[str, Any]] = None):
|
|
847
|
+
super().__init__(secrets_data or {})
|
|
848
|
+
|
|
849
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
850
|
+
"""Get decrypted secrets for a specific resource/service key."""
|
|
851
|
+
return super().get(key, default)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
class ParametersDict(Dict[str, Any]):
|
|
855
|
+
"""A dictionary wrapper that provides .get() method for parameters access."""
|
|
856
|
+
|
|
857
|
+
def __init__(self, parameters_data: Optional[Dict[str, Any]] = None):
|
|
858
|
+
super().__init__(parameters_data or {})
|
|
859
|
+
|
|
860
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
861
|
+
"""Get a step parameter with optional default."""
|
|
862
|
+
return super().get(key, default)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
class StepContext(BaseModel):
|
|
866
|
+
"""Context available to step functions with type-safe access
|
|
867
|
+
|
|
868
|
+
Provides access to:
|
|
869
|
+
- User query and parameters
|
|
870
|
+
- Previous step results (state)
|
|
871
|
+
- Available resources (datasets, APIs)
|
|
872
|
+
- Encrypted secrets
|
|
873
|
+
- System information
|
|
874
|
+
"""
|
|
875
|
+
|
|
876
|
+
# Core user input
|
|
877
|
+
query: Optional[str] = None
|
|
878
|
+
|
|
879
|
+
# Previous step results and state
|
|
880
|
+
state: Dict[str, Any] = Field(default_factory=dict)
|
|
881
|
+
steps: Dict[str, Any] = Field(default_factory=dict) # Alias for state.steps
|
|
882
|
+
|
|
883
|
+
# Resources and data access
|
|
884
|
+
resources: List[Dict[str, Any]] = Field(default_factory=list)
|
|
885
|
+
resource_definitions: Optional[Dict[str, Any]] = None
|
|
886
|
+
|
|
887
|
+
# Security and credentials - raw data
|
|
888
|
+
secrets_data: Dict[str, Any] = Field(default_factory=dict)
|
|
889
|
+
|
|
890
|
+
# Step-specific parameters - raw data
|
|
891
|
+
parameters_data: Dict[str, Any] = Field(default_factory=dict)
|
|
892
|
+
|
|
893
|
+
# System context
|
|
894
|
+
system: Dict[str, Any] = Field(default_factory=dict)
|
|
895
|
+
|
|
896
|
+
# Raw context for advanced users
|
|
897
|
+
raw: Dict[str, Any] = Field(default_factory=dict)
|
|
898
|
+
|
|
899
|
+
def __init__(self, **data: Any):
|
|
900
|
+
# Extract secrets and parameters from input data
|
|
901
|
+
secrets_data = data.pop("secrets", {})
|
|
902
|
+
parameters_data = data.pop("parameters", {})
|
|
903
|
+
|
|
904
|
+
# Set the internal data fields
|
|
905
|
+
data["secrets_data"] = secrets_data
|
|
906
|
+
data["parameters_data"] = parameters_data
|
|
907
|
+
|
|
908
|
+
super().__init__(**data)
|
|
909
|
+
|
|
910
|
+
# Create wrapper objects for secrets and parameters
|
|
911
|
+
self._secrets_wrapper = SecretsDict(self.secrets_data)
|
|
912
|
+
self._parameters_wrapper = ParametersDict(self.parameters_data)
|
|
913
|
+
|
|
914
|
+
@property
|
|
915
|
+
def secrets(self) -> SecretsDict:
|
|
916
|
+
"""Get secrets wrapper that supports .get() method."""
|
|
917
|
+
return self._secrets_wrapper
|
|
918
|
+
|
|
919
|
+
@property
|
|
920
|
+
def parameters(self) -> ParametersDict:
|
|
921
|
+
"""Get parameters wrapper that supports .get() method."""
|
|
922
|
+
return self._parameters_wrapper
|
|
923
|
+
|
|
924
|
+
def __getitem__(self, key: str) -> Any:
|
|
925
|
+
"""Allow bracket notation access to state"""
|
|
926
|
+
try:
|
|
927
|
+
state_value = super().__getattribute__("state")
|
|
928
|
+
return state_value.get(key) if isinstance(state_value, dict) else None
|
|
929
|
+
except AttributeError:
|
|
930
|
+
return None
|
|
931
|
+
|
|
932
|
+
def __getattr__(self, name: str) -> Any:
|
|
933
|
+
"""Allow dot notation access to state"""
|
|
934
|
+
# Avoid infinite recursion for Pydantic internal attributes
|
|
935
|
+
if name.startswith("_") or name in {
|
|
936
|
+
"state",
|
|
937
|
+
"steps",
|
|
938
|
+
"resources",
|
|
939
|
+
"secrets",
|
|
940
|
+
"parameters",
|
|
941
|
+
"system",
|
|
942
|
+
"raw",
|
|
943
|
+
"query",
|
|
944
|
+
"resource_definitions",
|
|
945
|
+
}:
|
|
946
|
+
raise AttributeError(
|
|
947
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
try:
|
|
951
|
+
state_value = super().__getattribute__("state")
|
|
952
|
+
if isinstance(state_value, dict) and name in state_value:
|
|
953
|
+
return state_value[name]
|
|
954
|
+
except AttributeError:
|
|
955
|
+
pass
|
|
956
|
+
raise AttributeError(
|
|
957
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
def get_resource(self, key: str) -> Optional[Dict[str, Any]]:
|
|
961
|
+
"""Get a specific resource by key"""
|
|
962
|
+
for resource in self.resources:
|
|
963
|
+
if resource.get("key") == key:
|
|
964
|
+
return resource
|
|
965
|
+
return None
|
|
966
|
+
|
|
967
|
+
def get_secret(self, key: str) -> Optional[Any]:
|
|
968
|
+
"""Get decrypted secrets for a specific resource/service"""
|
|
969
|
+
return self.secrets.get(key)
|
|
970
|
+
|
|
971
|
+
def get_parameter(self, key: str, default: Any = None) -> Any:
|
|
972
|
+
"""Get a step parameter with optional default"""
|
|
973
|
+
return self.parameters.get(key, default)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
class StepResult(BaseModel):
|
|
977
|
+
"""Result from executing a workflow step."""
|
|
978
|
+
|
|
979
|
+
success: bool
|
|
980
|
+
data: Any = None
|
|
981
|
+
error: Optional[str] = None
|
|
982
|
+
step_name: str
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
class Agent(BaseModel):
|
|
986
|
+
"""Main agent class for defining AI workflows."""
|
|
987
|
+
|
|
988
|
+
name: str
|
|
989
|
+
key: Optional[str] = None
|
|
990
|
+
description: Optional[str] = None
|
|
991
|
+
persona: Optional[str] = None
|
|
992
|
+
visibility: str = "public"
|
|
993
|
+
running_status: Optional[Union[str, TemplateString]] = None
|
|
994
|
+
finished_status: Optional[Union[str, TemplateString]] = None
|
|
995
|
+
running_status_context: Optional[Union[str, TemplateString]] = None
|
|
996
|
+
finished_status_context: Optional[Union[str, TemplateString]] = None
|
|
997
|
+
running_status_prompt: Optional[Union[str, TemplateString]] = None
|
|
998
|
+
finished_status_prompt: Optional[Union[str, TemplateString]] = None
|
|
999
|
+
version: str = "1.0"
|
|
1000
|
+
timeout: Optional[int] = None
|
|
1001
|
+
retry_attempts: int = 0
|
|
1002
|
+
tags: List[str] = Field(default_factory=list)
|
|
1003
|
+
steps: List[Step] = Field(default_factory=list)
|
|
1004
|
+
parameter_definitions: List["ParameterDefinition"] = Field(default_factory=list)
|
|
1005
|
+
|
|
1006
|
+
@field_serializer(
|
|
1007
|
+
"running_status",
|
|
1008
|
+
"finished_status",
|
|
1009
|
+
"running_status_context",
|
|
1010
|
+
"finished_status_context",
|
|
1011
|
+
"running_status_prompt",
|
|
1012
|
+
"finished_status_prompt",
|
|
1013
|
+
mode="wrap",
|
|
1014
|
+
when_used="always",
|
|
1015
|
+
)
|
|
1016
|
+
def serialize_message_fields(
|
|
1017
|
+
self, value: Optional[Union[str, TemplateString]], _info
|
|
1018
|
+
) -> Optional[str]:
|
|
1019
|
+
"""Convert TemplateString to str during serialization."""
|
|
1020
|
+
if isinstance(value, TemplateString):
|
|
1021
|
+
return str(value)
|
|
1022
|
+
return value
|
|
1023
|
+
|
|
1024
|
+
def step(
|
|
1025
|
+
self,
|
|
1026
|
+
action: Any,
|
|
1027
|
+
key: Optional[str] = None,
|
|
1028
|
+
depends_on: Optional[Union[Step, List[Step], str, List[str]]] = None,
|
|
1029
|
+
**kwargs: Any,
|
|
1030
|
+
) -> Step:
|
|
1031
|
+
"""Create a step with cleaner syntax and better defaults.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
action: The action to execute (e.g., codeexec.execute(...))
|
|
1035
|
+
key: Optional step key, auto-generated if not provided
|
|
1036
|
+
depends_on: Step(s) or key(s) this step depends on
|
|
1037
|
+
**kwargs: Additional step configuration
|
|
1038
|
+
|
|
1039
|
+
Returns:
|
|
1040
|
+
Step: The created step with sensible defaults
|
|
1041
|
+
"""
|
|
1042
|
+
# Extract step_metadata from the action if present
|
|
1043
|
+
extracted_config = _extract_step_config_from_action(action)
|
|
1044
|
+
|
|
1045
|
+
# Auto-generate key if not provided (either via param or step_metadata)
|
|
1046
|
+
if key is None and "key" not in extracted_config:
|
|
1047
|
+
key = f"step_{len(self.steps) + 1}"
|
|
1048
|
+
|
|
1049
|
+
# Set sensible defaults
|
|
1050
|
+
step_config = {
|
|
1051
|
+
"agent": self,
|
|
1052
|
+
"key": key,
|
|
1053
|
+
"action": action,
|
|
1054
|
+
"depends_on": (
|
|
1055
|
+
[depends_on]
|
|
1056
|
+
if depends_on and not isinstance(depends_on, list)
|
|
1057
|
+
else depends_on
|
|
1058
|
+
),
|
|
1059
|
+
"user_output_visibility": OutputVisibility.VISIBLE,
|
|
1060
|
+
"bot_output_visibility": OutputVisibility.HIDDEN,
|
|
1061
|
+
"output_content_type": OutputContentType.TEXT,
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
# First merge extracted_config (from step_metadata), then kwargs
|
|
1065
|
+
# This allows kwargs to override step_metadata if needed
|
|
1066
|
+
step_config.update(extracted_config)
|
|
1067
|
+
step_config.update(kwargs)
|
|
1068
|
+
|
|
1069
|
+
step = Step(**step_config)
|
|
1070
|
+
return step
|
|
1071
|
+
|
|
1072
|
+
def exec(
|
|
1073
|
+
self,
|
|
1074
|
+
step_metadata: Optional[StepMetadata] = None,
|
|
1075
|
+
**codeexec_params: Any,
|
|
1076
|
+
) -> Callable[..., Step]:
|
|
1077
|
+
"""Create a codeexec step using decorator syntax.
|
|
1078
|
+
|
|
1079
|
+
This method is designed to be used as a decorator for functions that implement
|
|
1080
|
+
code execution logic. It creates a codeexec.execute action with the provided
|
|
1081
|
+
parameters and step metadata.
|
|
1082
|
+
|
|
1083
|
+
Args:
|
|
1084
|
+
step_metadata: Step configuration (key, depends_on, etc.)
|
|
1085
|
+
**codeexec_params: Parameters for codeexec.execute (entrypoint, parameters,
|
|
1086
|
+
resources, etc.)
|
|
1087
|
+
|
|
1088
|
+
Returns:
|
|
1089
|
+
Callable: Decorator function that creates and returns a Step
|
|
1090
|
+
"""
|
|
1091
|
+
|
|
1092
|
+
def decorator(func: Callable[..., Any]) -> Step:
|
|
1093
|
+
# Import here to avoid circular imports
|
|
1094
|
+
from .actions import codeexec
|
|
1095
|
+
|
|
1096
|
+
# Create the codeexec.execute action with the provided parameters
|
|
1097
|
+
action = codeexec.execute(**codeexec_params)
|
|
1098
|
+
|
|
1099
|
+
# Extract step metadata or use defaults
|
|
1100
|
+
if step_metadata:
|
|
1101
|
+
step_config = {
|
|
1102
|
+
"agent": self,
|
|
1103
|
+
"action": action,
|
|
1104
|
+
"key": step_metadata.key or func.__name__,
|
|
1105
|
+
"depends_on": step_metadata.depends_on,
|
|
1106
|
+
"execution_mode": step_metadata.execution_mode,
|
|
1107
|
+
"output_behavior": step_metadata.output_behavior,
|
|
1108
|
+
"output_channels": step_metadata.output_channels,
|
|
1109
|
+
"output_content_type": step_metadata.output_content_type,
|
|
1110
|
+
"history_content_type": step_metadata.history_content_type,
|
|
1111
|
+
"history_role": step_metadata.history_role,
|
|
1112
|
+
"ui_content_type": step_metadata.ui_content_type,
|
|
1113
|
+
"user_output_visibility": step_metadata.user_output_visibility,
|
|
1114
|
+
"bot_output_visibility": step_metadata.bot_output_visibility,
|
|
1115
|
+
"running_status": step_metadata.running_status,
|
|
1116
|
+
"finished_status": step_metadata.finished_status,
|
|
1117
|
+
"parameter_hydration_behaviour": step_metadata.parameter_hydration_behaviour,
|
|
1118
|
+
}
|
|
1119
|
+
else:
|
|
1120
|
+
step_config = {
|
|
1121
|
+
"agent": self,
|
|
1122
|
+
"action": action,
|
|
1123
|
+
"key": func.__name__,
|
|
1124
|
+
"user_output_visibility": OutputVisibility.VISIBLE,
|
|
1125
|
+
"bot_output_visibility": OutputVisibility.HIDDEN,
|
|
1126
|
+
"output_content_type": OutputContentType.TEXT,
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
# Remove None values and extract action separately
|
|
1130
|
+
filtered_config = {
|
|
1131
|
+
k: v for k, v in step_config.items() if v is not None and k != "action"
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
# Create step with the codeexec action, not the function
|
|
1135
|
+
step = Step(action=action, step=None, **filtered_config)
|
|
1136
|
+
|
|
1137
|
+
# Store the function name on the step for runtime extraction
|
|
1138
|
+
if hasattr(step, "__dict__"):
|
|
1139
|
+
step.__dict__["__name__"] = func.__name__
|
|
1140
|
+
|
|
1141
|
+
return step
|
|
1142
|
+
|
|
1143
|
+
return decorator
|
|
1144
|
+
|
|
1145
|
+
def add_step(self, step: Step) -> None:
|
|
1146
|
+
"""Add a step to this agent if it's not already present."""
|
|
1147
|
+
# Only add if the step is not already in the list
|
|
1148
|
+
if step in self.steps:
|
|
1149
|
+
return
|
|
1150
|
+
|
|
1151
|
+
# Create a new list with the existing steps plus the new one
|
|
1152
|
+
current_steps = list(self.steps) if self.steps else []
|
|
1153
|
+
current_steps.append(step)
|
|
1154
|
+
self.steps = current_steps
|
|
1155
|
+
|
|
1156
|
+
def get_step(self, key: str) -> Optional[Step]:
|
|
1157
|
+
"""Get a step by its key."""
|
|
1158
|
+
for step in self.steps:
|
|
1159
|
+
if step.key == key:
|
|
1160
|
+
return step
|
|
1161
|
+
return None
|
|
1162
|
+
|
|
1163
|
+
def to_json(self) -> str:
|
|
1164
|
+
"""Export to JSON format expected by Go backend."""
|
|
1165
|
+
export_data = {
|
|
1166
|
+
"bot": {
|
|
1167
|
+
"Name": self.name,
|
|
1168
|
+
"Key": self.key,
|
|
1169
|
+
"Description": self.description or "",
|
|
1170
|
+
"Visibility": self.visibility,
|
|
1171
|
+
"Persona": self.persona, # Export as string or null, not object
|
|
1172
|
+
"RunningStatus": self.running_status,
|
|
1173
|
+
"FinishedStatus": self.finished_status,
|
|
1174
|
+
"RunningStatusContext": self.running_status_context,
|
|
1175
|
+
"FinishedStatusContext": self.finished_status_context,
|
|
1176
|
+
"RunningStatusPrompt": self.running_status_prompt,
|
|
1177
|
+
"FinishedStatusPrompt": self.finished_status_prompt,
|
|
1178
|
+
"Source": "python",
|
|
1179
|
+
},
|
|
1180
|
+
"parameter_definitions": self.parameter_definitions,
|
|
1181
|
+
"steps": [step.to_dict() for step in self.steps],
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
import json
|
|
1185
|
+
|
|
1186
|
+
return json.dumps(export_data, indent=2)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
class ExecutionMode(BaseModel):
|
|
1190
|
+
"""Execution mode matching Go backend structure."""
|
|
1191
|
+
|
|
1192
|
+
mode: ExecutionModeType = ExecutionModeType.ALL
|
|
1193
|
+
data: Optional[Any] = None
|
|
1194
|
+
if_condition: Optional[Any] = None
|
|
1195
|
+
|
|
1196
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1197
|
+
"""Convert to dict format expected by backend."""
|
|
1198
|
+
result: Dict[str, Any] = {"mode": self.mode}
|
|
1199
|
+
if self.data is not None:
|
|
1200
|
+
result["data"] = self.data
|
|
1201
|
+
# Always include if_condition to maintain parity with backend structure
|
|
1202
|
+
result["if_condition"] = self.if_condition
|
|
1203
|
+
return result
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
class Prompt(BaseModel):
|
|
1207
|
+
"""A prompt that can be loaded from a .prompt file or defined inline.
|
|
1208
|
+
|
|
1209
|
+
This class provides a clean way to manage prompts in agent code,
|
|
1210
|
+
supporting both inline strings and external .prompt files.
|
|
1211
|
+
"""
|
|
1212
|
+
|
|
1213
|
+
content: str = Field(..., description="The prompt content")
|
|
1214
|
+
filename: Optional[str] = Field(
|
|
1215
|
+
None, description="Source filename if loaded from file"
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
def __init__(
|
|
1219
|
+
self, content: Optional[str] = None, filename: Optional[str] = None, **data: Any
|
|
1220
|
+
):
|
|
1221
|
+
"""Initialize a Prompt.
|
|
1222
|
+
|
|
1223
|
+
Args:
|
|
1224
|
+
content: Direct prompt content
|
|
1225
|
+
filename: Path to .prompt file to load
|
|
1226
|
+
**data: Additional model data
|
|
1227
|
+
"""
|
|
1228
|
+
if content is not None and filename is not None:
|
|
1229
|
+
raise ValueError("Cannot specify both 'content' and 'filename' parameters")
|
|
1230
|
+
|
|
1231
|
+
if content is None and filename is None:
|
|
1232
|
+
raise ValueError("Must specify either 'content' or 'filename' parameter")
|
|
1233
|
+
|
|
1234
|
+
if filename is not None:
|
|
1235
|
+
# Load content from file
|
|
1236
|
+
content = self._load_from_file(filename)
|
|
1237
|
+
data.update({"content": content, "filename": filename})
|
|
1238
|
+
else:
|
|
1239
|
+
data.update({"content": content})
|
|
1240
|
+
|
|
1241
|
+
super().__init__(**data)
|
|
1242
|
+
|
|
1243
|
+
@classmethod
|
|
1244
|
+
def from_file(cls, filename: str, base_path: Optional[str] = None) -> "Prompt":
|
|
1245
|
+
"""Load a prompt from a .prompt file.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
filename: Path to the .prompt file
|
|
1249
|
+
base_path: Base directory to resolve relative paths (defaults to cwd)
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
Prompt: Loaded prompt instance
|
|
1253
|
+
"""
|
|
1254
|
+
if base_path is None:
|
|
1255
|
+
base_path = os.getcwd()
|
|
1256
|
+
|
|
1257
|
+
file_path = Path(base_path) / filename
|
|
1258
|
+
|
|
1259
|
+
try:
|
|
1260
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
1261
|
+
content = f.read().strip()
|
|
1262
|
+
return cls(content=content)
|
|
1263
|
+
except FileNotFoundError:
|
|
1264
|
+
raise FileNotFoundError(f"Prompt file not found: {file_path}")
|
|
1265
|
+
except Exception as e:
|
|
1266
|
+
raise RuntimeError(f"Error reading prompt file {file_path}: {e}")
|
|
1267
|
+
|
|
1268
|
+
def _load_from_file(self, filename: str) -> str:
|
|
1269
|
+
"""Load content from a .prompt file."""
|
|
1270
|
+
return self.from_file(filename).content
|
|
1271
|
+
|
|
1272
|
+
def __str__(self) -> str:
|
|
1273
|
+
"""Return the prompt content when used as a string."""
|
|
1274
|
+
return self.content
|
|
1275
|
+
|
|
1276
|
+
def __call__(self) -> str:
|
|
1277
|
+
"""Allow the prompt to be called like a function, returning content."""
|
|
1278
|
+
return self.content
|
|
1279
|
+
|
|
1280
|
+
def __repr__(self) -> str:
|
|
1281
|
+
"""Return a helpful representation."""
|
|
1282
|
+
if self.filename:
|
|
1283
|
+
return f"Prompt(filename='{self.filename}')"
|
|
1284
|
+
else:
|
|
1285
|
+
preview = (
|
|
1286
|
+
self.content[:50] + "..." if len(self.content) > 50 else self.content
|
|
1287
|
+
)
|
|
1288
|
+
return f"Prompt(content='{preview}')"
|
|
1289
|
+
|
|
1290
|
+
@classmethod
|
|
1291
|
+
def load_from_directory(
|
|
1292
|
+
cls, directory: str, base_path: Optional[str] = None
|
|
1293
|
+
) -> Dict[str, "Prompt"]:
|
|
1294
|
+
"""Load all .prompt files from a directory as Prompt objects.
|
|
1295
|
+
|
|
1296
|
+
Args:
|
|
1297
|
+
directory: Directory containing .prompt files
|
|
1298
|
+
base_path: Base directory to resolve relative paths (defaults to smart detection)
|
|
1299
|
+
|
|
1300
|
+
Returns:
|
|
1301
|
+
Dict[str, Prompt]: Mapping of filename (without extension) to Prompt objects
|
|
1302
|
+
"""
|
|
1303
|
+
if base_path is None:
|
|
1304
|
+
# Use stack inspection to determine the calling file's directory
|
|
1305
|
+
# This allows agents to load prompts from their own directory
|
|
1306
|
+
# even when executed from a different working directory
|
|
1307
|
+
import inspect
|
|
1308
|
+
|
|
1309
|
+
frame = inspect.currentframe()
|
|
1310
|
+
try:
|
|
1311
|
+
# Get the caller's frame (skip current frame)
|
|
1312
|
+
if frame and frame.f_back:
|
|
1313
|
+
caller_frame = frame.f_back
|
|
1314
|
+
caller_file = caller_frame.f_code.co_filename
|
|
1315
|
+
caller_dir = Path(caller_file).parent
|
|
1316
|
+
# Check if the caller is in an agent directory structure
|
|
1317
|
+
# (i.e., the calling file is agent.py in a directory under erdo-agents)
|
|
1318
|
+
if caller_file.endswith("agent.py") and "erdo-agents" in str(
|
|
1319
|
+
caller_dir
|
|
1320
|
+
):
|
|
1321
|
+
# The caller is an agent.py file, use its directory
|
|
1322
|
+
base_path = str(caller_dir)
|
|
1323
|
+
else:
|
|
1324
|
+
# Fall back to current working directory
|
|
1325
|
+
base_path = os.getcwd()
|
|
1326
|
+
else:
|
|
1327
|
+
# Fall back to current working directory
|
|
1328
|
+
base_path = os.getcwd()
|
|
1329
|
+
finally:
|
|
1330
|
+
del frame # Avoid reference cycles
|
|
1331
|
+
|
|
1332
|
+
dir_path = Path(base_path) / directory
|
|
1333
|
+
|
|
1334
|
+
if not dir_path.exists() or not dir_path.is_dir():
|
|
1335
|
+
# Provide a more helpful error message
|
|
1336
|
+
cwd = os.getcwd()
|
|
1337
|
+
caller_path = None
|
|
1338
|
+
import inspect
|
|
1339
|
+
|
|
1340
|
+
frame = inspect.currentframe()
|
|
1341
|
+
try:
|
|
1342
|
+
if frame and frame.f_back:
|
|
1343
|
+
caller_file = frame.f_back.f_code.co_filename
|
|
1344
|
+
caller_path = str(Path(caller_file).parent)
|
|
1345
|
+
finally:
|
|
1346
|
+
del frame
|
|
1347
|
+
|
|
1348
|
+
error_msg = f"Prompts directory not found: {dir_path}"
|
|
1349
|
+
if caller_path and caller_path != str(Path(base_path)):
|
|
1350
|
+
error_msg += f"\nCalling file: {caller_path}"
|
|
1351
|
+
error_msg += f"\nCurrent working directory: {cwd}"
|
|
1352
|
+
error_msg += f"\nBase path used: {base_path}"
|
|
1353
|
+
raise FileNotFoundError(error_msg)
|
|
1354
|
+
|
|
1355
|
+
prompts = {}
|
|
1356
|
+
for prompt_file in dir_path.glob("*.prompt"):
|
|
1357
|
+
prompt_name = prompt_file.stem # filename without extension
|
|
1358
|
+
relative_path = str(prompt_file.relative_to(base_path))
|
|
1359
|
+
prompts[prompt_name] = cls.from_file(relative_path, base_path)
|
|
1360
|
+
|
|
1361
|
+
return prompts
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
class ConditionDefinition(BaseModel):
|
|
1365
|
+
"""Condition definition for test expectations."""
|
|
1366
|
+
|
|
1367
|
+
type: str # "TextContains", "IsSuccess", etc.
|
|
1368
|
+
path: str # JSONPath or template path to value
|
|
1369
|
+
parameters: Dict[str, Any] = Field(default_factory=dict)
|
|
1370
|
+
|
|
1371
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1372
|
+
"""Convert to dict format expected by backend."""
|
|
1373
|
+
return {"type": self.type, "path": self.path, "parameters": self.parameters}
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
class APIConditionDefinition(BaseModel):
|
|
1377
|
+
"""API condition definition that matches backend expectations."""
|
|
1378
|
+
|
|
1379
|
+
type: str # "TextContains", "IsSuccess", etc.
|
|
1380
|
+
conditions: List[Any] = Field(default_factory=list) # For composite conditions
|
|
1381
|
+
leaf: Dict[str, Any] = Field(default_factory=dict) # Parameters for leaf conditions
|
|
1382
|
+
|
|
1383
|
+
@field_serializer("leaf")
|
|
1384
|
+
def serialize_leaf(self, leaf_data: Dict[str, Any]) -> str:
|
|
1385
|
+
"""Serialize leaf field as JSON string to match Go backend json.RawMessage expectation."""
|
|
1386
|
+
import json
|
|
1387
|
+
|
|
1388
|
+
if leaf_data:
|
|
1389
|
+
# Convert TemplateString objects to strings
|
|
1390
|
+
converted_leaf = {}
|
|
1391
|
+
for key, value in leaf_data.items():
|
|
1392
|
+
if hasattr(value, "template"): # This is a TemplateString
|
|
1393
|
+
converted_leaf[key] = value.template
|
|
1394
|
+
else:
|
|
1395
|
+
converted_leaf[key] = value
|
|
1396
|
+
return json.dumps(converted_leaf)
|
|
1397
|
+
else:
|
|
1398
|
+
# Even if leaf is empty, ensure it's a JSON string
|
|
1399
|
+
return "{}"
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
# Re-export commonly used types for convenience
|
|
1403
|
+
__all__ = [
|
|
1404
|
+
"Agent",
|
|
1405
|
+
"Step",
|
|
1406
|
+
"StepMetadata",
|
|
1407
|
+
"StepContext",
|
|
1408
|
+
"StepResult",
|
|
1409
|
+
"StepOutput",
|
|
1410
|
+
"ResultHandler",
|
|
1411
|
+
"ExecutionMode",
|
|
1412
|
+
"MessageRole",
|
|
1413
|
+
"Tool",
|
|
1414
|
+
"PythonFile",
|
|
1415
|
+
"Prompt",
|
|
1416
|
+
"TemplateString",
|
|
1417
|
+
# Complex types
|
|
1418
|
+
"ExecutionCondition",
|
|
1419
|
+
"ConditionDefinition",
|
|
1420
|
+
# Resource types
|
|
1421
|
+
"BotResource",
|
|
1422
|
+
"Dataset",
|
|
1423
|
+
"DatasetType",
|
|
1424
|
+
# Bot permissions
|
|
1425
|
+
"BotPermissions",
|
|
1426
|
+
"set_bot_public",
|
|
1427
|
+
"set_bot_user_permission",
|
|
1428
|
+
"set_bot_org_permission",
|
|
1429
|
+
"get_bot_permissions",
|
|
1430
|
+
"check_bot_access",
|
|
1431
|
+
]
|