jaf-py 2.5.10__py3-none-any.whl → 2.5.11__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.
- jaf/__init__.py +154 -57
- jaf/a2a/__init__.py +42 -21
- jaf/a2a/agent.py +79 -126
- jaf/a2a/agent_card.py +87 -78
- jaf/a2a/client.py +30 -66
- jaf/a2a/examples/client_example.py +12 -12
- jaf/a2a/examples/integration_example.py +38 -47
- jaf/a2a/examples/server_example.py +56 -53
- jaf/a2a/memory/__init__.py +0 -4
- jaf/a2a/memory/cleanup.py +28 -21
- jaf/a2a/memory/factory.py +155 -133
- jaf/a2a/memory/providers/composite.py +21 -26
- jaf/a2a/memory/providers/in_memory.py +89 -83
- jaf/a2a/memory/providers/postgres.py +117 -115
- jaf/a2a/memory/providers/redis.py +128 -121
- jaf/a2a/memory/serialization.py +77 -87
- jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
- jaf/a2a/memory/tests/test_cleanup.py +211 -94
- jaf/a2a/memory/tests/test_serialization.py +73 -68
- jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
- jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
- jaf/a2a/memory/types.py +91 -53
- jaf/a2a/protocol.py +95 -125
- jaf/a2a/server.py +90 -118
- jaf/a2a/standalone_client.py +30 -43
- jaf/a2a/tests/__init__.py +16 -33
- jaf/a2a/tests/run_tests.py +17 -53
- jaf/a2a/tests/test_agent.py +40 -140
- jaf/a2a/tests/test_client.py +54 -117
- jaf/a2a/tests/test_integration.py +28 -82
- jaf/a2a/tests/test_protocol.py +54 -139
- jaf/a2a/tests/test_types.py +50 -136
- jaf/a2a/types.py +58 -34
- jaf/cli.py +21 -41
- jaf/core/__init__.py +7 -1
- jaf/core/agent_tool.py +93 -72
- jaf/core/analytics.py +257 -207
- jaf/core/checkpoint.py +223 -0
- jaf/core/composition.py +249 -235
- jaf/core/engine.py +817 -519
- jaf/core/errors.py +55 -42
- jaf/core/guardrails.py +276 -202
- jaf/core/handoff.py +47 -31
- jaf/core/parallel_agents.py +69 -75
- jaf/core/performance.py +75 -73
- jaf/core/proxy.py +43 -44
- jaf/core/proxy_helpers.py +24 -27
- jaf/core/regeneration.py +220 -129
- jaf/core/state.py +68 -66
- jaf/core/streaming.py +115 -108
- jaf/core/tool_results.py +111 -101
- jaf/core/tools.py +114 -116
- jaf/core/tracing.py +269 -210
- jaf/core/types.py +371 -151
- jaf/core/workflows.py +209 -168
- jaf/exceptions.py +46 -38
- jaf/memory/__init__.py +1 -6
- jaf/memory/approval_storage.py +54 -77
- jaf/memory/factory.py +4 -4
- jaf/memory/providers/in_memory.py +216 -180
- jaf/memory/providers/postgres.py +216 -146
- jaf/memory/providers/redis.py +173 -116
- jaf/memory/types.py +70 -51
- jaf/memory/utils.py +36 -34
- jaf/plugins/__init__.py +12 -12
- jaf/plugins/base.py +105 -96
- jaf/policies/__init__.py +0 -1
- jaf/policies/handoff.py +37 -46
- jaf/policies/validation.py +76 -52
- jaf/providers/__init__.py +6 -3
- jaf/providers/mcp.py +97 -51
- jaf/providers/model.py +360 -279
- jaf/server/__init__.py +1 -1
- jaf/server/main.py +7 -11
- jaf/server/server.py +514 -359
- jaf/server/types.py +208 -52
- jaf/utils/__init__.py +17 -18
- jaf/utils/attachments.py +111 -116
- jaf/utils/document_processor.py +175 -174
- jaf/visualization/__init__.py +1 -1
- jaf/visualization/example.py +111 -110
- jaf/visualization/functional_core.py +46 -71
- jaf/visualization/graphviz.py +154 -189
- jaf/visualization/imperative_shell.py +7 -16
- jaf/visualization/types.py +8 -4
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
- jaf_py-2.5.11.dist-info/RECORD +97 -0
- jaf_py-2.5.10.dist-info/RECORD +0 -96
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
jaf/core/workflows.py
CHANGED
|
@@ -20,6 +20,7 @@ from .analytics import global_analytics_engine
|
|
|
20
20
|
|
|
21
21
|
class WorkflowStatus(Enum):
|
|
22
22
|
"""Status of workflow execution."""
|
|
23
|
+
|
|
23
24
|
PENDING = "pending"
|
|
24
25
|
RUNNING = "running"
|
|
25
26
|
COMPLETED = "completed"
|
|
@@ -30,6 +31,7 @@ class WorkflowStatus(Enum):
|
|
|
30
31
|
|
|
31
32
|
class StepStatus(Enum):
|
|
32
33
|
"""Status of individual workflow steps."""
|
|
34
|
+
|
|
33
35
|
PENDING = "pending"
|
|
34
36
|
RUNNING = "running"
|
|
35
37
|
COMPLETED = "completed"
|
|
@@ -41,47 +43,49 @@ class StepStatus(Enum):
|
|
|
41
43
|
@dataclass(frozen=True)
|
|
42
44
|
class WorkflowContext:
|
|
43
45
|
"""Context passed through workflow execution."""
|
|
46
|
+
|
|
44
47
|
workflow_id: str
|
|
45
48
|
user_context: Any
|
|
46
49
|
variables: Dict[str, Any] = field(default_factory=dict)
|
|
47
50
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
48
|
-
|
|
49
|
-
def with_variable(self, key: str, value: Any) ->
|
|
51
|
+
|
|
52
|
+
def with_variable(self, key: str, value: Any) -> "WorkflowContext":
|
|
50
53
|
"""Create new context with additional variable."""
|
|
51
54
|
new_vars = {**self.variables, key: value}
|
|
52
55
|
return WorkflowContext(
|
|
53
56
|
workflow_id=self.workflow_id,
|
|
54
57
|
user_context=self.user_context,
|
|
55
58
|
variables=new_vars,
|
|
56
|
-
metadata=self.metadata
|
|
59
|
+
metadata=self.metadata,
|
|
57
60
|
)
|
|
58
|
-
|
|
59
|
-
def with_metadata(self, key: str, value: Any) ->
|
|
61
|
+
|
|
62
|
+
def with_metadata(self, key: str, value: Any) -> "WorkflowContext":
|
|
60
63
|
"""Create new context with additional metadata."""
|
|
61
64
|
new_metadata = {**self.metadata, key: value}
|
|
62
65
|
return WorkflowContext(
|
|
63
66
|
workflow_id=self.workflow_id,
|
|
64
67
|
user_context=self.user_context,
|
|
65
68
|
variables=self.variables,
|
|
66
|
-
metadata=new_metadata
|
|
69
|
+
metadata=new_metadata,
|
|
67
70
|
)
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
@dataclass(frozen=True)
|
|
71
74
|
class StepResult:
|
|
72
75
|
"""Result of a workflow step execution."""
|
|
76
|
+
|
|
73
77
|
step_id: str
|
|
74
78
|
status: StepStatus
|
|
75
79
|
output: Any = None
|
|
76
80
|
error: Optional[str] = None
|
|
77
81
|
execution_time_ms: float = 0
|
|
78
82
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
79
|
-
|
|
83
|
+
|
|
80
84
|
@property
|
|
81
85
|
def is_success(self) -> bool:
|
|
82
86
|
"""Check if step completed successfully."""
|
|
83
87
|
return self.status == StepStatus.COMPLETED
|
|
84
|
-
|
|
88
|
+
|
|
85
89
|
@property
|
|
86
90
|
def is_failure(self) -> bool:
|
|
87
91
|
"""Check if step failed."""
|
|
@@ -91,6 +95,7 @@ class StepResult:
|
|
|
91
95
|
@dataclass(frozen=True)
|
|
92
96
|
class WorkflowResult:
|
|
93
97
|
"""Result of complete workflow execution."""
|
|
98
|
+
|
|
94
99
|
workflow_id: str
|
|
95
100
|
status: WorkflowStatus
|
|
96
101
|
steps: List[StepResult]
|
|
@@ -98,17 +103,17 @@ class WorkflowResult:
|
|
|
98
103
|
error: Optional[str] = None
|
|
99
104
|
total_execution_time_ms: float = 0
|
|
100
105
|
context: Optional[WorkflowContext] = None
|
|
101
|
-
|
|
106
|
+
|
|
102
107
|
@property
|
|
103
108
|
def is_success(self) -> bool:
|
|
104
109
|
"""Check if workflow completed successfully."""
|
|
105
110
|
return self.status == WorkflowStatus.COMPLETED
|
|
106
|
-
|
|
111
|
+
|
|
107
112
|
@property
|
|
108
113
|
def failed_steps(self) -> List[StepResult]:
|
|
109
114
|
"""Get list of failed steps."""
|
|
110
115
|
return [step for step in self.steps if step.is_failure]
|
|
111
|
-
|
|
116
|
+
|
|
112
117
|
@property
|
|
113
118
|
def success_rate(self) -> float:
|
|
114
119
|
"""Calculate success rate of steps."""
|
|
@@ -120,7 +125,7 @@ class WorkflowResult:
|
|
|
120
125
|
|
|
121
126
|
class WorkflowStep(ABC):
|
|
122
127
|
"""Abstract base class for workflow steps."""
|
|
123
|
-
|
|
128
|
+
|
|
124
129
|
def __init__(self, step_id: str, name: str, description: str = ""):
|
|
125
130
|
self.step_id = step_id
|
|
126
131
|
self.name = name
|
|
@@ -129,27 +134,27 @@ class WorkflowStep(ABC):
|
|
|
129
134
|
self.max_retries = 3
|
|
130
135
|
self.timeout_seconds = 30
|
|
131
136
|
self.conditions: List[Callable[[WorkflowContext], bool]] = []
|
|
132
|
-
|
|
137
|
+
|
|
133
138
|
@abstractmethod
|
|
134
139
|
async def execute(self, context: WorkflowContext) -> StepResult:
|
|
135
140
|
"""Execute the workflow step."""
|
|
136
141
|
pass
|
|
137
|
-
|
|
138
|
-
def add_condition(self, condition: Callable[[WorkflowContext], bool]) ->
|
|
142
|
+
|
|
143
|
+
def add_condition(self, condition: Callable[[WorkflowContext], bool]) -> "WorkflowStep":
|
|
139
144
|
"""Add execution condition."""
|
|
140
145
|
self.conditions.append(condition)
|
|
141
146
|
return self
|
|
142
|
-
|
|
143
|
-
def with_retry(self, max_retries: int) ->
|
|
147
|
+
|
|
148
|
+
def with_retry(self, max_retries: int) -> "WorkflowStep":
|
|
144
149
|
"""Configure retry behavior."""
|
|
145
150
|
self.max_retries = max_retries
|
|
146
151
|
return self
|
|
147
|
-
|
|
148
|
-
def with_timeout(self, timeout_seconds: int) ->
|
|
152
|
+
|
|
153
|
+
def with_timeout(self, timeout_seconds: int) -> "WorkflowStep":
|
|
149
154
|
"""Configure timeout."""
|
|
150
155
|
self.timeout_seconds = timeout_seconds
|
|
151
156
|
return self
|
|
152
|
-
|
|
157
|
+
|
|
153
158
|
def should_execute(self, context: WorkflowContext) -> bool:
|
|
154
159
|
"""Check if step should execute based on conditions."""
|
|
155
160
|
return all(condition(context) for condition in self.conditions)
|
|
@@ -157,105 +162,110 @@ class WorkflowStep(ABC):
|
|
|
157
162
|
|
|
158
163
|
class AgentStep(WorkflowStep):
|
|
159
164
|
"""Workflow step that executes an agent."""
|
|
160
|
-
|
|
165
|
+
|
|
161
166
|
def __init__(self, step_id: str, agent: Agent, message: str, name: str = ""):
|
|
162
167
|
super().__init__(step_id, name or f"Agent: {agent.name}")
|
|
163
168
|
self.agent = agent
|
|
164
169
|
self.message = message
|
|
165
|
-
|
|
170
|
+
|
|
166
171
|
async def execute(self, context: WorkflowContext) -> StepResult:
|
|
167
172
|
"""Execute agent step."""
|
|
168
173
|
start_time = time.time()
|
|
169
|
-
|
|
174
|
+
|
|
170
175
|
try:
|
|
171
176
|
# Create run state for agent
|
|
172
177
|
from .types import RunState, create_run_id, create_trace_id
|
|
173
|
-
|
|
178
|
+
|
|
174
179
|
run_state = RunState(
|
|
175
180
|
run_id=create_run_id(f"{context.workflow_id}_{self.step_id}"),
|
|
176
181
|
trace_id=create_trace_id(f"{context.workflow_id}"),
|
|
177
182
|
messages=[Message(role=ContentRole.USER, content=self.message)],
|
|
178
183
|
current_agent_name=self.agent.name,
|
|
179
184
|
context=context.user_context,
|
|
180
|
-
turn_count=0
|
|
185
|
+
turn_count=0,
|
|
181
186
|
)
|
|
182
|
-
|
|
187
|
+
|
|
183
188
|
# Get agent instructions
|
|
184
189
|
instructions = self.agent.instructions(run_state)
|
|
185
|
-
|
|
190
|
+
|
|
186
191
|
# Simulate agent execution (in real implementation, use JAF engine)
|
|
187
192
|
await asyncio.sleep(0.1) # Simulate processing
|
|
188
|
-
|
|
193
|
+
|
|
189
194
|
execution_time = (time.time() - start_time) * 1000
|
|
190
|
-
|
|
195
|
+
|
|
191
196
|
return StepResult(
|
|
192
197
|
step_id=self.step_id,
|
|
193
198
|
status=StepStatus.COMPLETED,
|
|
194
199
|
output=f"Agent {self.agent.name} processed: {self.message}",
|
|
195
200
|
execution_time_ms=execution_time,
|
|
196
|
-
metadata={
|
|
201
|
+
metadata={"agent_name": self.agent.name, "instructions": instructions},
|
|
197
202
|
)
|
|
198
|
-
|
|
203
|
+
|
|
199
204
|
except Exception as e:
|
|
200
205
|
execution_time = (time.time() - start_time) * 1000
|
|
201
206
|
return StepResult(
|
|
202
207
|
step_id=self.step_id,
|
|
203
208
|
status=StepStatus.FAILED,
|
|
204
209
|
error=str(e),
|
|
205
|
-
execution_time_ms=execution_time
|
|
210
|
+
execution_time_ms=execution_time,
|
|
206
211
|
)
|
|
207
212
|
|
|
208
213
|
|
|
209
214
|
class ToolStep(WorkflowStep):
|
|
210
215
|
"""Workflow step that executes a tool."""
|
|
211
|
-
|
|
216
|
+
|
|
212
217
|
def __init__(self, step_id: str, tool: Tool, args: Any, name: str = ""):
|
|
213
218
|
super().__init__(step_id, name or f"Tool: {tool.schema.name}")
|
|
214
219
|
self.tool = tool
|
|
215
220
|
self.args = args
|
|
216
|
-
|
|
221
|
+
|
|
217
222
|
async def execute(self, context: WorkflowContext) -> StepResult:
|
|
218
223
|
"""Execute tool step."""
|
|
219
224
|
start_time = time.time()
|
|
220
|
-
|
|
225
|
+
|
|
221
226
|
try:
|
|
222
227
|
# Execute tool
|
|
223
228
|
result = await self.tool.execute(self.args, context.user_context)
|
|
224
|
-
|
|
229
|
+
|
|
225
230
|
execution_time = (time.time() - start_time) * 1000
|
|
226
|
-
|
|
231
|
+
|
|
227
232
|
return StepResult(
|
|
228
233
|
step_id=self.step_id,
|
|
229
234
|
status=StepStatus.COMPLETED,
|
|
230
235
|
output=result,
|
|
231
236
|
execution_time_ms=execution_time,
|
|
232
|
-
metadata={
|
|
237
|
+
metadata={"tool_name": self.tool.schema.name},
|
|
233
238
|
)
|
|
234
|
-
|
|
239
|
+
|
|
235
240
|
except Exception as e:
|
|
236
241
|
execution_time = (time.time() - start_time) * 1000
|
|
237
242
|
return StepResult(
|
|
238
243
|
step_id=self.step_id,
|
|
239
244
|
status=StepStatus.FAILED,
|
|
240
245
|
error=str(e),
|
|
241
|
-
execution_time_ms=execution_time
|
|
246
|
+
execution_time_ms=execution_time,
|
|
242
247
|
)
|
|
243
248
|
|
|
244
249
|
|
|
245
250
|
class ConditionalStep(WorkflowStep):
|
|
246
251
|
"""Workflow step that executes conditionally."""
|
|
247
|
-
|
|
248
|
-
def __init__(
|
|
249
|
-
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
step_id: str,
|
|
256
|
+
condition: Callable[[WorkflowContext], bool],
|
|
257
|
+
true_step: WorkflowStep,
|
|
258
|
+
false_step: Optional[WorkflowStep] = None,
|
|
259
|
+
):
|
|
250
260
|
super().__init__(step_id, "Conditional Step")
|
|
251
261
|
self.condition = condition
|
|
252
262
|
self.true_step = true_step
|
|
253
263
|
self.false_step = false_step
|
|
254
|
-
|
|
264
|
+
|
|
255
265
|
async def execute(self, context: WorkflowContext) -> StepResult:
|
|
256
266
|
"""Execute conditional step."""
|
|
257
267
|
start_time = time.time()
|
|
258
|
-
|
|
268
|
+
|
|
259
269
|
try:
|
|
260
270
|
if self.condition(context):
|
|
261
271
|
result = await self.true_step.execute(context)
|
|
@@ -267,9 +277,9 @@ class ConditionalStep(WorkflowStep):
|
|
|
267
277
|
step_id=self.step_id,
|
|
268
278
|
status=StepStatus.SKIPPED,
|
|
269
279
|
output="Condition not met, no alternative step",
|
|
270
|
-
execution_time_ms=execution_time
|
|
280
|
+
execution_time_ms=execution_time,
|
|
271
281
|
)
|
|
272
|
-
|
|
282
|
+
|
|
273
283
|
execution_time = (time.time() - start_time) * 1000
|
|
274
284
|
return StepResult(
|
|
275
285
|
step_id=self.step_id,
|
|
@@ -277,36 +287,37 @@ class ConditionalStep(WorkflowStep):
|
|
|
277
287
|
output=result.output,
|
|
278
288
|
error=result.error,
|
|
279
289
|
execution_time_ms=execution_time,
|
|
280
|
-
metadata={
|
|
290
|
+
metadata={"delegated_to": result.step_id},
|
|
281
291
|
)
|
|
282
|
-
|
|
292
|
+
|
|
283
293
|
except Exception as e:
|
|
284
294
|
execution_time = (time.time() - start_time) * 1000
|
|
285
295
|
return StepResult(
|
|
286
296
|
step_id=self.step_id,
|
|
287
297
|
status=StepStatus.FAILED,
|
|
288
298
|
error=str(e),
|
|
289
|
-
execution_time_ms=execution_time
|
|
299
|
+
execution_time_ms=execution_time,
|
|
290
300
|
)
|
|
291
301
|
|
|
292
302
|
|
|
293
303
|
class ParallelStep(WorkflowStep):
|
|
294
304
|
"""Workflow step that executes multiple steps in parallel."""
|
|
295
|
-
|
|
296
|
-
def __init__(
|
|
297
|
-
|
|
305
|
+
|
|
306
|
+
def __init__(
|
|
307
|
+
self, step_id: str, steps: List[WorkflowStep], wait_for_all: bool = True, name: str = ""
|
|
308
|
+
):
|
|
298
309
|
super().__init__(step_id, name or "Parallel Execution")
|
|
299
310
|
self.steps = steps
|
|
300
311
|
self.wait_for_all = wait_for_all
|
|
301
|
-
|
|
312
|
+
|
|
302
313
|
async def execute(self, context: WorkflowContext) -> StepResult:
|
|
303
314
|
"""Execute parallel steps."""
|
|
304
315
|
start_time = time.time()
|
|
305
|
-
|
|
316
|
+
|
|
306
317
|
try:
|
|
307
318
|
# Execute all steps in parallel
|
|
308
319
|
tasks = [step.execute(context) for step in self.steps]
|
|
309
|
-
|
|
320
|
+
|
|
310
321
|
if self.wait_for_all:
|
|
311
322
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
312
323
|
else:
|
|
@@ -316,22 +327,24 @@ class ParallelStep(WorkflowStep):
|
|
|
316
327
|
# Cancel pending tasks
|
|
317
328
|
for task in pending:
|
|
318
329
|
task.cancel()
|
|
319
|
-
|
|
330
|
+
|
|
320
331
|
execution_time = (time.time() - start_time) * 1000
|
|
321
|
-
|
|
332
|
+
|
|
322
333
|
# Process results
|
|
323
334
|
step_results = []
|
|
324
335
|
for i, result in enumerate(results):
|
|
325
336
|
if isinstance(result, Exception):
|
|
326
|
-
step_results.append(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
step_results.append(
|
|
338
|
+
StepResult(
|
|
339
|
+
step_id=f"{self.step_id}_parallel_{i}",
|
|
340
|
+
status=StepStatus.FAILED,
|
|
341
|
+
error=str(result),
|
|
342
|
+
execution_time_ms=0,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
332
345
|
else:
|
|
333
346
|
step_results.append(result)
|
|
334
|
-
|
|
347
|
+
|
|
335
348
|
# Determine overall status
|
|
336
349
|
if all(r.is_success for r in step_results):
|
|
337
350
|
status = StepStatus.COMPLETED
|
|
@@ -339,57 +352,63 @@ class ParallelStep(WorkflowStep):
|
|
|
339
352
|
status = StepStatus.COMPLETED
|
|
340
353
|
else:
|
|
341
354
|
status = StepStatus.FAILED
|
|
342
|
-
|
|
355
|
+
|
|
343
356
|
return StepResult(
|
|
344
357
|
step_id=self.step_id,
|
|
345
358
|
status=status,
|
|
346
359
|
output=step_results,
|
|
347
360
|
execution_time_ms=execution_time,
|
|
348
|
-
metadata={
|
|
361
|
+
metadata={"parallel_results": len(step_results)},
|
|
349
362
|
)
|
|
350
|
-
|
|
363
|
+
|
|
351
364
|
except Exception as e:
|
|
352
365
|
execution_time = (time.time() - start_time) * 1000
|
|
353
366
|
return StepResult(
|
|
354
367
|
step_id=self.step_id,
|
|
355
368
|
status=StepStatus.FAILED,
|
|
356
369
|
error=str(e),
|
|
357
|
-
execution_time_ms=execution_time
|
|
370
|
+
execution_time_ms=execution_time,
|
|
358
371
|
)
|
|
359
372
|
|
|
360
373
|
|
|
361
374
|
class LoopStep(WorkflowStep):
|
|
362
375
|
"""Workflow step that executes in a loop."""
|
|
363
|
-
|
|
364
|
-
def __init__(
|
|
365
|
-
|
|
366
|
-
|
|
376
|
+
|
|
377
|
+
def __init__(
|
|
378
|
+
self,
|
|
379
|
+
step_id: str,
|
|
380
|
+
step: WorkflowStep,
|
|
381
|
+
condition: Callable[[WorkflowContext, int], bool],
|
|
382
|
+
max_iterations: int = 10,
|
|
383
|
+
):
|
|
367
384
|
super().__init__(step_id, "Loop Step")
|
|
368
385
|
self.step = step
|
|
369
386
|
self.condition = condition
|
|
370
387
|
self.max_iterations = max_iterations
|
|
371
|
-
|
|
388
|
+
|
|
372
389
|
async def execute(self, context: WorkflowContext) -> StepResult:
|
|
373
390
|
"""Execute loop step."""
|
|
374
391
|
start_time = time.time()
|
|
375
392
|
results = []
|
|
376
393
|
iteration = 0
|
|
377
|
-
|
|
394
|
+
|
|
378
395
|
try:
|
|
379
396
|
while iteration < self.max_iterations and self.condition(context, iteration):
|
|
380
397
|
result = await self.step.execute(context)
|
|
381
398
|
results.append(result)
|
|
382
|
-
|
|
399
|
+
|
|
383
400
|
# Update context with iteration results
|
|
384
|
-
context = context.with_variable(
|
|
385
|
-
|
|
401
|
+
context = context.with_variable(
|
|
402
|
+
f"loop_{self.step_id}_iteration_{iteration}", result.output
|
|
403
|
+
)
|
|
404
|
+
|
|
386
405
|
if result.is_failure:
|
|
387
406
|
break
|
|
388
|
-
|
|
407
|
+
|
|
389
408
|
iteration += 1
|
|
390
|
-
|
|
409
|
+
|
|
391
410
|
execution_time = (time.time() - start_time) * 1000
|
|
392
|
-
|
|
411
|
+
|
|
393
412
|
# Determine overall status
|
|
394
413
|
if results and all(r.is_success for r in results):
|
|
395
414
|
status = StepStatus.COMPLETED
|
|
@@ -397,28 +416,28 @@ class LoopStep(WorkflowStep):
|
|
|
397
416
|
status = StepStatus.SKIPPED
|
|
398
417
|
else:
|
|
399
418
|
status = StepStatus.FAILED
|
|
400
|
-
|
|
419
|
+
|
|
401
420
|
return StepResult(
|
|
402
421
|
step_id=self.step_id,
|
|
403
422
|
status=status,
|
|
404
423
|
output=results,
|
|
405
424
|
execution_time_ms=execution_time,
|
|
406
|
-
metadata={
|
|
425
|
+
metadata={"iterations": iteration, "results_count": len(results)},
|
|
407
426
|
)
|
|
408
|
-
|
|
427
|
+
|
|
409
428
|
except Exception as e:
|
|
410
429
|
execution_time = (time.time() - start_time) * 1000
|
|
411
430
|
return StepResult(
|
|
412
431
|
step_id=self.step_id,
|
|
413
432
|
status=StepStatus.FAILED,
|
|
414
433
|
error=str(e),
|
|
415
|
-
execution_time_ms=execution_time
|
|
434
|
+
execution_time_ms=execution_time,
|
|
416
435
|
)
|
|
417
436
|
|
|
418
437
|
|
|
419
438
|
class Workflow:
|
|
420
439
|
"""Main workflow orchestrator."""
|
|
421
|
-
|
|
440
|
+
|
|
422
441
|
def __init__(self, workflow_id: str, name: str, description: str = ""):
|
|
423
442
|
self.workflow_id = workflow_id
|
|
424
443
|
self.name = name
|
|
@@ -428,36 +447,39 @@ class Workflow:
|
|
|
428
447
|
self.on_step_complete: Optional[Callable[[StepResult, WorkflowContext], None]] = None
|
|
429
448
|
self.on_workflow_complete: Optional[Callable[[WorkflowResult], None]] = None
|
|
430
449
|
self.performance_monitor = PerformanceMonitor()
|
|
431
|
-
|
|
432
|
-
def add_step(self, step: WorkflowStep) ->
|
|
450
|
+
|
|
451
|
+
def add_step(self, step: WorkflowStep) -> "Workflow":
|
|
433
452
|
"""Add a step to the workflow."""
|
|
434
453
|
self.steps.append(step)
|
|
435
454
|
return self
|
|
436
|
-
|
|
437
|
-
def add_error_handler(
|
|
438
|
-
|
|
455
|
+
|
|
456
|
+
def add_error_handler(
|
|
457
|
+
self, step_id: str, handler: Callable[[StepResult, WorkflowContext], WorkflowStep]
|
|
458
|
+
) -> "Workflow":
|
|
439
459
|
"""Add error handler for specific step."""
|
|
440
460
|
self.error_handlers[step_id] = handler
|
|
441
461
|
return self
|
|
442
|
-
|
|
443
|
-
def on_step_completed(
|
|
462
|
+
|
|
463
|
+
def on_step_completed(
|
|
464
|
+
self, callback: Callable[[StepResult, WorkflowContext], None]
|
|
465
|
+
) -> "Workflow":
|
|
444
466
|
"""Set callback for step completion."""
|
|
445
467
|
self.on_step_complete = callback
|
|
446
468
|
return self
|
|
447
|
-
|
|
448
|
-
def on_workflow_completed(self, callback: Callable[[WorkflowResult], None]) ->
|
|
469
|
+
|
|
470
|
+
def on_workflow_completed(self, callback: Callable[[WorkflowResult], None]) -> "Workflow":
|
|
449
471
|
"""Set callback for workflow completion."""
|
|
450
472
|
self.on_workflow_complete = callback
|
|
451
473
|
return self
|
|
452
|
-
|
|
474
|
+
|
|
453
475
|
async def execute(self, context: WorkflowContext) -> WorkflowResult:
|
|
454
476
|
"""Execute the complete workflow."""
|
|
455
477
|
start_time = time.time()
|
|
456
478
|
self.performance_monitor.start_monitoring()
|
|
457
|
-
|
|
479
|
+
|
|
458
480
|
step_results: List[StepResult] = []
|
|
459
481
|
current_context = context
|
|
460
|
-
|
|
482
|
+
|
|
461
483
|
try:
|
|
462
484
|
for step in self.steps:
|
|
463
485
|
# Check if step should execute
|
|
@@ -466,186 +488,203 @@ class Workflow:
|
|
|
466
488
|
step_id=step.step_id,
|
|
467
489
|
status=StepStatus.SKIPPED,
|
|
468
490
|
output="Conditions not met",
|
|
469
|
-
execution_time_ms=0
|
|
491
|
+
execution_time_ms=0,
|
|
470
492
|
)
|
|
471
493
|
step_results.append(step_result)
|
|
472
494
|
continue
|
|
473
|
-
|
|
495
|
+
|
|
474
496
|
# Execute step with retry logic
|
|
475
497
|
step_result = await self._execute_step_with_retry(step, current_context)
|
|
476
498
|
step_results.append(step_result)
|
|
477
|
-
|
|
499
|
+
|
|
478
500
|
# Handle step completion
|
|
479
501
|
if self.on_step_complete:
|
|
480
502
|
self.on_step_complete(step_result, current_context)
|
|
481
|
-
|
|
503
|
+
|
|
482
504
|
# Update context with step result
|
|
483
505
|
current_context = current_context.with_variable(
|
|
484
|
-
f"step_{step.step_id}_result",
|
|
485
|
-
step_result.output
|
|
506
|
+
f"step_{step.step_id}_result", step_result.output
|
|
486
507
|
)
|
|
487
|
-
|
|
508
|
+
|
|
488
509
|
# Handle errors
|
|
489
510
|
if step_result.is_failure and step.step_id in self.error_handlers:
|
|
490
511
|
error_handler = self.error_handlers[step.step_id]
|
|
491
512
|
recovery_step = error_handler(step_result, current_context)
|
|
492
|
-
recovery_result = await self._execute_step_with_retry(
|
|
513
|
+
recovery_result = await self._execute_step_with_retry(
|
|
514
|
+
recovery_step, current_context
|
|
515
|
+
)
|
|
493
516
|
step_results.append(recovery_result)
|
|
494
|
-
|
|
517
|
+
|
|
495
518
|
if recovery_result.is_failure:
|
|
496
519
|
# Recovery failed, stop workflow
|
|
497
520
|
break
|
|
498
521
|
elif step_result.is_failure:
|
|
499
522
|
# No error handler, stop workflow
|
|
500
523
|
break
|
|
501
|
-
|
|
524
|
+
|
|
502
525
|
# Calculate final status
|
|
503
526
|
if all(r.is_success or r.status == StepStatus.SKIPPED for r in step_results):
|
|
504
527
|
final_status = WorkflowStatus.COMPLETED
|
|
505
528
|
else:
|
|
506
529
|
final_status = WorkflowStatus.FAILED
|
|
507
|
-
|
|
530
|
+
|
|
508
531
|
execution_time = (time.time() - start_time) * 1000
|
|
509
532
|
performance_metrics = self.performance_monitor.stop_monitoring()
|
|
510
|
-
|
|
533
|
+
|
|
511
534
|
# Get final output from last successful step
|
|
512
535
|
final_output = None
|
|
513
536
|
for result in reversed(step_results):
|
|
514
537
|
if result.is_success and result.output is not None:
|
|
515
538
|
final_output = result.output
|
|
516
539
|
break
|
|
517
|
-
|
|
540
|
+
|
|
518
541
|
workflow_result = WorkflowResult(
|
|
519
542
|
workflow_id=self.workflow_id,
|
|
520
543
|
status=final_status,
|
|
521
544
|
steps=step_results,
|
|
522
545
|
final_output=final_output,
|
|
523
546
|
total_execution_time_ms=execution_time,
|
|
524
|
-
context=current_context
|
|
547
|
+
context=current_context,
|
|
525
548
|
)
|
|
526
|
-
|
|
549
|
+
|
|
527
550
|
# Record analytics
|
|
528
|
-
global_analytics_engine.record_system_metrics(
|
|
529
|
-
|
|
551
|
+
global_analytics_engine.record_system_metrics(
|
|
552
|
+
performance_metrics, f"workflow_{self.workflow_id}"
|
|
553
|
+
)
|
|
554
|
+
|
|
530
555
|
# Handle workflow completion
|
|
531
556
|
if self.on_workflow_complete:
|
|
532
557
|
self.on_workflow_complete(workflow_result)
|
|
533
|
-
|
|
558
|
+
|
|
534
559
|
return workflow_result
|
|
535
|
-
|
|
560
|
+
|
|
536
561
|
except Exception as e:
|
|
537
562
|
execution_time = (time.time() - start_time) * 1000
|
|
538
|
-
|
|
563
|
+
|
|
539
564
|
return WorkflowResult(
|
|
540
565
|
workflow_id=self.workflow_id,
|
|
541
566
|
status=WorkflowStatus.FAILED,
|
|
542
567
|
steps=step_results,
|
|
543
568
|
error=str(e),
|
|
544
569
|
total_execution_time_ms=execution_time,
|
|
545
|
-
context=current_context
|
|
570
|
+
context=current_context,
|
|
546
571
|
)
|
|
547
|
-
|
|
548
|
-
async def _execute_step_with_retry(
|
|
572
|
+
|
|
573
|
+
async def _execute_step_with_retry(
|
|
574
|
+
self, step: WorkflowStep, context: WorkflowContext
|
|
575
|
+
) -> StepResult:
|
|
549
576
|
"""Execute step with retry logic."""
|
|
550
577
|
last_result = None
|
|
551
|
-
|
|
578
|
+
|
|
552
579
|
for attempt in range(step.max_retries + 1):
|
|
553
580
|
try:
|
|
554
581
|
# Execute with timeout
|
|
555
|
-
result = await asyncio.wait_for(
|
|
556
|
-
|
|
557
|
-
timeout=step.timeout_seconds
|
|
558
|
-
)
|
|
559
|
-
|
|
582
|
+
result = await asyncio.wait_for(step.execute(context), timeout=step.timeout_seconds)
|
|
583
|
+
|
|
560
584
|
if result.is_success:
|
|
561
585
|
return result
|
|
562
|
-
|
|
586
|
+
|
|
563
587
|
last_result = result
|
|
564
|
-
|
|
588
|
+
|
|
565
589
|
if attempt < step.max_retries:
|
|
566
590
|
# Wait before retry with exponential backoff
|
|
567
|
-
wait_time = 2
|
|
591
|
+
wait_time = 2**attempt
|
|
568
592
|
await asyncio.sleep(wait_time)
|
|
569
|
-
|
|
593
|
+
|
|
570
594
|
except asyncio.TimeoutError:
|
|
571
595
|
last_result = StepResult(
|
|
572
596
|
step_id=step.step_id,
|
|
573
597
|
status=StepStatus.FAILED,
|
|
574
598
|
error=f"Step timed out after {step.timeout_seconds} seconds",
|
|
575
|
-
execution_time_ms=step.timeout_seconds * 1000
|
|
599
|
+
execution_time_ms=step.timeout_seconds * 1000,
|
|
576
600
|
)
|
|
577
601
|
except Exception as e:
|
|
578
602
|
last_result = StepResult(
|
|
579
603
|
step_id=step.step_id,
|
|
580
604
|
status=StepStatus.FAILED,
|
|
581
605
|
error=str(e),
|
|
582
|
-
execution_time_ms=0
|
|
606
|
+
execution_time_ms=0,
|
|
583
607
|
)
|
|
584
|
-
|
|
608
|
+
|
|
585
609
|
return last_result or StepResult(
|
|
586
610
|
step_id=step.step_id,
|
|
587
611
|
status=StepStatus.FAILED,
|
|
588
612
|
error="Unknown error during execution",
|
|
589
|
-
execution_time_ms=0
|
|
613
|
+
execution_time_ms=0,
|
|
590
614
|
)
|
|
591
615
|
|
|
592
616
|
|
|
593
617
|
class WorkflowBuilder:
|
|
594
618
|
"""Builder for creating workflows with fluent API."""
|
|
595
|
-
|
|
619
|
+
|
|
596
620
|
def __init__(self, workflow_id: str, name: str):
|
|
597
621
|
self.workflow = Workflow(workflow_id, name)
|
|
598
|
-
|
|
599
|
-
def add_agent_step(self, step_id: str, agent: Agent, message: str) ->
|
|
622
|
+
|
|
623
|
+
def add_agent_step(self, step_id: str, agent: Agent, message: str) -> "WorkflowBuilder":
|
|
600
624
|
"""Add agent execution step."""
|
|
601
625
|
step = AgentStep(step_id, agent, message)
|
|
602
626
|
self.workflow.add_step(step)
|
|
603
627
|
return self
|
|
604
|
-
|
|
605
|
-
def add_tool_step(self, step_id: str, tool: Tool, args: Any) ->
|
|
628
|
+
|
|
629
|
+
def add_tool_step(self, step_id: str, tool: Tool, args: Any) -> "WorkflowBuilder":
|
|
606
630
|
"""Add tool execution step."""
|
|
607
631
|
step = ToolStep(step_id, tool, args)
|
|
608
632
|
self.workflow.add_step(step)
|
|
609
633
|
return self
|
|
610
|
-
|
|
611
|
-
def add_conditional_step(
|
|
612
|
-
|
|
634
|
+
|
|
635
|
+
def add_conditional_step(
|
|
636
|
+
self,
|
|
637
|
+
step_id: str,
|
|
638
|
+
condition: Callable[[WorkflowContext], bool],
|
|
639
|
+
true_step: WorkflowStep,
|
|
640
|
+
false_step: Optional[WorkflowStep] = None,
|
|
641
|
+
) -> "WorkflowBuilder":
|
|
613
642
|
"""Add conditional execution step."""
|
|
614
643
|
step = ConditionalStep(step_id, condition, true_step, false_step)
|
|
615
644
|
self.workflow.add_step(step)
|
|
616
645
|
return self
|
|
617
|
-
|
|
618
|
-
def add_parallel_step(
|
|
619
|
-
|
|
646
|
+
|
|
647
|
+
def add_parallel_step(
|
|
648
|
+
self, step_id: str, steps: List[WorkflowStep], wait_for_all: bool = True
|
|
649
|
+
) -> "WorkflowBuilder":
|
|
620
650
|
"""Add parallel execution step."""
|
|
621
651
|
step = ParallelStep(step_id, steps, wait_for_all)
|
|
622
652
|
self.workflow.add_step(step)
|
|
623
653
|
return self
|
|
624
|
-
|
|
625
|
-
def add_loop_step(
|
|
626
|
-
|
|
627
|
-
|
|
654
|
+
|
|
655
|
+
def add_loop_step(
|
|
656
|
+
self,
|
|
657
|
+
step_id: str,
|
|
658
|
+
step: WorkflowStep,
|
|
659
|
+
condition: Callable[[WorkflowContext, int], bool],
|
|
660
|
+
max_iterations: int = 10,
|
|
661
|
+
) -> "WorkflowBuilder":
|
|
628
662
|
"""Add loop execution step."""
|
|
629
663
|
loop_step = LoopStep(step_id, step, condition, max_iterations)
|
|
630
664
|
self.workflow.add_step(loop_step)
|
|
631
665
|
return self
|
|
632
|
-
|
|
633
|
-
def with_error_handler(
|
|
634
|
-
|
|
666
|
+
|
|
667
|
+
def with_error_handler(
|
|
668
|
+
self, step_id: str, handler: Callable[[StepResult, WorkflowContext], WorkflowStep]
|
|
669
|
+
) -> "WorkflowBuilder":
|
|
635
670
|
"""Add error handler."""
|
|
636
671
|
self.workflow.add_error_handler(step_id, handler)
|
|
637
672
|
return self
|
|
638
|
-
|
|
639
|
-
def with_step_callback(
|
|
673
|
+
|
|
674
|
+
def with_step_callback(
|
|
675
|
+
self, callback: Callable[[StepResult, WorkflowContext], None]
|
|
676
|
+
) -> "WorkflowBuilder":
|
|
640
677
|
"""Add step completion callback."""
|
|
641
678
|
self.workflow.on_step_completed(callback)
|
|
642
679
|
return self
|
|
643
|
-
|
|
644
|
-
def with_completion_callback(
|
|
680
|
+
|
|
681
|
+
def with_completion_callback(
|
|
682
|
+
self, callback: Callable[[WorkflowResult], None]
|
|
683
|
+
) -> "WorkflowBuilder":
|
|
645
684
|
"""Add workflow completion callback."""
|
|
646
685
|
self.workflow.on_workflow_completed(callback)
|
|
647
686
|
return self
|
|
648
|
-
|
|
687
|
+
|
|
649
688
|
def build(self) -> Workflow:
|
|
650
689
|
"""Build the workflow."""
|
|
651
690
|
return self.workflow
|
|
@@ -665,8 +704,9 @@ def create_sequential_workflow(workflow_id: str, name: str, steps: List[Workflow
|
|
|
665
704
|
return workflow
|
|
666
705
|
|
|
667
706
|
|
|
668
|
-
def create_parallel_workflow(
|
|
669
|
-
|
|
707
|
+
def create_parallel_workflow(
|
|
708
|
+
workflow_id: str, name: str, steps: List[WorkflowStep], wait_for_all: bool = True
|
|
709
|
+
) -> Workflow:
|
|
670
710
|
"""Create a workflow that executes all steps in parallel."""
|
|
671
711
|
workflow = Workflow(workflow_id, name)
|
|
672
712
|
parallel_step = ParallelStep("parallel_execution", steps, wait_for_all)
|
|
@@ -674,32 +714,33 @@ def create_parallel_workflow(workflow_id: str, name: str, steps: List[WorkflowSt
|
|
|
674
714
|
return workflow
|
|
675
715
|
|
|
676
716
|
|
|
677
|
-
async def execute_workflow_stream(
|
|
717
|
+
async def execute_workflow_stream(
|
|
718
|
+
workflow: Workflow, context: WorkflowContext
|
|
719
|
+
) -> AsyncIterator[StepResult]:
|
|
678
720
|
"""Execute workflow and stream step results as they complete."""
|
|
679
721
|
step_results: List[StepResult] = []
|
|
680
722
|
current_context = context
|
|
681
|
-
|
|
723
|
+
|
|
682
724
|
for step in workflow.steps:
|
|
683
725
|
if not step.should_execute(current_context):
|
|
684
726
|
step_result = StepResult(
|
|
685
727
|
step_id=step.step_id,
|
|
686
728
|
status=StepStatus.SKIPPED,
|
|
687
729
|
output="Conditions not met",
|
|
688
|
-
execution_time_ms=0
|
|
730
|
+
execution_time_ms=0,
|
|
689
731
|
)
|
|
690
732
|
step_results.append(step_result)
|
|
691
733
|
yield step_result
|
|
692
734
|
continue
|
|
693
|
-
|
|
735
|
+
|
|
694
736
|
step_result = await workflow._execute_step_with_retry(step, current_context)
|
|
695
737
|
step_results.append(step_result)
|
|
696
738
|
yield step_result
|
|
697
|
-
|
|
739
|
+
|
|
698
740
|
# Update context
|
|
699
741
|
current_context = current_context.with_variable(
|
|
700
|
-
f"step_{step.step_id}_result",
|
|
701
|
-
step_result.output
|
|
742
|
+
f"step_{step.step_id}_result", step_result.output
|
|
702
743
|
)
|
|
703
|
-
|
|
744
|
+
|
|
704
745
|
if step_result.is_failure:
|
|
705
746
|
break
|