soe-ai 0.2.0b1__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.
- soe/__init__.py +50 -0
- soe/broker.py +168 -0
- soe/builtin_tools/__init__.py +51 -0
- soe/builtin_tools/soe_add_signal.py +82 -0
- soe/builtin_tools/soe_call_tool.py +111 -0
- soe/builtin_tools/soe_copy_context.py +80 -0
- soe/builtin_tools/soe_explore_docs.py +290 -0
- soe/builtin_tools/soe_get_available_tools.py +42 -0
- soe/builtin_tools/soe_get_context.py +50 -0
- soe/builtin_tools/soe_get_context_schema.py +56 -0
- soe/builtin_tools/soe_get_identities.py +63 -0
- soe/builtin_tools/soe_get_workflows.py +63 -0
- soe/builtin_tools/soe_inject_context_schema_field.py +80 -0
- soe/builtin_tools/soe_inject_identity.py +64 -0
- soe/builtin_tools/soe_inject_node.py +86 -0
- soe/builtin_tools/soe_inject_workflow.py +105 -0
- soe/builtin_tools/soe_list_contexts.py +73 -0
- soe/builtin_tools/soe_remove_context_schema_field.py +61 -0
- soe/builtin_tools/soe_remove_identity.py +61 -0
- soe/builtin_tools/soe_remove_node.py +72 -0
- soe/builtin_tools/soe_remove_workflow.py +62 -0
- soe/builtin_tools/soe_update_context.py +54 -0
- soe/docs/_config.yml +10 -0
- soe/docs/advanced_patterns/guide_fanout_and_aggregations.md +318 -0
- soe/docs/advanced_patterns/guide_inheritance.md +435 -0
- soe/docs/advanced_patterns/hybrid_intelligence.md +237 -0
- soe/docs/advanced_patterns/index.md +49 -0
- soe/docs/advanced_patterns/operational.md +781 -0
- soe/docs/advanced_patterns/self_evolving_workflows.md +385 -0
- soe/docs/advanced_patterns/swarm_intelligence.md +211 -0
- soe/docs/builtins/context.md +164 -0
- soe/docs/builtins/context_schema.md +158 -0
- soe/docs/builtins/identity.md +139 -0
- soe/docs/builtins/soe_explore_docs.md +135 -0
- soe/docs/builtins/tools.md +164 -0
- soe/docs/builtins/workflows.md +199 -0
- soe/docs/guide_00_getting_started.md +341 -0
- soe/docs/guide_01_tool.md +206 -0
- soe/docs/guide_02_llm.md +143 -0
- soe/docs/guide_03_router.md +146 -0
- soe/docs/guide_04_patterns.md +475 -0
- soe/docs/guide_05_agent.md +159 -0
- soe/docs/guide_06_schema.md +397 -0
- soe/docs/guide_07_identity.md +540 -0
- soe/docs/guide_08_child.md +612 -0
- soe/docs/guide_09_ecosystem.md +690 -0
- soe/docs/guide_10_infrastructure.md +427 -0
- soe/docs/guide_11_builtins.md +126 -0
- soe/docs/index.md +104 -0
- soe/docs/primitives/backends.md +281 -0
- soe/docs/primitives/context.md +256 -0
- soe/docs/primitives/node_reference.md +259 -0
- soe/docs/primitives/primitives.md +331 -0
- soe/docs/primitives/signals.md +865 -0
- soe/docs_index.py +2 -0
- soe/init.py +165 -0
- soe/lib/__init__.py +0 -0
- soe/lib/child_context.py +46 -0
- soe/lib/context_fields.py +51 -0
- soe/lib/inheritance.py +172 -0
- soe/lib/jinja_render.py +113 -0
- soe/lib/operational.py +51 -0
- soe/lib/parent_sync.py +71 -0
- soe/lib/register_event.py +75 -0
- soe/lib/schema_validation.py +134 -0
- soe/lib/yaml_parser.py +14 -0
- soe/local_backends/__init__.py +18 -0
- soe/local_backends/factory.py +124 -0
- soe/local_backends/in_memory/context.py +38 -0
- soe/local_backends/in_memory/conversation_history.py +60 -0
- soe/local_backends/in_memory/identity.py +52 -0
- soe/local_backends/in_memory/schema.py +40 -0
- soe/local_backends/in_memory/telemetry.py +38 -0
- soe/local_backends/in_memory/workflow.py +33 -0
- soe/local_backends/storage/context.py +57 -0
- soe/local_backends/storage/conversation_history.py +82 -0
- soe/local_backends/storage/identity.py +118 -0
- soe/local_backends/storage/schema.py +96 -0
- soe/local_backends/storage/telemetry.py +72 -0
- soe/local_backends/storage/workflow.py +56 -0
- soe/nodes/__init__.py +13 -0
- soe/nodes/agent/__init__.py +10 -0
- soe/nodes/agent/factory.py +134 -0
- soe/nodes/agent/lib/loop_handlers.py +150 -0
- soe/nodes/agent/lib/loop_state.py +157 -0
- soe/nodes/agent/lib/prompts.py +65 -0
- soe/nodes/agent/lib/tools.py +35 -0
- soe/nodes/agent/stages/__init__.py +12 -0
- soe/nodes/agent/stages/parameter.py +37 -0
- soe/nodes/agent/stages/response.py +54 -0
- soe/nodes/agent/stages/router.py +37 -0
- soe/nodes/agent/state.py +111 -0
- soe/nodes/agent/types.py +66 -0
- soe/nodes/agent/validation/__init__.py +11 -0
- soe/nodes/agent/validation/config.py +95 -0
- soe/nodes/agent/validation/operational.py +24 -0
- soe/nodes/child/__init__.py +3 -0
- soe/nodes/child/factory.py +61 -0
- soe/nodes/child/state.py +59 -0
- soe/nodes/child/validation/__init__.py +11 -0
- soe/nodes/child/validation/config.py +126 -0
- soe/nodes/child/validation/operational.py +28 -0
- soe/nodes/lib/conditions.py +71 -0
- soe/nodes/lib/context.py +24 -0
- soe/nodes/lib/conversation_history.py +77 -0
- soe/nodes/lib/identity.py +64 -0
- soe/nodes/lib/llm_resolver.py +142 -0
- soe/nodes/lib/output.py +68 -0
- soe/nodes/lib/response_builder.py +91 -0
- soe/nodes/lib/signal_emission.py +79 -0
- soe/nodes/lib/signals.py +54 -0
- soe/nodes/lib/tools.py +100 -0
- soe/nodes/llm/__init__.py +7 -0
- soe/nodes/llm/factory.py +103 -0
- soe/nodes/llm/state.py +76 -0
- soe/nodes/llm/types.py +12 -0
- soe/nodes/llm/validation/__init__.py +11 -0
- soe/nodes/llm/validation/config.py +89 -0
- soe/nodes/llm/validation/operational.py +23 -0
- soe/nodes/router/__init__.py +3 -0
- soe/nodes/router/factory.py +37 -0
- soe/nodes/router/state.py +32 -0
- soe/nodes/router/validation/__init__.py +11 -0
- soe/nodes/router/validation/config.py +58 -0
- soe/nodes/router/validation/operational.py +16 -0
- soe/nodes/tool/factory.py +66 -0
- soe/nodes/tool/lib/__init__.py +11 -0
- soe/nodes/tool/lib/conditions.py +35 -0
- soe/nodes/tool/lib/failure.py +28 -0
- soe/nodes/tool/lib/parameters.py +67 -0
- soe/nodes/tool/state.py +66 -0
- soe/nodes/tool/types.py +27 -0
- soe/nodes/tool/validation/__init__.py +15 -0
- soe/nodes/tool/validation/config.py +132 -0
- soe/nodes/tool/validation/operational.py +16 -0
- soe/types.py +209 -0
- soe/validation/__init__.py +18 -0
- soe/validation/config.py +195 -0
- soe/validation/jinja.py +54 -0
- soe/validation/operational.py +110 -0
- soe_ai-0.2.0b1.dist-info/METADATA +262 -0
- soe_ai-0.2.0b1.dist-info/RECORD +145 -0
- soe_ai-0.2.0b1.dist-info/WHEEL +5 -0
- soe_ai-0.2.0b1.dist-info/licenses/LICENSE +21 -0
- soe_ai-0.2.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
|
|
2
|
+
# SOE Guide: Chapter 4 - Building Custom Workflows
|
|
3
|
+
|
|
4
|
+
## The Three Core Node Types
|
|
5
|
+
|
|
6
|
+
You now know SOE's three core node types:
|
|
7
|
+
|
|
8
|
+
| Node | What It Does | Key Capability |
|
|
9
|
+
|------|--------------|----------------|
|
|
10
|
+
| **Tool** | Executes Python functions | Real-world actions |
|
|
11
|
+
| **LLM** | Calls language models | Intelligence & generation |
|
|
12
|
+
| **Router** | Routes based on context | Control flow & branching |
|
|
13
|
+
|
|
14
|
+
These three nodes are **all you need** to build sophisticated AI workflows. This chapter shows you how to combine them into powerful patterns—including building your own agent loops from scratch.
|
|
15
|
+
|
|
16
|
+
## Why Build Custom Workflows?
|
|
17
|
+
|
|
18
|
+
The built-in **Agent Node** (covered in the next chapter) provides a convenient ReAct loop. But sometimes you need:
|
|
19
|
+
|
|
20
|
+
- **Custom reasoning patterns** (chain-of-thought, tree-of-thought, metacognition)
|
|
21
|
+
- **Fine-grained control** over each step
|
|
22
|
+
- **Hybrid logic** mixing deterministic and AI-driven decisions
|
|
23
|
+
- **Domain-specific agent architectures**
|
|
24
|
+
|
|
25
|
+
With these three core nodes, you can build any pattern—the Agent Node is just one opinionated implementation.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Pattern 1: Chain of Thought
|
|
30
|
+
|
|
31
|
+
**The Pattern**: Break complex reasoning into explicit steps, where each LLM call builds on the previous one.
|
|
32
|
+
|
|
33
|
+
### The Workflow
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
example_workflow:
|
|
37
|
+
Understand:
|
|
38
|
+
node_type: llm
|
|
39
|
+
event_triggers: [START]
|
|
40
|
+
prompt: "Analyze this problem and restate it in your own words: {{ context.problem }}"
|
|
41
|
+
output_field: understanding
|
|
42
|
+
event_emissions:
|
|
43
|
+
- signal_name: UNDERSTOOD
|
|
44
|
+
|
|
45
|
+
Plan:
|
|
46
|
+
node_type: llm
|
|
47
|
+
event_triggers: [UNDERSTOOD]
|
|
48
|
+
prompt: |
|
|
49
|
+
Based on your understanding: {{ context.understanding }}
|
|
50
|
+
|
|
51
|
+
Create a step-by-step plan to solve this problem.
|
|
52
|
+
output_field: plan
|
|
53
|
+
event_emissions:
|
|
54
|
+
- signal_name: PLANNED
|
|
55
|
+
|
|
56
|
+
Execute:
|
|
57
|
+
node_type: llm
|
|
58
|
+
event_triggers: [PLANNED]
|
|
59
|
+
prompt: |
|
|
60
|
+
Understanding: {{ context.understanding }}
|
|
61
|
+
Plan: {{ context.plan }}
|
|
62
|
+
|
|
63
|
+
Now execute the plan and provide the final answer.
|
|
64
|
+
output_field: answer
|
|
65
|
+
event_emissions:
|
|
66
|
+
- signal_name: COMPLETE
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### How It Works
|
|
70
|
+
|
|
71
|
+
1. **Understand**: LLM analyzes and restates the problem
|
|
72
|
+
2. **Plan**: LLM creates a step-by-step plan based on its understanding
|
|
73
|
+
3. **Execute**: LLM generates the final answer using the plan
|
|
74
|
+
|
|
75
|
+
Each step stores its output in context, and the next step reads it.
|
|
76
|
+
|
|
77
|
+
### Why This Pattern?
|
|
78
|
+
|
|
79
|
+
- **Transparency**: Each reasoning step is visible and inspectable
|
|
80
|
+
- **Debuggability**: If the answer is wrong, you can see exactly where reasoning failed
|
|
81
|
+
- **Control**: You can add validation routers between steps
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Pattern 2: Custom ReAct Loop
|
|
86
|
+
|
|
87
|
+
**The Pattern**: Build your own Reasoning + Acting loop using the three core node types.
|
|
88
|
+
|
|
89
|
+
### The Workflow
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
example_workflow:
|
|
93
|
+
Reason:
|
|
94
|
+
node_type: llm
|
|
95
|
+
event_triggers: [START, TOOL_RESULT]
|
|
96
|
+
prompt: |
|
|
97
|
+
Task: {{ context.task }}
|
|
98
|
+
{% if context.tool_results %}
|
|
99
|
+
Previous tool results: {{ context.tool_results }}
|
|
100
|
+
{% endif %}
|
|
101
|
+
|
|
102
|
+
Decide what to do next. If you need to use a tool, output:
|
|
103
|
+
{"action": "use_tool", "tool": "tool_name", "args": {...}}
|
|
104
|
+
|
|
105
|
+
If you have the final answer, output:
|
|
106
|
+
{"action": "finish", "answer": "your answer"}
|
|
107
|
+
output_field: decision
|
|
108
|
+
event_emissions:
|
|
109
|
+
- signal_name: DECIDED
|
|
110
|
+
|
|
111
|
+
Route:
|
|
112
|
+
node_type: router
|
|
113
|
+
event_triggers: [DECIDED]
|
|
114
|
+
event_emissions:
|
|
115
|
+
- signal_name: USE_TOOL
|
|
116
|
+
condition: "{{ context.decision.action == 'use_tool' }}"
|
|
117
|
+
- signal_name: FINISH
|
|
118
|
+
condition: "{{ context.decision.action == 'finish' }}"
|
|
119
|
+
|
|
120
|
+
Act:
|
|
121
|
+
node_type: tool
|
|
122
|
+
event_triggers: [USE_TOOL]
|
|
123
|
+
tool_name: dynamic_tool
|
|
124
|
+
context_parameter_field: decision
|
|
125
|
+
output_field: tool_results
|
|
126
|
+
event_emissions:
|
|
127
|
+
- signal_name: TOOL_RESULT
|
|
128
|
+
|
|
129
|
+
Complete:
|
|
130
|
+
node_type: router
|
|
131
|
+
event_triggers: [FINISH]
|
|
132
|
+
event_emissions:
|
|
133
|
+
- signal_name: TASK_COMPLETE
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### How It Works
|
|
137
|
+
|
|
138
|
+
1. **Reason**: LLM analyzes the situation and decides what to do
|
|
139
|
+
2. **Route**: Router checks if LLM wants to use a tool or finish
|
|
140
|
+
3. **Act**: If tool needed, execute it and loop back to Reason
|
|
141
|
+
4. **Complete**: When done, emit final signal
|
|
142
|
+
|
|
143
|
+
### The Loop Structure
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
┌──────────────────────────────────────────────────────┐
|
|
147
|
+
│ │
|
|
148
|
+
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
|
149
|
+
│ │ Reason │───▶│ Route │───▶│ Act │──────────┘
|
|
150
|
+
│ │ (LLM) │ │(Router) │ │ (Tool) │
|
|
151
|
+
│ └─────────┘ └────┬────┘ └─────────┘
|
|
152
|
+
│ │
|
|
153
|
+
│ ▼
|
|
154
|
+
│ ┌─────────┐
|
|
155
|
+
│ │Complete │
|
|
156
|
+
│ │(Router) │
|
|
157
|
+
│ └─────────┘
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This is exactly what the Agent Node does internally—but now you control every piece.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Pattern 3: Metacognition (Self-Reflection)
|
|
165
|
+
|
|
166
|
+
**The Pattern**: Have the LLM review and critique its own output before finalizing.
|
|
167
|
+
|
|
168
|
+
### The Workflow
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
example_workflow:
|
|
172
|
+
Generate:
|
|
173
|
+
node_type: llm
|
|
174
|
+
event_triggers: [START]
|
|
175
|
+
prompt: "Write a response to: {{ context.request }}"
|
|
176
|
+
output_field: draft
|
|
177
|
+
event_emissions:
|
|
178
|
+
- signal_name: DRAFT_READY
|
|
179
|
+
|
|
180
|
+
Critique:
|
|
181
|
+
node_type: llm
|
|
182
|
+
event_triggers: [DRAFT_READY]
|
|
183
|
+
prompt: |
|
|
184
|
+
Review this response for errors, unclear language, or improvements:
|
|
185
|
+
{{ context.draft }}
|
|
186
|
+
|
|
187
|
+
Output JSON with needs_revision (true or false) and critique fields.
|
|
188
|
+
output_field: review
|
|
189
|
+
event_emissions:
|
|
190
|
+
- signal_name: REVIEWED
|
|
191
|
+
|
|
192
|
+
CheckRevision:
|
|
193
|
+
node_type: router
|
|
194
|
+
event_triggers: [REVIEWED]
|
|
195
|
+
event_emissions:
|
|
196
|
+
- signal_name: REVISE
|
|
197
|
+
condition: "{{ context.review.needs_revision == true }}"
|
|
198
|
+
- signal_name: FINALIZE
|
|
199
|
+
condition: "{{ context.review.needs_revision == false }}"
|
|
200
|
+
|
|
201
|
+
Refine:
|
|
202
|
+
node_type: llm
|
|
203
|
+
event_triggers: [REVISE]
|
|
204
|
+
prompt: |
|
|
205
|
+
Original: {{ context.draft }}
|
|
206
|
+
Critique: {{ context.review.critique }}
|
|
207
|
+
|
|
208
|
+
Write an improved version addressing the feedback.
|
|
209
|
+
output_field: final_response
|
|
210
|
+
event_emissions:
|
|
211
|
+
- signal_name: COMPLETE
|
|
212
|
+
|
|
213
|
+
AcceptDraft:
|
|
214
|
+
node_type: router
|
|
215
|
+
event_triggers: [FINALIZE]
|
|
216
|
+
event_emissions:
|
|
217
|
+
- signal_name: COMPLETE
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### How It Works
|
|
221
|
+
|
|
222
|
+
1. **Generate**: LLM produces an initial response
|
|
223
|
+
2. **Critique**: A second LLM call reviews the response for errors or improvements
|
|
224
|
+
3. **Route**: Check if revision is needed
|
|
225
|
+
4. **Refine** (if needed): Generate improved response based on critique
|
|
226
|
+
5. **Finalize**: Output the best version
|
|
227
|
+
|
|
228
|
+
### Why This Pattern?
|
|
229
|
+
|
|
230
|
+
- **Quality improvement**: Catches errors the first pass missed
|
|
231
|
+
- **Self-correction**: The model identifies its own weaknesses
|
|
232
|
+
- **Controllable iteration**: You decide when to stop refining
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Pattern 4: Parallel Analysis with Voting
|
|
237
|
+
|
|
238
|
+
**The Pattern**: Run multiple LLM analyses in parallel, then aggregate results.
|
|
239
|
+
|
|
240
|
+
### The Workflow
|
|
241
|
+
|
|
242
|
+
```yaml
|
|
243
|
+
example_workflow:
|
|
244
|
+
FanOut:
|
|
245
|
+
node_type: router
|
|
246
|
+
event_triggers: [START]
|
|
247
|
+
event_emissions:
|
|
248
|
+
- signal_name: ANALYZE_SAFETY
|
|
249
|
+
- signal_name: ANALYZE_QUALITY
|
|
250
|
+
- signal_name: ANALYZE_RELEVANCE
|
|
251
|
+
|
|
252
|
+
SafetyCheck:
|
|
253
|
+
node_type: llm
|
|
254
|
+
event_triggers: [ANALYZE_SAFETY]
|
|
255
|
+
prompt: |
|
|
256
|
+
Check if this content is safe: {{ context.content }}
|
|
257
|
+
Return JSON with safe (boolean) and reason (string) fields.
|
|
258
|
+
output_field: safety_result
|
|
259
|
+
event_emissions:
|
|
260
|
+
- signal_name: SAFETY_DONE
|
|
261
|
+
|
|
262
|
+
QualityCheck:
|
|
263
|
+
node_type: llm
|
|
264
|
+
event_triggers: [ANALYZE_QUALITY]
|
|
265
|
+
prompt: |
|
|
266
|
+
Rate the quality of this content: {{ context.content }}
|
|
267
|
+
Return JSON with score (1-10) and feedback (string) fields.
|
|
268
|
+
output_field: quality_result
|
|
269
|
+
event_emissions:
|
|
270
|
+
- signal_name: QUALITY_DONE
|
|
271
|
+
|
|
272
|
+
RelevanceCheck:
|
|
273
|
+
node_type: llm
|
|
274
|
+
event_triggers: [ANALYZE_RELEVANCE]
|
|
275
|
+
prompt: |
|
|
276
|
+
Check relevance to topic '{{ context.topic }}': {{ context.content }}
|
|
277
|
+
Return JSON with relevant (boolean) field.
|
|
278
|
+
output_field: relevance_result
|
|
279
|
+
event_emissions:
|
|
280
|
+
- signal_name: RELEVANCE_DONE
|
|
281
|
+
|
|
282
|
+
Aggregate:
|
|
283
|
+
node_type: tool
|
|
284
|
+
event_triggers: [SAFETY_DONE, QUALITY_DONE, RELEVANCE_DONE]
|
|
285
|
+
tool_name: aggregate_votes
|
|
286
|
+
context_parameter_field: vote_params
|
|
287
|
+
output_field: final_decision
|
|
288
|
+
event_emissions:
|
|
289
|
+
- signal_name: APPROVED
|
|
290
|
+
condition: "{{ result.approved == true }}"
|
|
291
|
+
- signal_name: REJECTED
|
|
292
|
+
condition: "{{ result.approved == false }}"
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### How It Works
|
|
296
|
+
|
|
297
|
+
1. **Fan-Out**: Router emits multiple signals simultaneously
|
|
298
|
+
2. **Parallel Analysis**: Multiple LLM nodes run independently
|
|
299
|
+
3. **Aggregate**: Tool collects all analyses and determines consensus
|
|
300
|
+
4. **Route Result**: Based on the aggregated vote
|
|
301
|
+
|
|
302
|
+
### Why This Pattern?
|
|
303
|
+
|
|
304
|
+
- **Diversity of perspective**: Different prompts catch different issues
|
|
305
|
+
- **Robustness**: Single LLM errors are outvoted
|
|
306
|
+
- **Speed**: Parallel execution (if your infrastructure supports it)
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Pattern 5: Iterative Refinement with Tools
|
|
311
|
+
|
|
312
|
+
**The Pattern**: LLM generates, tool validates, loop until valid.
|
|
313
|
+
|
|
314
|
+
### The Workflow
|
|
315
|
+
|
|
316
|
+
```yaml
|
|
317
|
+
example_workflow:
|
|
318
|
+
Generate:
|
|
319
|
+
node_type: llm
|
|
320
|
+
event_triggers: [START, RETRY]
|
|
321
|
+
prompt: |
|
|
322
|
+
Generate Python code for: {{ context.task }}
|
|
323
|
+
{% if context.errors %}
|
|
324
|
+
Previous attempt had these errors: {{ context.errors }}
|
|
325
|
+
Fix them in this attempt.
|
|
326
|
+
{% endif %}
|
|
327
|
+
output_field: code
|
|
328
|
+
event_emissions:
|
|
329
|
+
- signal_name: CODE_GENERATED
|
|
330
|
+
|
|
331
|
+
Validate:
|
|
332
|
+
node_type: tool
|
|
333
|
+
event_triggers: [CODE_GENERATED]
|
|
334
|
+
tool_name: lint_code
|
|
335
|
+
context_parameter_field: code
|
|
336
|
+
output_field: validation
|
|
337
|
+
event_emissions:
|
|
338
|
+
- signal_name: VALID
|
|
339
|
+
condition: "{{ result.errors|length == 0 }}"
|
|
340
|
+
- signal_name: INVALID
|
|
341
|
+
condition: "{{ result.errors|length > 0 }}"
|
|
342
|
+
|
|
343
|
+
PrepareRetry:
|
|
344
|
+
node_type: router
|
|
345
|
+
event_triggers: [INVALID]
|
|
346
|
+
event_emissions:
|
|
347
|
+
- signal_name: RETRY
|
|
348
|
+
|
|
349
|
+
Complete:
|
|
350
|
+
node_type: router
|
|
351
|
+
event_triggers: [VALID]
|
|
352
|
+
event_emissions:
|
|
353
|
+
- signal_name: CODE_COMPLETE
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### How It Works
|
|
357
|
+
|
|
358
|
+
1. **Generate**: LLM creates code/content
|
|
359
|
+
2. **Validate**: Tool runs linter, tests, or other validation
|
|
360
|
+
3. **Route**: Check validation result
|
|
361
|
+
4. **Loop or Complete**: If invalid, inject errors and regenerate
|
|
362
|
+
|
|
363
|
+
### The Loop Structure
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
┌────────────────────────────────────────────────┐
|
|
367
|
+
│ │
|
|
368
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
369
|
+
│ │ Generate │───▶│ Validate │───▶│ Route │─┘
|
|
370
|
+
│ │ (LLM) │ │ (Tool) │ │ (Router) │
|
|
371
|
+
│ └──────────┘ └──────────┘ └────┬─────┘
|
|
372
|
+
│ │
|
|
373
|
+
│ ▼
|
|
374
|
+
│ ┌──────────┐
|
|
375
|
+
│ │ Complete │
|
|
376
|
+
│ └──────────┘
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Pattern 6: Hierarchical Task Decomposition
|
|
382
|
+
|
|
383
|
+
**The Pattern**: Break complex tasks into subtasks, solve each, then synthesize.
|
|
384
|
+
|
|
385
|
+
### The Workflow
|
|
386
|
+
|
|
387
|
+
```yaml
|
|
388
|
+
example_workflow:
|
|
389
|
+
Decompose:
|
|
390
|
+
node_type: llm
|
|
391
|
+
event_triggers: [START]
|
|
392
|
+
prompt: |
|
|
393
|
+
Break this task into subtasks: {{ context.task }}
|
|
394
|
+
Output JSON with subtasks array and types array (research, code, writing).
|
|
395
|
+
output_field: breakdown
|
|
396
|
+
event_emissions:
|
|
397
|
+
- signal_name: DECOMPOSED
|
|
398
|
+
|
|
399
|
+
RouteSubtasks:
|
|
400
|
+
node_type: router
|
|
401
|
+
event_triggers: [DECOMPOSED]
|
|
402
|
+
event_emissions:
|
|
403
|
+
- signal_name: DO_RESEARCH
|
|
404
|
+
condition: "{{ 'research' in context.breakdown.types }}"
|
|
405
|
+
- signal_name: DO_CODING
|
|
406
|
+
condition: "{{ 'code' in context.breakdown.types }}"
|
|
407
|
+
- signal_name: DO_WRITING
|
|
408
|
+
condition: "{{ 'writing' in context.breakdown.types }}"
|
|
409
|
+
|
|
410
|
+
ResearchSubtask:
|
|
411
|
+
node_type: llm
|
|
412
|
+
event_triggers: [DO_RESEARCH]
|
|
413
|
+
prompt: "Research: {{ context.breakdown.subtasks | selectattr('type', 'equalto', 'research') | list }}"
|
|
414
|
+
output_field: research_result
|
|
415
|
+
event_emissions:
|
|
416
|
+
- signal_name: SUBTASK_DONE
|
|
417
|
+
|
|
418
|
+
CodingSubtask:
|
|
419
|
+
node_type: llm
|
|
420
|
+
event_triggers: [DO_CODING]
|
|
421
|
+
prompt: "Code: {{ context.breakdown.subtasks | selectattr('type', 'equalto', 'code') | list }}"
|
|
422
|
+
output_field: code_result
|
|
423
|
+
event_emissions:
|
|
424
|
+
- signal_name: SUBTASK_DONE
|
|
425
|
+
|
|
426
|
+
Synthesize:
|
|
427
|
+
node_type: llm
|
|
428
|
+
event_triggers: [SUBTASK_DONE]
|
|
429
|
+
prompt: |
|
|
430
|
+
Combine these results into a final answer:
|
|
431
|
+
Research: {{ context.research_result }}
|
|
432
|
+
Code: {{ context.code_result }}
|
|
433
|
+
output_field: final_answer
|
|
434
|
+
event_emissions:
|
|
435
|
+
- signal_name: COMPLETE
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### How It Works
|
|
439
|
+
|
|
440
|
+
1. **Decompose**: LLM breaks the task into subtasks
|
|
441
|
+
2. **Fan-Out**: Router emits signals for each subtask type
|
|
442
|
+
3. **Solve**: Specialized nodes handle each subtask
|
|
443
|
+
4. **Synthesize**: Combine subtask results into final answer
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Combining Patterns
|
|
448
|
+
|
|
449
|
+
The real power comes from combining patterns. For example:
|
|
450
|
+
|
|
451
|
+
- **Chain-of-Thought + Metacognition**: Reason step-by-step, then self-review
|
|
452
|
+
- **Custom ReAct + Iterative Refinement**: Agent loop with validation gates
|
|
453
|
+
- **Parallel Voting + Hierarchical Decomposition**: Ensemble of specialized agents
|
|
454
|
+
|
|
455
|
+
Since everything is YAML, you can compose patterns freely.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## When to Use Custom Workflows vs Agent Node
|
|
460
|
+
|
|
461
|
+
| Use Custom Workflows When... | Use Agent Node When... |
|
|
462
|
+
|------------------------------|------------------------|
|
|
463
|
+
| You need non-standard reasoning patterns | Standard ReAct loop is sufficient |
|
|
464
|
+
| You want explicit control over each step | You want hands-off tool-using agent |
|
|
465
|
+
| You're debugging or iterating on agent logic | You need quick prototyping |
|
|
466
|
+
| You need deterministic checkpoints | Tool selection is the main complexity |
|
|
467
|
+
| You're building production systems with audit requirements | Development speed matters most |
|
|
468
|
+
|
|
469
|
+
The Agent Node is a **convenience**—it encapsulates a common pattern. These custom workflows show you can build anything.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Next Steps
|
|
474
|
+
|
|
475
|
+
Now that you can build custom agent patterns, see how the built-in [Agent Node](guide_05_agent.md) encapsulates the ReAct pattern for convenience →
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
|
|
2
|
+
# SOE Guide: Chapter 5 - Agent Nodes
|
|
3
|
+
|
|
4
|
+
## Introduction to Agent Nodes
|
|
5
|
+
|
|
6
|
+
The **Agent Node** is a convenience wrapper that encapsulates the **ReAct pattern** (Reasoning + Acting) into a single, configurable component.
|
|
7
|
+
|
|
8
|
+
In the previous chapter, you learned how to build custom agent loops using Tool, LLM, and Router nodes. The Agent Node does the same thing—but packaged for common use cases where you want a "hands-off" tool-using agent.
|
|
9
|
+
|
|
10
|
+
### Why Use Agent Nodes?
|
|
11
|
+
|
|
12
|
+
| Building Custom (Ch. 4) | Using Agent Node |
|
|
13
|
+
|------------------------|------------------|
|
|
14
|
+
| Full control over each step | Quick to configure |
|
|
15
|
+
| Custom reasoning patterns | Standard ReAct loop |
|
|
16
|
+
| Explicit debugging points | Batteries included |
|
|
17
|
+
| Any architecture you want | Opinionated but effective |
|
|
18
|
+
|
|
19
|
+
**Use Agent Node when**: You need a straightforward tool-using agent and don't need custom reasoning patterns.
|
|
20
|
+
|
|
21
|
+
**Use custom workflows when**: You need chain-of-thought, metacognition, voting, or other advanced patterns.
|
|
22
|
+
|
|
23
|
+
**Note**: Agent nodes can also be used for other patterns (routing, summarization, etc.), but they involve more LLM calls than specialized nodes. Use the right node type for your use case.
|
|
24
|
+
|
|
25
|
+
### The Agent Loop
|
|
26
|
+
|
|
27
|
+
The Agent Node runs this loop internally:
|
|
28
|
+
|
|
29
|
+
1. **Router Stage**: The agent decides what to do (Call a Tool or Finish)
|
|
30
|
+
2. **Parameter Stage**: If calling a tool, it generates the arguments
|
|
31
|
+
3. **Execution Stage**: The tool is executed
|
|
32
|
+
4. **Loop**: The results are fed back into the history, and the agent decides again
|
|
33
|
+
|
|
34
|
+
This is exactly what you built manually in Chapter 4's "Custom ReAct Loop" pattern—but as a single node.
|
|
35
|
+
|
|
36
|
+
### Multiple Tool Calls
|
|
37
|
+
|
|
38
|
+
The agent can call multiple tools in sequence during a single node execution:
|
|
39
|
+
|
|
40
|
+
1. Agent decides to call Tool A → executes → sees result
|
|
41
|
+
2. Agent decides to call Tool B → executes → sees result
|
|
42
|
+
3. Agent decides to call Tool A again → executes → sees result
|
|
43
|
+
4. Agent decides to Finish
|
|
44
|
+
|
|
45
|
+
**Exhaustive tool calling**: The agent will keep calling tools until it decides to finish. There's no hard limit on tool calls—the agent uses its judgment. To prevent runaway loops, use operational limits (see [Operational Features](advanced_patterns/operational.md)).
|
|
46
|
+
|
|
47
|
+
## Configuring Tools
|
|
48
|
+
|
|
49
|
+
Agents have access to the tools you provide in their `tools` list.
|
|
50
|
+
|
|
51
|
+
> [!IMPORTANT]
|
|
52
|
+
> **Tool Restriction**: Agent nodes can ONLY call tools explicitly listed in their `tools` configuration, even if a tool is a SOE built-in. If you want an agent to be able to use a built-in tool like `soe_inject_node`, you must include it in the `tools` list for that node.
|
|
53
|
+
|
|
54
|
+
### Example: Tool Restriction
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
MyAgent:
|
|
58
|
+
node_type: agent
|
|
59
|
+
tools:
|
|
60
|
+
- calculate_total # User tool
|
|
61
|
+
- soe_inject_node # Built-in tool (MUST be listed here)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### The Workflow
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
example_workflow:
|
|
68
|
+
CalculatorAgent:
|
|
69
|
+
node_type: agent
|
|
70
|
+
event_triggers: [START]
|
|
71
|
+
prompt: "You are a calculator. Solve the user's math problem: {{ context.problem }}"
|
|
72
|
+
tools: [calculator]
|
|
73
|
+
output_field: result
|
|
74
|
+
event_emissions:
|
|
75
|
+
- signal_name: CALCULATION_DONE
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### How It Works
|
|
79
|
+
|
|
80
|
+
1. **`tools`**: We give the agent a list of tools (e.g., `[calculator]`).
|
|
81
|
+
2. **The Loop**:
|
|
82
|
+
* The agent sees the prompt: "Solve... {{ context.problem }}"
|
|
83
|
+
* It decides to call `calculator(5, 3, 'add')`.
|
|
84
|
+
* The tool returns `8`.
|
|
85
|
+
* The agent sees the result and decides to **Finish**.
|
|
86
|
+
3. **Output**: The final answer is stored in `context.result`.
|
|
87
|
+
|
|
88
|
+
## Advanced Configuration: Multiple Tools
|
|
89
|
+
|
|
90
|
+
Agents can handle complex tasks with multiple tools.
|
|
91
|
+
|
|
92
|
+
### The Workflow
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
example_workflow:
|
|
96
|
+
ResearchAgent:
|
|
97
|
+
node_type: agent
|
|
98
|
+
event_triggers: [START]
|
|
99
|
+
prompt: "Research the topic: {{ context.topic }}"
|
|
100
|
+
tools: [search_web, summarize_text]
|
|
101
|
+
output_field: report
|
|
102
|
+
event_emissions:
|
|
103
|
+
- signal_name: REPORT_READY
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Robustness Features
|
|
107
|
+
|
|
108
|
+
- **Tool Selection**: The agent intelligently chooses between `search_web` and `summarize_text` based on the goal.
|
|
109
|
+
- **Error Recovery**: If a tool fails (throws an exception), the agent sees the error and can try again or try a different strategy.
|
|
110
|
+
|
|
111
|
+
## Agent Signal Selection
|
|
112
|
+
|
|
113
|
+
Like LLM nodes, Agent nodes can select which signal to emit based on their analysis:
|
|
114
|
+
|
|
115
|
+
### The Workflow
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
example_workflow:
|
|
119
|
+
AnalysisAgent:
|
|
120
|
+
node_type: agent
|
|
121
|
+
event_triggers: [START]
|
|
122
|
+
prompt: "Analyze the data and determine if it needs human review: {{ context.data }}"
|
|
123
|
+
tools: [analyze_data]
|
|
124
|
+
output_field: analysis
|
|
125
|
+
event_emissions:
|
|
126
|
+
- signal_name: AUTO_APPROVE
|
|
127
|
+
condition: "The analysis shows the data is clearly valid"
|
|
128
|
+
- signal_name: NEEDS_REVIEW
|
|
129
|
+
condition: "The analysis shows the data requires human review"
|
|
130
|
+
- signal_name: REJECT
|
|
131
|
+
condition: "The analysis shows the data is clearly invalid"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The agent completes its task, then the signal selection works the same as LLM nodes:
|
|
135
|
+
- **Plain text conditions**: LLM chooses semantically
|
|
136
|
+
- **Jinja conditions**: Evaluated programmatically
|
|
137
|
+
|
|
138
|
+
## When to Choose Agent vs Custom Workflow
|
|
139
|
+
|
|
140
|
+
### Use Agent Node For:
|
|
141
|
+
|
|
142
|
+
- **Quick prototyping**: Get a tool-using agent running fast
|
|
143
|
+
- **Standard tasks**: Research, calculation, data gathering
|
|
144
|
+
- **Simple tool orchestration**: When tool selection is the main complexity
|
|
145
|
+
|
|
146
|
+
### Build Custom Workflows For:
|
|
147
|
+
|
|
148
|
+
- **Chain-of-thought reasoning**: Explicit step-by-step thinking
|
|
149
|
+
- **Metacognition**: Self-review and refinement loops
|
|
150
|
+
- **Parallel/voting patterns**: Multiple analyses combined
|
|
151
|
+
- **Hybrid logic**: Mixing deterministic gates with AI
|
|
152
|
+
- **Audit requirements**: When you need to log/inspect each decision
|
|
153
|
+
- **Custom termination conditions**: Beyond simple "finish" detection
|
|
154
|
+
|
|
155
|
+
Remember: The Agent Node is **syntactic sugar** for a common pattern. Everything it does, you can build yourself with the three core node types.
|
|
156
|
+
|
|
157
|
+
## Next Steps
|
|
158
|
+
|
|
159
|
+
Now that you understand both custom workflows and the Agent Node, let's explore [Schemas](guide_06_schema.md) for structured LLM output →
|