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,206 @@
|
|
|
1
|
+
|
|
2
|
+
# SOE Guide: Chapter 1 - Tool Nodes
|
|
3
|
+
|
|
4
|
+
## Introduction to Tool Nodes
|
|
5
|
+
|
|
6
|
+
The **Tool Node** executes a Python function directly. It's the most concrete node type in SOE—it does real work: calling APIs, processing data, interacting with databases.
|
|
7
|
+
|
|
8
|
+
We start with Tool nodes because they represent **actual execution**. While other nodes route signals or generate text, Tool nodes are where your workflow touches the real world.
|
|
9
|
+
|
|
10
|
+
### When to Use Tool Nodes
|
|
11
|
+
|
|
12
|
+
- **External Operations**: Sending emails, processing payments, database queries
|
|
13
|
+
- **API Calls**: HTTP requests, third-party service integrations
|
|
14
|
+
- **Data Transformations**: Calculations, file processing, format conversions
|
|
15
|
+
- **Deterministic Logic**: When you need code, not AI
|
|
16
|
+
|
|
17
|
+
## Your First Tool Node
|
|
18
|
+
|
|
19
|
+
Let's send an email using a tool node.
|
|
20
|
+
|
|
21
|
+
### The Workflow
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
example_workflow:
|
|
25
|
+
SendEmail:
|
|
26
|
+
node_type: tool
|
|
27
|
+
event_triggers: [START]
|
|
28
|
+
tool_name: send_email
|
|
29
|
+
context_parameter_field: email_data
|
|
30
|
+
output_field: email_result
|
|
31
|
+
event_emissions:
|
|
32
|
+
- signal_name: EMAIL_SENT
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### How It Works
|
|
36
|
+
|
|
37
|
+
1. **`tool_name`**: The key in your `tools_registry` dict.
|
|
38
|
+
2. **`context_parameter_field`**: The context field containing the tool's kwargs (a dict).
|
|
39
|
+
3. **`output_field`**: Where to store the result in context.
|
|
40
|
+
4. **`event_emissions`**: Signals to emit after execution (conditions evaluate `result`).
|
|
41
|
+
|
|
42
|
+
### Understanding context_parameter_field
|
|
43
|
+
|
|
44
|
+
The `context_parameter_field` specifies which context field contains the parameters to pass to your tool. This field must contain a dictionary that will be unpacked as keyword arguments.
|
|
45
|
+
|
|
46
|
+
**Where does this data come from?**
|
|
47
|
+
|
|
48
|
+
1. **Initial context**: You can pass it when starting the workflow:
|
|
49
|
+
```python
|
|
50
|
+
orchestrate(
|
|
51
|
+
config=workflow,
|
|
52
|
+
initial_context={"email_data": {"to": "user@example.com", "subject": "Hello"}}
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
2. **LLM output**: An LLM node can generate structured parameters:
|
|
57
|
+
```yaml
|
|
58
|
+
ExtractEmailParams:
|
|
59
|
+
node_type: llm
|
|
60
|
+
prompt: "Extract email parameters from: {{ context.user_request }}"
|
|
61
|
+
output_field: email_data # LLM returns {"to": "...", "subject": "...", "body": "..."}
|
|
62
|
+
|
|
63
|
+
SendEmail:
|
|
64
|
+
node_type: tool
|
|
65
|
+
tool_name: send_email
|
|
66
|
+
context_parameter_field: email_data # Uses LLM output as tool input
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. **Another tool's output**: Chain tools together:
|
|
70
|
+
```yaml
|
|
71
|
+
PrepareData:
|
|
72
|
+
node_type: tool
|
|
73
|
+
tool_name: prepare_email_data
|
|
74
|
+
output_field: email_data
|
|
75
|
+
|
|
76
|
+
SendEmail:
|
|
77
|
+
node_type: tool
|
|
78
|
+
tool_name: send_email
|
|
79
|
+
context_parameter_field: email_data # Uses previous tool's output
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Tip**: Use [Context Schema](guide_06_schema.md) to validate that LLM output has the correct structure before passing to tools.
|
|
83
|
+
|
|
84
|
+
### The Tools Registry
|
|
85
|
+
|
|
86
|
+
Tools are registered as plain Python functions:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
def send_email(to: str, subject: str, body: str) -> dict:
|
|
90
|
+
# Your email sending logic
|
|
91
|
+
return {"status": "sent", "to": to}
|
|
92
|
+
|
|
93
|
+
tools_registry = {
|
|
94
|
+
"send_email": send_email,
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Event Emissions with Conditions
|
|
99
|
+
|
|
100
|
+
Tool nodes use `event_emissions` with optional Jinja conditions. Signals without conditions are always emitted on success. Signals with conditions can reference:
|
|
101
|
+
|
|
102
|
+
- **`result`**: The tool's return value
|
|
103
|
+
- **`context`**: The full workflow context
|
|
104
|
+
|
|
105
|
+
### Conditional Signal Example
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
example_workflow:
|
|
109
|
+
ProcessPayment:
|
|
110
|
+
node_type: tool
|
|
111
|
+
event_triggers: [START]
|
|
112
|
+
tool_name: process_payment
|
|
113
|
+
context_parameter_field: payment_data
|
|
114
|
+
output_field: payment_result
|
|
115
|
+
event_emissions:
|
|
116
|
+
- signal_name: PAYMENT_SUCCESS
|
|
117
|
+
condition: "{{ result.status == 'approved' }}"
|
|
118
|
+
- signal_name: PAYMENT_DECLINED
|
|
119
|
+
condition: "{{ result.status == 'declined' }}"
|
|
120
|
+
- signal_name: PAYMENT_PENDING
|
|
121
|
+
condition: "{{ result.status == 'pending' }}"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### How It Works
|
|
125
|
+
|
|
126
|
+
1. Tool executes and stores result in `output_field`.
|
|
127
|
+
2. Each `event_emissions` condition is evaluated against both `result` and `context`.
|
|
128
|
+
3. Signals with matching conditions (or no condition) are emitted.
|
|
129
|
+
|
|
130
|
+
**Note:** `event_emissions` are only evaluated on successful execution. For failure handling, use `failure_signal` in the registry.
|
|
131
|
+
|
|
132
|
+
### Combining Result and Context
|
|
133
|
+
|
|
134
|
+
Conditions can use both the tool's output and existing context values:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
example_workflow:
|
|
138
|
+
ProcessPayment:
|
|
139
|
+
node_type: tool
|
|
140
|
+
event_triggers: [START]
|
|
141
|
+
tool_name: process_payment
|
|
142
|
+
context_parameter_field: payment_data
|
|
143
|
+
output_field: payment_result
|
|
144
|
+
event_emissions:
|
|
145
|
+
- signal_name: PAYMENT_SUCCESS
|
|
146
|
+
condition: "{{ result.status == 'approved' }}"
|
|
147
|
+
- signal_name: VIP_PAYMENT_SUCCESS
|
|
148
|
+
condition: "{{ result.status == 'approved' and context.customer.is_vip }}"
|
|
149
|
+
- signal_name: LARGE_PAYMENT_SUCCESS
|
|
150
|
+
condition: "{{ result.status == 'approved' and context.payment_data.amount > 1000 }}"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This is useful when you need to:
|
|
154
|
+
- Combine tool output with customer data (VIP handling)
|
|
155
|
+
- Check result against thresholds from context
|
|
156
|
+
- Route based on both tool success and workflow state
|
|
157
|
+
|
|
158
|
+
## Extended Tool Registry
|
|
159
|
+
|
|
160
|
+
For more control over tool behavior, use the extended format:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
tools_registry = {
|
|
164
|
+
# Simple format (default: max_retries=1)
|
|
165
|
+
"send_email": send_email,
|
|
166
|
+
|
|
167
|
+
# Extended format with configuration
|
|
168
|
+
"process_payment": {
|
|
169
|
+
"function": process_payment,
|
|
170
|
+
"max_retries": 3, # Retry up to 3 times on failure
|
|
171
|
+
"failure_signal": "PAYMENT_FAILED", # Emit when all retries exhausted
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Extended registry fields:**
|
|
177
|
+
|
|
178
|
+
| Field | Type | Default | Description |
|
|
179
|
+
|-------|------|---------|-------------|
|
|
180
|
+
| `function` | Callable | (required) | The Python function to execute |
|
|
181
|
+
| `max_retries` | int | 1 | Number of retries after initial failure |
|
|
182
|
+
| `failure_signal` | str | None | Signal to emit when all retries exhausted |
|
|
183
|
+
| `process_accumulated` | bool | False | Pass full history list (advanced) |
|
|
184
|
+
|
|
185
|
+
> **Note:** The `process_accumulated` option is an advanced feature for aggregation patterns. It allows a tool to receive the entire history of a context field instead of just the last value. See [Fan-Out, Fan-In & Aggregations](advanced_patterns/guide_fanout_and_aggregations.md) for details.
|
|
186
|
+
|
|
187
|
+
### Success vs Failure Flow
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Tool executes
|
|
191
|
+
│
|
|
192
|
+
├── Success → evaluate event_emissions → emit matching signals
|
|
193
|
+
│
|
|
194
|
+
└── Exception → retry up to max_retries
|
|
195
|
+
│
|
|
196
|
+
└── All retries failed → emit failure_signal (if defined)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**When to use extended format:**
|
|
200
|
+
|
|
201
|
+
- **Flaky operations**: Set `max_retries` higher for network calls or external APIs
|
|
202
|
+
- **Critical failures**: Use `failure_signal` to handle unrecoverable errors
|
|
203
|
+
|
|
204
|
+
## Next Steps
|
|
205
|
+
|
|
206
|
+
Now that you can execute real operations, let's add intelligence with [LLM Nodes](guide_02_llm.md) →
|
soe/docs/guide_02_llm.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
|
|
2
|
+
# SOE Guide: Chapter 2 - LLM Nodes
|
|
3
|
+
|
|
4
|
+
## Introduction to LLM Nodes
|
|
5
|
+
|
|
6
|
+
The **LLM Node** is a simple way to call a language model directly. Unlike Agent nodes (which we'll cover later), LLM nodes make a single call to the model and store the response.
|
|
7
|
+
|
|
8
|
+
Think of an LLM node as a specialist: it receives context, generates a response, and passes it along.
|
|
9
|
+
|
|
10
|
+
## Your First LLM Node: Text Summarization
|
|
11
|
+
|
|
12
|
+
Let's start with a common pattern: summarizing text.
|
|
13
|
+
|
|
14
|
+
### The Workflow
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
example_workflow:
|
|
18
|
+
SimpleLLM:
|
|
19
|
+
node_type: llm
|
|
20
|
+
event_triggers: [START]
|
|
21
|
+
prompt: "Summarize the following text in one sentence: {{ context.text }}"
|
|
22
|
+
output_field: summary
|
|
23
|
+
event_emissions:
|
|
24
|
+
- signal_name: SUMMARY_COMPLETE
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### How It Works
|
|
28
|
+
|
|
29
|
+
1. **Event Trigger**: The `START` signal triggers the `SimpleLLM` node.
|
|
30
|
+
2. **Prompt Rendering**: The Jinja2 template `{{ context.text }}` is replaced with the actual text.
|
|
31
|
+
3. **LLM Call**: The rendered prompt is sent to your LLM provider.
|
|
32
|
+
4. **Output Storage**: The response is stored in `context.summary` (the `output_field`).
|
|
33
|
+
5. **Signal Emission**: `SUMMARY_COMPLETE` is emitted to continue the workflow.
|
|
34
|
+
|
|
35
|
+
### Key Concepts
|
|
36
|
+
|
|
37
|
+
- **`prompt`**: A Jinja2 template. Use `{{ context.field }}` to inject context values.
|
|
38
|
+
- **`output_field`**: Where the LLM response is stored in context.
|
|
39
|
+
- **`event_emissions`**: Signals to emit after the LLM call completes.
|
|
40
|
+
|
|
41
|
+
## Chaining LLM Nodes
|
|
42
|
+
|
|
43
|
+
LLM nodes become powerful when chained together. Each node can use the output of the previous one.
|
|
44
|
+
|
|
45
|
+
### The Workflow
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
example_workflow:
|
|
49
|
+
Translator:
|
|
50
|
+
node_type: llm
|
|
51
|
+
event_triggers: [START]
|
|
52
|
+
prompt: "Translate the following to Spanish: {{ context.text }}"
|
|
53
|
+
output_field: spanish_text
|
|
54
|
+
event_emissions:
|
|
55
|
+
- signal_name: TRANSLATED
|
|
56
|
+
|
|
57
|
+
Summarizer:
|
|
58
|
+
node_type: llm
|
|
59
|
+
event_triggers: [TRANSLATED]
|
|
60
|
+
prompt: "Summarize this Spanish text: {{ context.spanish_text }}"
|
|
61
|
+
output_field: summary
|
|
62
|
+
event_emissions:
|
|
63
|
+
- signal_name: CHAIN_COMPLETE
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### How It Works
|
|
67
|
+
|
|
68
|
+
1. `START` triggers `Translator`
|
|
69
|
+
2. Translator stores Spanish text in `context.spanish_text`
|
|
70
|
+
3. Translator emits `TRANSLATED`
|
|
71
|
+
4. `TRANSLATED` triggers `Summarizer`
|
|
72
|
+
5. Summarizer reads `context.spanish_text` and stores summary
|
|
73
|
+
6. Summarizer emits `CHAIN_COMPLETE`
|
|
74
|
+
|
|
75
|
+
This pattern is incredibly useful for:
|
|
76
|
+
- Multi-step processing pipelines
|
|
77
|
+
- Translation → Analysis workflows
|
|
78
|
+
- Extract → Transform → Load patterns
|
|
79
|
+
|
|
80
|
+
## LLM Signal Selection (Resolution Step)
|
|
81
|
+
|
|
82
|
+
When an LLM node has multiple signals with **conditions** (plain text, not Jinja), the LLM itself decides which signal to emit.
|
|
83
|
+
|
|
84
|
+
### The Workflow
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
example_workflow:
|
|
88
|
+
SentimentAnalyzer:
|
|
89
|
+
node_type: llm
|
|
90
|
+
event_triggers: [START]
|
|
91
|
+
prompt: "Analyze the sentiment of: {{ context.user_message }}"
|
|
92
|
+
output_field: analysis
|
|
93
|
+
event_emissions:
|
|
94
|
+
- signal_name: POSITIVE
|
|
95
|
+
condition: "The message expresses positive sentiment"
|
|
96
|
+
- signal_name: NEGATIVE
|
|
97
|
+
condition: "The message expresses negative sentiment"
|
|
98
|
+
- signal_name: NEUTRAL
|
|
99
|
+
condition: "The message is neutral or factual"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### How It Works
|
|
103
|
+
|
|
104
|
+
1. The LLM analyzes the sentiment
|
|
105
|
+
2. SOE sees multiple signals with plain-text conditions (no `{{ }}`)
|
|
106
|
+
3. SOE asks the LLM: "Based on your analysis, which signal should be emitted?" using the conditions as descriptions
|
|
107
|
+
4. The LLM returns the appropriate signal (POSITIVE, NEGATIVE, or NEUTRAL)
|
|
108
|
+
|
|
109
|
+
This is called the **resolution step** - it lets the LLM make routing decisions based on its understanding.
|
|
110
|
+
|
|
111
|
+
### Signal Emission Rules
|
|
112
|
+
|
|
113
|
+
The `condition` field controls how signals are emitted:
|
|
114
|
+
|
|
115
|
+
| Condition | Behavior |
|
|
116
|
+
|-----------|----------|
|
|
117
|
+
| **No condition** | Signal is always emitted |
|
|
118
|
+
| **Plain text** | Semantic—LLM selects which signal to emit based on the description |
|
|
119
|
+
| **Jinja template (`{{ }}`)** | Programmatic—evaluated against `context`, emits if truthy |
|
|
120
|
+
|
|
121
|
+
**How SOE decides:**
|
|
122
|
+
|
|
123
|
+
1. **No conditions**: All signals emit unconditionally after node execution
|
|
124
|
+
2. **Has conditions**: SOE checks if they contain `{{ }}`:
|
|
125
|
+
- **Yes (Jinja)**: Evaluate expression against `context`—emit if result is truthy
|
|
126
|
+
- **No (plain text)**: Ask LLM to choose which signal best matches its output
|
|
127
|
+
|
|
128
|
+
## Testing LLM Nodes
|
|
129
|
+
|
|
130
|
+
In tests, we inject a stub function instead of calling a real LLM:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
def stub_llm(prompt: str, config: dict) -> str:
|
|
134
|
+
return "This is a predictable response."
|
|
135
|
+
|
|
136
|
+
nodes["llm"] = create_llm_node_caller(backends, stub_llm, broadcast_signals_caller)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The stub knows the *contract* (prompt → response), not the implementation. This keeps tests fast and deterministic.
|
|
140
|
+
|
|
141
|
+
## Next Steps
|
|
142
|
+
|
|
143
|
+
Now that you understand LLM nodes, let's see how [Router Nodes](guide_03_router.md) connect your workflows together →
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
|
|
2
|
+
# SOE Guide: Chapter 3 - Router Nodes
|
|
3
|
+
|
|
4
|
+
## Introduction to Router Nodes
|
|
5
|
+
|
|
6
|
+
The **Router** is a pure routing node—it doesn't execute code or call LLMs. It simply reads the context, evaluates conditions, and emits signals to direct the workflow.
|
|
7
|
+
|
|
8
|
+
Think of a Router as a **traffic controller**: based on the current state (context), it decides which signal to emit, directing the workflow to the next step. Routers are the glue that connects your Tool and LLM nodes together.
|
|
9
|
+
|
|
10
|
+
### When to Use Router Nodes
|
|
11
|
+
|
|
12
|
+
- **Entry Points**: Start a workflow and fan out to multiple paths
|
|
13
|
+
- **Conditional Branching**: Route based on context values
|
|
14
|
+
- **Signal Transformation**: Convert one signal to another
|
|
15
|
+
- **Checkpoints**: Create explicit decision points in your workflow
|
|
16
|
+
|
|
17
|
+
### Why Router Comes Third
|
|
18
|
+
|
|
19
|
+
We covered Tool and LLM nodes first because they **do work**. Routers don't—they route. Now that you understand the nodes that execute, you can see how Routers connect them into powerful workflows.
|
|
20
|
+
|
|
21
|
+
## Your First Router: Input Validation
|
|
22
|
+
|
|
23
|
+
Let's validate that user input exists before processing it.
|
|
24
|
+
|
|
25
|
+
### The Workflow
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
example_workflow:
|
|
29
|
+
InputValidator:
|
|
30
|
+
node_type: router
|
|
31
|
+
event_triggers: [START]
|
|
32
|
+
event_emissions:
|
|
33
|
+
- signal_name: VALID_INPUT
|
|
34
|
+
condition: "{{ context.user_input is defined and context.user_input != '' }}"
|
|
35
|
+
- signal_name: INVALID_INPUT
|
|
36
|
+
condition: "{{ context.user_input is not defined or context.user_input == '' }}"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### How It Works
|
|
40
|
+
|
|
41
|
+
1. **Event Trigger**: The `START` signal triggers the `InputValidator` node.
|
|
42
|
+
2. **Condition Evaluation**: The router checks two conditions using Jinja2 templates:
|
|
43
|
+
- If `context.user_input` is defined and not empty → emit `VALID_INPUT`
|
|
44
|
+
- If `context.user_input` is missing or empty → emit `INVALID_INPUT`
|
|
45
|
+
3. **Signal Emission**: One or more signals are emitted based on which conditions are true.
|
|
46
|
+
|
|
47
|
+
### Key Concepts
|
|
48
|
+
|
|
49
|
+
- **Routers** evaluate Jinja2 conditions and emit signals
|
|
50
|
+
- **Conditions** can check if variables exist, compare values, etc.
|
|
51
|
+
- **Signals** are the "nervous system" of SOE—they trigger the next steps
|
|
52
|
+
|
|
53
|
+
## Unconditional Signals
|
|
54
|
+
|
|
55
|
+
Not every router needs a condition. Sometimes you just want to forward a signal unconditionally.
|
|
56
|
+
|
|
57
|
+
### The Workflow
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
example_workflow:
|
|
61
|
+
Forwarder:
|
|
62
|
+
node_type: router
|
|
63
|
+
event_triggers: [START]
|
|
64
|
+
event_emissions:
|
|
65
|
+
- signal_name: CONTINUE
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This router simply receives `START` and immediately emits `CONTINUE`. It's useful when you want to:
|
|
69
|
+
- **Rename signals**: Transform external signals to internal naming
|
|
70
|
+
- **Create checkpoints**: Make workflow structure explicit
|
|
71
|
+
- **Fan-out**: Emit multiple signals to trigger parallel processing
|
|
72
|
+
|
|
73
|
+
## Decision Trees: Routers Calling Routers
|
|
74
|
+
|
|
75
|
+
The real power of routers emerges when you chain them together. Since each router emits signals that trigger other nodes, you can build **decision trees** (or more generally, **directed graphs**) where the workflow branches based on context.
|
|
76
|
+
|
|
77
|
+
### The Pattern
|
|
78
|
+
|
|
79
|
+
Think of it as a flowchart:
|
|
80
|
+
- **Level 1 Router**: Classifies the initial request
|
|
81
|
+
- **Level 2+ Routers**: Handle sub-cases, triggered by signals from Level 1
|
|
82
|
+
- **Terminal Nodes**: Complete specific branches of the tree
|
|
83
|
+
|
|
84
|
+
### The Workflow
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
example_workflow:
|
|
88
|
+
# Level 1: Is the user premium or free?
|
|
89
|
+
UserTierCheck:
|
|
90
|
+
node_type: router
|
|
91
|
+
event_triggers: [START]
|
|
92
|
+
event_emissions:
|
|
93
|
+
- signal_name: PREMIUM_PATH
|
|
94
|
+
condition: "{{ context.user_tier == 'premium' }}"
|
|
95
|
+
- signal_name: FREE_PATH
|
|
96
|
+
condition: "{{ context.user_tier == 'free' }}"
|
|
97
|
+
|
|
98
|
+
# Level 2a: Premium users get feature checks
|
|
99
|
+
PremiumFeatureRouter:
|
|
100
|
+
node_type: router
|
|
101
|
+
event_triggers: [PREMIUM_PATH]
|
|
102
|
+
event_emissions:
|
|
103
|
+
- signal_name: ENABLE_ADVANCED
|
|
104
|
+
condition: "{{ context.feature_level == 'advanced' }}"
|
|
105
|
+
- signal_name: ENABLE_BASIC
|
|
106
|
+
condition: "{{ context.feature_level != 'advanced' }}"
|
|
107
|
+
|
|
108
|
+
# Level 2b: Free users get upgrade prompts
|
|
109
|
+
FreeUserRouter:
|
|
110
|
+
node_type: router
|
|
111
|
+
event_triggers: [FREE_PATH]
|
|
112
|
+
event_emissions:
|
|
113
|
+
- signal_name: SHOW_UPGRADE
|
|
114
|
+
condition: "{{ context.show_upsell }}"
|
|
115
|
+
- signal_name: CONTINUE_FREE
|
|
116
|
+
condition: "{{ not context.show_upsell }}"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### How It Works
|
|
120
|
+
|
|
121
|
+
1. `UserTierCheck` receives `START` and evaluates `context.user_tier`
|
|
122
|
+
2. Depending on the tier, it emits either `PREMIUM_PATH` or `FREE_PATH`
|
|
123
|
+
3. The corresponding router (`PremiumFeatureRouter` or `FreeUserRouter`) activates
|
|
124
|
+
4. That router evaluates its own conditions and emits the final signal
|
|
125
|
+
|
|
126
|
+
### Why This Matters
|
|
127
|
+
|
|
128
|
+
- **Modularity**: Each router handles one decision. Easy to test, easy to modify.
|
|
129
|
+
- **Composition**: Add new branches by adding new routers—no need to modify existing ones.
|
|
130
|
+
- **Visibility**: The workflow YAML *is* the flowchart. No hidden logic in code.
|
|
131
|
+
|
|
132
|
+
## The Three Core Node Types
|
|
133
|
+
|
|
134
|
+
You now know the three core node types in SOE:
|
|
135
|
+
|
|
136
|
+
| Node | Purpose | Does Work? |
|
|
137
|
+
|------|---------|-----------|
|
|
138
|
+
| **Tool** | Execute Python functions | ✅ Yes |
|
|
139
|
+
| **LLM** | Call language models | ✅ Yes |
|
|
140
|
+
| **Router** | Route signals based on context | ❌ No (pure routing) |
|
|
141
|
+
|
|
142
|
+
With just these three nodes, you can build remarkably sophisticated workflows—including custom agent patterns, chain-of-thought reasoning, and more. The next chapter shows you how.
|
|
143
|
+
|
|
144
|
+
## Next Steps
|
|
145
|
+
|
|
146
|
+
Now that you understand the three core node types, let's combine them into powerful patterns with [Building Custom Workflows](guide_04_patterns.md) →
|