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
soe/validation/config.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config validation - validates config structure before orchestration starts.
|
|
3
|
+
|
|
4
|
+
Supports two config formats:
|
|
5
|
+
1. Single Workflow format: Dict of workflow definitions directly
|
|
6
|
+
2. Combined config format: Dict with 'workflows', 'context_schema', 'identities' keys
|
|
7
|
+
|
|
8
|
+
Runs once at orchestration start, before any execution.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, Any
|
|
12
|
+
|
|
13
|
+
from ..types import WorkflowValidationError
|
|
14
|
+
from ..nodes.router.validation import validate_node_config as validate_router
|
|
15
|
+
from ..nodes.agent.validation import validate_node_config as validate_agent
|
|
16
|
+
from ..nodes.llm.validation import validate_node_config as validate_llm
|
|
17
|
+
from ..nodes.child.validation import validate_node_config as validate_child
|
|
18
|
+
from ..nodes.tool.validation import validate_node_config as validate_tool
|
|
19
|
+
from ..lib.yaml_parser import parse_yaml
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
NODE_VALIDATORS = {
|
|
23
|
+
"router": validate_router,
|
|
24
|
+
"agent": validate_agent,
|
|
25
|
+
"llm": validate_llm,
|
|
26
|
+
"child": validate_child,
|
|
27
|
+
"tool": validate_tool,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_workflow_section(workflow_name: str, workflow: Dict[str, Any]) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Validate all nodes in a workflow section.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
workflow_name: Name of the workflow (for error messages)
|
|
37
|
+
workflow: Workflow definition dict
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
WorkflowValidationError: If any node configuration is invalid
|
|
41
|
+
"""
|
|
42
|
+
if not workflow:
|
|
43
|
+
raise WorkflowValidationError(
|
|
44
|
+
f"Workflow '{workflow_name}' is empty - at least one node is required"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
for node_name, node_config in workflow.items():
|
|
48
|
+
if node_name.startswith("__"):
|
|
49
|
+
raise WorkflowValidationError(
|
|
50
|
+
f"Workflow '{workflow_name}': node name '{node_name}' is reserved - "
|
|
51
|
+
f"names starting with '__' are reserved for internal use"
|
|
52
|
+
)
|
|
53
|
+
node_type = node_config.get("node_type")
|
|
54
|
+
|
|
55
|
+
if not node_type:
|
|
56
|
+
raise WorkflowValidationError(
|
|
57
|
+
f"Workflow '{workflow_name}', node '{node_name}': "
|
|
58
|
+
f"'node_type' is required - specify the type (router, agent, llm, tool, child)"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if node_type.startswith("_"):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
validator = NODE_VALIDATORS.get(node_type)
|
|
65
|
+
if not validator:
|
|
66
|
+
valid_types = ", ".join(NODE_VALIDATORS.keys())
|
|
67
|
+
raise WorkflowValidationError(
|
|
68
|
+
f"Workflow '{workflow_name}', node '{node_name}': "
|
|
69
|
+
f"unknown node_type '{node_type}'. Valid types are: {valid_types}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
validator(node_config)
|
|
74
|
+
except WorkflowValidationError as e:
|
|
75
|
+
raise WorkflowValidationError(
|
|
76
|
+
f"Workflow '{workflow_name}', node '{node_name}': {e}"
|
|
77
|
+
) from e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _validate_context_schema_section(context_schema: Dict[str, Any]) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Validate context_schema section of config.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
context_schema: Context schema definitions
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
WorkflowValidationError: If schema format is invalid
|
|
89
|
+
"""
|
|
90
|
+
if not isinstance(context_schema, dict):
|
|
91
|
+
raise WorkflowValidationError(
|
|
92
|
+
"'context_schema' section must be an object mapping field names to schemas"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for field_name, field_schema in context_schema.items():
|
|
96
|
+
if not isinstance(field_schema, (dict, str)):
|
|
97
|
+
raise WorkflowValidationError(
|
|
98
|
+
f"context_schema.{field_name}: schema must be an object or type string"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _validate_identities_section(identities: Dict[str, str]) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Validate identities section of config.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
identities: Identity definitions (name -> system prompt)
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
WorkflowValidationError: If identity format is invalid
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(identities, dict):
|
|
113
|
+
raise WorkflowValidationError(
|
|
114
|
+
"'identities' section must be an object mapping identity names to prompts"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for identity_name, identity_prompt in identities.items():
|
|
118
|
+
if not isinstance(identity_prompt, str):
|
|
119
|
+
raise WorkflowValidationError(
|
|
120
|
+
f"identities.{identity_name}: identity prompt must be a string, "
|
|
121
|
+
f"got {type(identity_prompt).__name__}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_config(config) -> Dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Parse and validate config.
|
|
128
|
+
|
|
129
|
+
Supports two formats:
|
|
130
|
+
1. Legacy format: Dict of workflow definitions directly
|
|
131
|
+
2. Combined config format: Dict with 'workflows', 'context_schema', 'identities' keys
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
config: YAML string or dict (workflows only or combined config)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Parsed config dict (either workflows directly or combined structure)
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
WorkflowValidationError: If any configuration is invalid
|
|
141
|
+
"""
|
|
142
|
+
parsed = parse_yaml(config)
|
|
143
|
+
|
|
144
|
+
if "workflows" in parsed:
|
|
145
|
+
workflows = parsed["workflows"]
|
|
146
|
+
if not isinstance(workflows, dict):
|
|
147
|
+
raise WorkflowValidationError(
|
|
148
|
+
"'workflows' section must be an object containing workflow definitions"
|
|
149
|
+
)
|
|
150
|
+
for workflow_name, workflow in workflows.items():
|
|
151
|
+
if not isinstance(workflow, dict):
|
|
152
|
+
raise WorkflowValidationError(
|
|
153
|
+
f"Workflow '{workflow_name}' must be an object containing node definitions"
|
|
154
|
+
)
|
|
155
|
+
_validate_workflow_section(workflow_name, workflow)
|
|
156
|
+
|
|
157
|
+
context_schema = parsed.get("context_schema")
|
|
158
|
+
if context_schema is not None:
|
|
159
|
+
_validate_context_schema_section(context_schema)
|
|
160
|
+
|
|
161
|
+
identities = parsed.get("identities")
|
|
162
|
+
if identities is not None:
|
|
163
|
+
_validate_identities_section(identities)
|
|
164
|
+
else:
|
|
165
|
+
for workflow_name, workflow in parsed.items():
|
|
166
|
+
if not isinstance(workflow, dict):
|
|
167
|
+
raise WorkflowValidationError(
|
|
168
|
+
f"Workflow '{workflow_name}' must be an object containing node definitions"
|
|
169
|
+
)
|
|
170
|
+
_validate_workflow_section(workflow_name, workflow)
|
|
171
|
+
|
|
172
|
+
return parsed
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
validate_workflow = _validate_workflow_section
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def validate_orchestrate_params(
|
|
179
|
+
initial_workflow_name: str,
|
|
180
|
+
initial_signals: list,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Validate orchestrate() parameters before execution starts."""
|
|
183
|
+
if not isinstance(initial_signals, list):
|
|
184
|
+
raise WorkflowValidationError(
|
|
185
|
+
f"'initial_signals' must be a list, got {type(initial_signals).__name__}. "
|
|
186
|
+
f"Example: initial_signals=['START']"
|
|
187
|
+
)
|
|
188
|
+
if not initial_signals:
|
|
189
|
+
raise WorkflowValidationError(
|
|
190
|
+
"'initial_signals' cannot be empty - at least one signal is required to start execution"
|
|
191
|
+
)
|
|
192
|
+
if not isinstance(initial_workflow_name, str) or not initial_workflow_name:
|
|
193
|
+
raise WorkflowValidationError(
|
|
194
|
+
"'initial_workflow_name' must be a non-empty string"
|
|
195
|
+
)
|
soe/validation/jinja.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jinja template validation utilities.
|
|
3
|
+
|
|
4
|
+
Used during config validation to catch Jinja errors early.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from jinja2 import Environment, BaseLoader, TemplateSyntaxError
|
|
8
|
+
from ..types import WorkflowValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _dummy_accumulated_filter(value):
|
|
12
|
+
"""Dummy accumulated filter for validation - just returns value as list."""
|
|
13
|
+
return [value] if value is not None else []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_jinja_syntax(template: str, context_description: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Validate Jinja template syntax at config time.
|
|
19
|
+
|
|
20
|
+
Called during config validation to catch Jinja errors early.
|
|
21
|
+
|
|
22
|
+
Catches:
|
|
23
|
+
- Unclosed braces {{ without }}
|
|
24
|
+
- Unknown filters like | capitalize_all
|
|
25
|
+
- Basic syntax errors
|
|
26
|
+
|
|
27
|
+
Does NOT catch:
|
|
28
|
+
- Runtime errors like division by zero (depends on context values)
|
|
29
|
+
- Undefined variables (depends on context at runtime)
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
template: The Jinja template string to validate
|
|
33
|
+
context_description: Description for error messages (e.g., "condition for signal 'DONE'")
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
WorkflowValidationError: If template has syntax or filter errors
|
|
37
|
+
"""
|
|
38
|
+
if not template or ("{{" not in template and "{%" not in template):
|
|
39
|
+
return
|
|
40
|
+
try:
|
|
41
|
+
env = Environment(loader=BaseLoader())
|
|
42
|
+
env.filters["accumulated"] = _dummy_accumulated_filter
|
|
43
|
+
env.parse(template)
|
|
44
|
+
env.from_string(template)
|
|
45
|
+
except TemplateSyntaxError as e:
|
|
46
|
+
raise WorkflowValidationError(
|
|
47
|
+
f"{context_description}: Jinja syntax error - {e.message}"
|
|
48
|
+
)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
error_msg = str(e)
|
|
51
|
+
if "filter" in error_msg.lower():
|
|
52
|
+
raise WorkflowValidationError(
|
|
53
|
+
f"{context_description}: {error_msg}"
|
|
54
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Operational validation - validates runtime state before node execution.
|
|
3
|
+
|
|
4
|
+
This validates that the context and backends are in a valid state
|
|
5
|
+
for node execution. Runs before each node executes so that operational
|
|
6
|
+
code can trust the structure and avoid defensive programming.
|
|
7
|
+
|
|
8
|
+
Fail-fast: If validation fails, raise immediately with clear message.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, Any
|
|
12
|
+
|
|
13
|
+
from ..types import Backends, SoeError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OperationalValidationError(SoeError):
|
|
17
|
+
"""Raised when operational context is invalid."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_operational(
|
|
22
|
+
execution_id: str,
|
|
23
|
+
backends: Backends,
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Validate that operational context exists and has required structure.
|
|
27
|
+
|
|
28
|
+
Call this before any node execution to ensure __operational__ is valid.
|
|
29
|
+
Returns the context so caller doesn't need to fetch it again.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
execution_id: The execution ID
|
|
33
|
+
backends: Backend services
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The validated context dict
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
OperationalValidationError: If context or __operational__ is invalid
|
|
40
|
+
"""
|
|
41
|
+
context = backends.context.get_context(execution_id)
|
|
42
|
+
|
|
43
|
+
if not context:
|
|
44
|
+
raise OperationalValidationError(
|
|
45
|
+
f"No context found for execution_id '{execution_id}'. "
|
|
46
|
+
f"Context must be initialized before node execution."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
operational = context.get("__operational__")
|
|
50
|
+
|
|
51
|
+
if operational is None:
|
|
52
|
+
raise OperationalValidationError(
|
|
53
|
+
f"Missing '__operational__' in context for execution_id '{execution_id}'. "
|
|
54
|
+
f"Call initialize_operational_context() before node execution."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
required_fields = ["signals", "nodes", "llm_calls", "tool_calls", "errors", "main_execution_id"]
|
|
58
|
+
missing = [f for f in required_fields if f not in operational]
|
|
59
|
+
|
|
60
|
+
if missing:
|
|
61
|
+
raise OperationalValidationError(
|
|
62
|
+
f"Invalid '__operational__' structure for execution_id '{execution_id}'. "
|
|
63
|
+
f"Missing fields: {missing}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not isinstance(operational["signals"], list):
|
|
67
|
+
raise OperationalValidationError(
|
|
68
|
+
f"Invalid '__operational__.signals' - must be a list, got {type(operational['signals']).__name__}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if not isinstance(operational["nodes"], dict):
|
|
72
|
+
raise OperationalValidationError(
|
|
73
|
+
f"Invalid '__operational__.nodes' - must be a dict, got {type(operational['nodes']).__name__}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not isinstance(operational["llm_calls"], int):
|
|
77
|
+
raise OperationalValidationError(
|
|
78
|
+
f"Invalid '__operational__.llm_calls' - must be an int, got {type(operational['llm_calls']).__name__}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if not isinstance(operational["tool_calls"], int):
|
|
82
|
+
raise OperationalValidationError(
|
|
83
|
+
f"Invalid '__operational__.tool_calls' - must be an int, got {type(operational['tool_calls']).__name__}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if not isinstance(operational["errors"], int):
|
|
87
|
+
raise OperationalValidationError(
|
|
88
|
+
f"Invalid '__operational__.errors' - must be an int, got {type(operational['errors']).__name__}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return context
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def validate_backends(backends: Backends) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Validate that backends has required attributes.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
backends: Backend services
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
OperationalValidationError: If backends is invalid
|
|
103
|
+
"""
|
|
104
|
+
required = ["context", "workflow"]
|
|
105
|
+
|
|
106
|
+
for attr in required:
|
|
107
|
+
if not hasattr(backends, attr) or getattr(backends, attr) is None:
|
|
108
|
+
raise OperationalValidationError(
|
|
109
|
+
f"Invalid backends: missing required attribute '{attr}'"
|
|
110
|
+
)
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: soe-ai
|
|
3
|
+
Version: 0.2.0b1
|
|
4
|
+
Summary: Signal-driven Orchestration Engine - Agent orchestration with event-driven workflow engine
|
|
5
|
+
Author-email: Pedro Garcia <pgarcia14180@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pgarcia14180/soe
|
|
8
|
+
Project-URL: Documentation, https://github.com/pgarcia14180/soe/tree/master/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/pgarcia14180/soe
|
|
10
|
+
Project-URL: Issues, https://github.com/pgarcia14180/soe/issues
|
|
11
|
+
Keywords: orchestration,agent,workflow,automation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: jinja2>=2.11.3
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
32
|
+
Provides-Extra: integration
|
|
33
|
+
Requires-Dist: openai>=1.0.0; extra == "integration"
|
|
34
|
+
Requires-Dist: requests>=2.28.0; extra == "integration"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# SOE — Signal-driven Orchestration Engine
|
|
38
|
+
|
|
39
|
+
**A protocol for orchestrating AI workflows through signals.**
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## What SOE Is
|
|
44
|
+
|
|
45
|
+
SOE is an orchestration engine where nodes communicate through **signals** rather than direct function calls. You define workflows in YAML, and the engine handles execution.
|
|
46
|
+
|
|
47
|
+
| Approach | How It Works | Trade-off |
|
|
48
|
+
|----------|--------------|-----------|
|
|
49
|
+
| Chain-based | `Step A → B → C → D` | Simple but rigid |
|
|
50
|
+
| SOE Signal-based | `[SIGNAL] → all listeners respond` | Flexible, requires understanding signals |
|
|
51
|
+
|
|
52
|
+
**The C++ analogy**: Like C++ gives you control over memory and execution (compared to higher-level languages), SOE gives you control over orchestration primitives. You decide how state is stored, how LLMs are called, and how signals are broadcast. This requires more setup but means no vendor lock-in and full observability.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What SOE Does
|
|
57
|
+
|
|
58
|
+
SOE orchestrates workflows through **signals**. Nodes don't call each other—they emit signals that other nodes listen for.
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
example_workflow:
|
|
62
|
+
ValidateInput:
|
|
63
|
+
node_type: router
|
|
64
|
+
event_triggers: [START]
|
|
65
|
+
event_emissions:
|
|
66
|
+
- signal_name: VALID
|
|
67
|
+
condition: "{{ context.data is defined }}"
|
|
68
|
+
- signal_name: INVALID
|
|
69
|
+
|
|
70
|
+
ProcessData:
|
|
71
|
+
node_type: llm
|
|
72
|
+
event_triggers: [VALID]
|
|
73
|
+
prompt: "Process this: {{ context.data }}"
|
|
74
|
+
output_field: result
|
|
75
|
+
event_emissions:
|
|
76
|
+
- signal_name: DONE
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**That's the entire workflow definition.** No SDK, no decorators, no base classes.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Why SOE
|
|
84
|
+
|
|
85
|
+
### 1. Infrastructure-Agnostic
|
|
86
|
+
SOE defines **protocols**, not implementations. Swap PostgreSQL for DynamoDB. Replace OpenAI with a local LLM. Deploy to Lambda, Kubernetes, or a single Python script. Your workflow YAML stays the same.
|
|
87
|
+
|
|
88
|
+
### 2. Context-Driven with Jinja
|
|
89
|
+
All workflow state flows through **context**—a shared dictionary accessible via Jinja2 templates. This means:
|
|
90
|
+
- Conditions like `{{ context.user_validated }}` are readable and debuggable
|
|
91
|
+
- LLM prompts can interpolate any context field
|
|
92
|
+
- No hidden state—everything is inspectable
|
|
93
|
+
|
|
94
|
+
### 3. Purely Deterministic or Hybrid Agentic
|
|
95
|
+
SOE is a complete orchestration solution. You can use it as a purely deterministic engine for standard business logic, or mix in LLM-driven "Agentic" behavior.
|
|
96
|
+
- **Deterministic**: Use `router` and `tool` nodes for 100% predictable workflows.
|
|
97
|
+
- **Agentic**: Add `llm` and `agent` nodes for creative, reasoning-based tasks.
|
|
98
|
+
You get the safety of code with the flexibility of AI in a single, unified system.
|
|
99
|
+
|
|
100
|
+
### 4. Portable
|
|
101
|
+
Workflows are YAML. Run them locally, in CI, in production. Extract them, version them, share them.
|
|
102
|
+
|
|
103
|
+
### 5. Self-Evolving
|
|
104
|
+
Workflows can modify themselves at runtime. Built-in tools like `inject_workflow`, `inject_node_configuration`, and `add_signal` allow agents to:
|
|
105
|
+
- Create new workflows dynamically
|
|
106
|
+
- Add or modify nodes in existing workflows
|
|
107
|
+
- Update signal routing on the fly
|
|
108
|
+
|
|
109
|
+
This enables **meta-programming**: an AI system that can extend its own capabilities without human intervention.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## What SOE Unlocks
|
|
114
|
+
|
|
115
|
+
SOE is a **Protocol for Intelligence** that unlocks new forms of intelligent behavior:
|
|
116
|
+
|
|
117
|
+
### Self-Evolving Intelligence
|
|
118
|
+
AI systems that can rewrite and improve themselves at runtime - the ultimate evolution of software.
|
|
119
|
+
|
|
120
|
+
### Swarm Intelligence
|
|
121
|
+
Efficient collective decision-making among multiple agents through signal-based consensus.
|
|
122
|
+
|
|
123
|
+
### Hybrid Intelligence
|
|
124
|
+
Seamless combination of deterministic logic and AI creativity with programmatic safety rails.
|
|
125
|
+
|
|
126
|
+
### Fractal Intelligence
|
|
127
|
+
Hierarchical agent organizations that scale complexity while remaining manageable.
|
|
128
|
+
|
|
129
|
+
### Infrastructure Intelligence
|
|
130
|
+
AI orchestration that works everywhere - from edge devices to cloud platforms.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Installation
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# With uv (recommended)
|
|
138
|
+
uv add soe-ai
|
|
139
|
+
|
|
140
|
+
# With pip
|
|
141
|
+
pip install soe-ai
|
|
142
|
+
|
|
143
|
+
# From source
|
|
144
|
+
git clone https://github.com/pgarcia14180/soe.git
|
|
145
|
+
cd soe && uv sync
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Quick Start
|
|
151
|
+
|
|
152
|
+
### 1. Provide Your LLM
|
|
153
|
+
|
|
154
|
+
SOE is LLM-agnostic. You must provide a `call_llm` function that matches this signature:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
def call_llm(
|
|
158
|
+
prompt: str,
|
|
159
|
+
config: dict,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""
|
|
162
|
+
Called by SOE when a node needs LLM processing.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
prompt: The rendered prompt string (includes instructions, context, and schemas)
|
|
166
|
+
config: The full node configuration from YAML (useful for model parameters)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The raw text response from the LLM.
|
|
170
|
+
"""
|
|
171
|
+
# Example with OpenAI:
|
|
172
|
+
from openai import OpenAI
|
|
173
|
+
client = OpenAI()
|
|
174
|
+
response = client.chat.completions.create(
|
|
175
|
+
model=config.get("model", "gpt-4o"),
|
|
176
|
+
messages=[{"role": "user", "content": prompt}],
|
|
177
|
+
)
|
|
178
|
+
return response.choices[0].message.content
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 2. Run a Workflow
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from soe import orchestrate, create_all_nodes
|
|
185
|
+
from soe.local_backends import create_local_backends
|
|
186
|
+
|
|
187
|
+
# Your workflow (can also be loaded from file or database)
|
|
188
|
+
workflow = """
|
|
189
|
+
example_workflow:
|
|
190
|
+
Start:
|
|
191
|
+
node_type: router
|
|
192
|
+
event_triggers: [START]
|
|
193
|
+
event_emissions:
|
|
194
|
+
- signal_name: DONE
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
# Create backends (storage for context, workflows, etc.)
|
|
198
|
+
backends = create_local_backends("./data")
|
|
199
|
+
|
|
200
|
+
# Create all node handlers (pass your call_llm function)
|
|
201
|
+
nodes, broadcast = create_all_nodes(backends, call_llm=call_llm)
|
|
202
|
+
|
|
203
|
+
# Run the workflow
|
|
204
|
+
execution_id = orchestrate(
|
|
205
|
+
config=workflow,
|
|
206
|
+
initial_workflow_name="example_workflow",
|
|
207
|
+
initial_signals=["START"],
|
|
208
|
+
initial_context={"user": "alice"},
|
|
209
|
+
backends=backends,
|
|
210
|
+
broadcast_signals_caller=broadcast,
|
|
211
|
+
)
|
|
212
|
+
# When orchestrate() returns, the workflow is complete
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**For product managers and less technical users**: The Quick Start above is all you need to run a workflow. The `config` parameter accepts YAML defining your workflow structure. The `initial_context` is where you pass input data (like user IDs, requests, etc.).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Documentation
|
|
220
|
+
|
|
221
|
+
| Audience | Start Here |
|
|
222
|
+
|----------|------------|
|
|
223
|
+
| **Builders** (workflow authors) | [Documentation](soe/docs/index.md) — Step-by-step chapters |
|
|
224
|
+
| **Engineers** (infrastructure) | [Infrastructure Guide](soe/docs/guide_10_infrastructure.md) — Backend protocols |
|
|
225
|
+
| **Researchers** (advanced patterns) | [Advanced Patterns](docs/advanced_patterns/) — Swarm, hybrid, self-evolving |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Node Types
|
|
230
|
+
|
|
231
|
+
| Node | Purpose |
|
|
232
|
+
|------|---------|
|
|
233
|
+
| `router` | Conditional signal emission (no LLM) |
|
|
234
|
+
| `llm` | Single LLM call with output |
|
|
235
|
+
| `agent` | Multi-turn LLM with tool access |
|
|
236
|
+
| `tool` | Execute Python functions |
|
|
237
|
+
| `child` | Spawn sub-workflows |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Backend Protocols
|
|
242
|
+
|
|
243
|
+
Implement these to plug SOE into your infrastructure:
|
|
244
|
+
|
|
245
|
+
| Protocol | Purpose |
|
|
246
|
+
|----------|---------|
|
|
247
|
+
| `ContextBackend` | Workflow state storage |
|
|
248
|
+
| `WorkflowBackend` | Workflow definitions |
|
|
249
|
+
| `ContextSchemaBackend` | Output validation (optional) |
|
|
250
|
+
| `IdentityBackend` | LLM system prompts (optional) |
|
|
251
|
+
| `ConversationHistoryBackend` | Agent memory (optional) |
|
|
252
|
+
| `TelemetryBackend` | Observability (optional) |
|
|
253
|
+
|
|
254
|
+
**Recommendation**: Use the same database for context, workflows, identities, and context_schema—just separate tables. The backend methods handle table creation.
|
|
255
|
+
|
|
256
|
+
See [Infrastructure Guide](docs/guide_10_infrastructure.md) for PostgreSQL, DynamoDB, and Lambda examples.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT
|