soe-ai 0.1.0__py3-none-any.whl → 0.1.2__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/broker.py +4 -5
- soe/builtin_tools/__init__.py +39 -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_workflows.py +63 -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_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/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 +118 -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 +1 -1
- soe/init.py +2 -2
- 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 +40 -28
- 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.1.0.dist-info → soe_ai-0.1.2.dist-info}/METADATA +72 -9
- soe_ai-0.1.2.dist-info/RECORD +137 -0
- {soe_ai-0.1.0.dist-info → soe_ai-0.1.2.dist-info}/WHEEL +1 -1
- soe/validation.py +0 -8
- soe_ai-0.1.0.dist-info/RECORD +0 -11
- {soe_ai-0.1.0.dist-info → soe_ai-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {soe_ai-0.1.0.dist-info → soe_ai-0.1.2.dist-info}/top_level.txt +0 -0
soe/broker.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from uuid import uuid4
|
|
2
|
-
from typing import Dict, List, Any, Union,
|
|
3
|
-
from .types import Backends, BroadcastSignalsCaller
|
|
4
|
-
from .local_backends import EventTypes
|
|
2
|
+
from typing import Dict, List, Any, Union, Optional
|
|
3
|
+
from .types import Backends, BroadcastSignalsCaller, NodeCaller, EventTypes, WorkflowValidationError
|
|
5
4
|
from .lib.register_event import register_event
|
|
6
5
|
from .lib.yaml_parser import parse_yaml
|
|
7
6
|
from .lib.operational import add_operational_state
|
|
@@ -131,7 +130,7 @@ def orchestrate(
|
|
|
131
130
|
def broadcast_signals(
|
|
132
131
|
id: str,
|
|
133
132
|
signals: List[str],
|
|
134
|
-
nodes: Dict[str,
|
|
133
|
+
nodes: Dict[str, NodeCaller],
|
|
135
134
|
backends: Backends,
|
|
136
135
|
) -> None:
|
|
137
136
|
"""Broadcast signals to matching nodes in the current workflow"""
|
|
@@ -139,7 +138,7 @@ def broadcast_signals(
|
|
|
139
138
|
|
|
140
139
|
register_event(backends, id, EventTypes.SIGNALS_BROADCAST, {"signals": signals})
|
|
141
140
|
|
|
142
|
-
workflows_registry = backends.workflow.
|
|
141
|
+
workflows_registry = backends.workflow.get_workflows_registry(id)
|
|
143
142
|
|
|
144
143
|
workflow_name = backends.workflow.get_current_workflow_name(id)
|
|
145
144
|
workflow = workflows_registry.get(workflow_name, {})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in tools registry
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .soe_inject_workflow import create_soe_inject_workflow_tool
|
|
6
|
+
from .soe_inject_node import create_soe_inject_node_tool
|
|
7
|
+
from .soe_get_workflows import create_soe_get_workflows_tool
|
|
8
|
+
from .soe_get_available_tools import create_soe_get_available_tools_tool
|
|
9
|
+
from .soe_explore_docs import create_soe_explore_docs_tool
|
|
10
|
+
from .soe_remove_workflow import create_soe_remove_workflow_tool
|
|
11
|
+
from .soe_remove_node import create_soe_remove_node_tool
|
|
12
|
+
from .soe_get_context import create_soe_get_context_tool
|
|
13
|
+
from .soe_update_context import create_soe_update_context_tool
|
|
14
|
+
from .soe_copy_context import create_soe_copy_context_tool
|
|
15
|
+
from .soe_list_contexts import create_soe_list_contexts_tool
|
|
16
|
+
from .soe_add_signal import create_soe_add_signal_tool
|
|
17
|
+
from .soe_call_tool import create_soe_call_tool_tool
|
|
18
|
+
|
|
19
|
+
# Registry of all available built-in tools
|
|
20
|
+
BUILTIN_TOOLS = {
|
|
21
|
+
"soe_inject_workflow": create_soe_inject_workflow_tool,
|
|
22
|
+
"soe_inject_node": create_soe_inject_node_tool,
|
|
23
|
+
"soe_get_workflows": create_soe_get_workflows_tool,
|
|
24
|
+
"soe_get_available_tools": create_soe_get_available_tools_tool,
|
|
25
|
+
"soe_explore_docs": create_soe_explore_docs_tool,
|
|
26
|
+
"soe_remove_workflow": create_soe_remove_workflow_tool,
|
|
27
|
+
"soe_remove_node": create_soe_remove_node_tool,
|
|
28
|
+
"soe_get_context": create_soe_get_context_tool,
|
|
29
|
+
"soe_update_context": create_soe_update_context_tool,
|
|
30
|
+
"soe_copy_context": create_soe_copy_context_tool,
|
|
31
|
+
"soe_list_contexts": create_soe_list_contexts_tool,
|
|
32
|
+
"soe_add_signal": create_soe_add_signal_tool,
|
|
33
|
+
"soe_call_tool": create_soe_call_tool_tool,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_builtin_tool_factory(tool_name: str):
|
|
38
|
+
"""Get factory function for built-in tool"""
|
|
39
|
+
return BUILTIN_TOOLS.get(tool_name)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Built-in tool to add a signal to a node's event emissions."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Callable
|
|
4
|
+
from ..types import EventTypes
|
|
5
|
+
from ..lib.register_event import register_event
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_soe_add_signal_tool(
|
|
9
|
+
execution_id: str,
|
|
10
|
+
backends,
|
|
11
|
+
tools_registry: dict = None,
|
|
12
|
+
) -> Callable:
|
|
13
|
+
"""
|
|
14
|
+
Factory function to create add_signal tool.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def add_signal(
|
|
18
|
+
workflow_name: str, node_name: str, signal_name: str, condition: str
|
|
19
|
+
) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Add a signal to a node's event_emissions list.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
workflow_name: Name of the workflow
|
|
25
|
+
node_name: Name of the node
|
|
26
|
+
signal_name: Name of the signal to add
|
|
27
|
+
condition: Jinja condition for the signal
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Success confirmation
|
|
31
|
+
"""
|
|
32
|
+
workflows_registry = backends.workflow.get_workflows_registry(execution_id)
|
|
33
|
+
|
|
34
|
+
if workflow_name not in workflows_registry:
|
|
35
|
+
raise ValueError(f"Workflow '{workflow_name}' not found")
|
|
36
|
+
|
|
37
|
+
workflow = workflows_registry[workflow_name]
|
|
38
|
+
if node_name not in workflow:
|
|
39
|
+
raise ValueError(f"Node '{node_name}' not found in workflow '{workflow_name}'")
|
|
40
|
+
|
|
41
|
+
node_config = workflow[node_name]
|
|
42
|
+
|
|
43
|
+
if "event_emissions" not in node_config:
|
|
44
|
+
node_config["event_emissions"] = []
|
|
45
|
+
|
|
46
|
+
# Check if signal already exists
|
|
47
|
+
for emission in node_config["event_emissions"]:
|
|
48
|
+
if emission.get("signal_name") == signal_name:
|
|
49
|
+
# Update existing
|
|
50
|
+
emission["condition"] = condition
|
|
51
|
+
backends.workflow.save_workflows_registry(execution_id, workflows_registry)
|
|
52
|
+
return {
|
|
53
|
+
"status": "updated",
|
|
54
|
+
"message": f"Updated signal '{signal_name}' in node '{node_name}'"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Add new signal
|
|
58
|
+
node_config["event_emissions"].append({
|
|
59
|
+
"signal_name": signal_name,
|
|
60
|
+
"condition": condition
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
backends.workflow.save_workflows_registry(execution_id, workflows_registry)
|
|
64
|
+
|
|
65
|
+
register_event(
|
|
66
|
+
backends,
|
|
67
|
+
execution_id,
|
|
68
|
+
EventTypes.NODE_EXECUTION,
|
|
69
|
+
{
|
|
70
|
+
"tool": "add_signal",
|
|
71
|
+
"workflow_name": workflow_name,
|
|
72
|
+
"node_name": node_name,
|
|
73
|
+
"signal": signal_name
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"status": "added",
|
|
79
|
+
"message": f"Added signal '{signal_name}' to node '{node_name}'"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return add_signal
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in dynamic tool invocation.
|
|
3
|
+
|
|
4
|
+
Allows LLMs to call any registered tool by name at runtime.
|
|
5
|
+
This enables meta-level tool orchestration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, Any, Callable
|
|
10
|
+
from ..types import EventTypes
|
|
11
|
+
from ..lib.register_event import register_event
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_soe_call_tool_tool(
|
|
15
|
+
execution_id: str,
|
|
16
|
+
backends,
|
|
17
|
+
tools_registry: dict,
|
|
18
|
+
) -> Callable:
|
|
19
|
+
"""
|
|
20
|
+
Factory function to create call_tool with access to tools registry.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
execution_id: Current execution ID
|
|
24
|
+
backends: Backend services
|
|
25
|
+
tools_registry: Registry of available tools {name: {"function": callable, ...}}
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Configured call_tool function
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def call_tool(tool_name: str, arguments: str = "{}") -> Dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
Dynamically invoke a registered tool by name.
|
|
34
|
+
|
|
35
|
+
This is a meta-tool that allows calling any other tool at runtime.
|
|
36
|
+
Useful for dynamic workflows where the tool to call is determined
|
|
37
|
+
by context or user input.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
tool_name: Name of the tool to invoke (must be registered)
|
|
41
|
+
arguments: JSON string of arguments to pass to the tool
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The result from the invoked tool, or an error dict
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
call_tool("get_secret", '{"key": "password"}')
|
|
48
|
+
call_tool("write_file", '{"path": "test.txt", "content": "hello"}')
|
|
49
|
+
"""
|
|
50
|
+
# Parse arguments
|
|
51
|
+
try:
|
|
52
|
+
args = json.loads(arguments) if arguments else {}
|
|
53
|
+
except json.JSONDecodeError as e:
|
|
54
|
+
return {
|
|
55
|
+
"error": f"Invalid JSON arguments: {e}",
|
|
56
|
+
"tool_name": tool_name,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Check if tool exists
|
|
60
|
+
if tool_name not in tools_registry:
|
|
61
|
+
available = list(tools_registry.keys())
|
|
62
|
+
return {
|
|
63
|
+
"error": f"Tool '{tool_name}' not found",
|
|
64
|
+
"available_tools": available[:20], # Limit to avoid huge responses
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Get tool function
|
|
68
|
+
tool_entry = tools_registry[tool_name]
|
|
69
|
+
if isinstance(tool_entry, dict):
|
|
70
|
+
tool_func = tool_entry.get("function")
|
|
71
|
+
elif callable(tool_entry):
|
|
72
|
+
tool_func = tool_entry
|
|
73
|
+
else:
|
|
74
|
+
return {"error": f"Invalid tool registry entry for '{tool_name}'"}
|
|
75
|
+
|
|
76
|
+
if not callable(tool_func):
|
|
77
|
+
return {"error": f"Tool '{tool_name}' is not callable"}
|
|
78
|
+
|
|
79
|
+
# Log the dynamic invocation
|
|
80
|
+
register_event(
|
|
81
|
+
backends,
|
|
82
|
+
execution_id,
|
|
83
|
+
EventTypes.TOOL_CALL,
|
|
84
|
+
{
|
|
85
|
+
"meta_tool": "call_tool",
|
|
86
|
+
"invoked_tool": tool_name,
|
|
87
|
+
"arguments": args,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Invoke the tool
|
|
92
|
+
try:
|
|
93
|
+
result = tool_func(**args)
|
|
94
|
+
return {
|
|
95
|
+
"success": True,
|
|
96
|
+
"tool_name": tool_name,
|
|
97
|
+
"result": result,
|
|
98
|
+
}
|
|
99
|
+
except TypeError as e:
|
|
100
|
+
# Argument mismatch
|
|
101
|
+
return {
|
|
102
|
+
"error": f"Argument error for '{tool_name}': {e}",
|
|
103
|
+
"tool_name": tool_name,
|
|
104
|
+
}
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return {
|
|
107
|
+
"error": f"Tool '{tool_name}' failed: {e}",
|
|
108
|
+
"tool_name": tool_name,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return call_tool
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in tool: copy_context
|
|
3
|
+
Allows agents to copy context fields between executions or within the same execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_soe_copy_context_tool(backends, execution_id: str, tools_registry=None):
|
|
10
|
+
"""
|
|
11
|
+
Factory that creates a copy_context tool bound to the current execution.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
backends: Backend instances (needs context backend)
|
|
15
|
+
execution_id: Current execution ID
|
|
16
|
+
tools_registry: Tool registry (unused, for interface compatibility)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Configured tool function
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def copy_context(
|
|
23
|
+
source_execution_id: Optional[str] = None,
|
|
24
|
+
fields: Optional[Dict[str, str]] = None,
|
|
25
|
+
all_fields: bool = False,
|
|
26
|
+
target_execution_id: Optional[str] = None,
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Copy context fields between executions or within the same execution.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
source_execution_id: Execution ID to copy from (default: current)
|
|
33
|
+
fields: Dict of {source_field: target_field} to copy
|
|
34
|
+
all_fields: If True, copy all non-operational fields
|
|
35
|
+
target_execution_id: Execution ID to copy to (default: current)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict with copy results
|
|
39
|
+
"""
|
|
40
|
+
source_id = source_execution_id or execution_id
|
|
41
|
+
target_id = target_execution_id or execution_id
|
|
42
|
+
|
|
43
|
+
# Get source context
|
|
44
|
+
source_context = backends.context.get_context(source_id)
|
|
45
|
+
|
|
46
|
+
# Filter out operational fields
|
|
47
|
+
source_filtered = {k: v for k, v in source_context.items() if not k.startswith("__")}
|
|
48
|
+
|
|
49
|
+
# Get target context
|
|
50
|
+
target_context = backends.context.get_context(target_id)
|
|
51
|
+
|
|
52
|
+
copied_fields = {}
|
|
53
|
+
|
|
54
|
+
if all_fields:
|
|
55
|
+
# Copy all non-operational fields
|
|
56
|
+
for field, value in source_filtered.items():
|
|
57
|
+
target_context[field] = value
|
|
58
|
+
copied_fields[field] = field
|
|
59
|
+
elif fields:
|
|
60
|
+
# Copy specific field mappings
|
|
61
|
+
for source_field, target_field in fields.items():
|
|
62
|
+
if source_field in source_filtered:
|
|
63
|
+
target_context[target_field] = source_filtered[source_field]
|
|
64
|
+
copied_fields[source_field] = target_field
|
|
65
|
+
else:
|
|
66
|
+
return {"error": f"Source field '{source_field}' not found in execution {source_id}"}
|
|
67
|
+
else:
|
|
68
|
+
return {"error": "Must specify either 'fields' mapping or 'all_fields=True'"}
|
|
69
|
+
|
|
70
|
+
# Save target context
|
|
71
|
+
backends.context.save_context(target_id, target_context)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"status": "copied",
|
|
75
|
+
"source_execution": source_id,
|
|
76
|
+
"target_execution": target_id,
|
|
77
|
+
"fields_copied": copied_fields,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return copy_context
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from typing import Literal, Optional, List, Callable
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from soe.docs_index import DOCS_INDEX
|
|
6
|
+
|
|
7
|
+
# Get the SOE package root directory (where soe/docs/ lives)
|
|
8
|
+
# This goes from soe/builtin_tools/soe_explore_docs.py → soe/builtin_tools → soe/soe → soe/
|
|
9
|
+
SOE_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_soe_explore_docs_tool(
|
|
13
|
+
execution_id: str = None,
|
|
14
|
+
backends = None,
|
|
15
|
+
tools_registry: dict = None,
|
|
16
|
+
) -> Callable:
|
|
17
|
+
"""
|
|
18
|
+
Factory for the soe_explore_docs tool.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
execution_id: ID to access workflow data via backends (unused by this tool)
|
|
22
|
+
backends: Backend services (unused by this tool)
|
|
23
|
+
tools_registry: Optional registry of available tools (unused by this tool)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Configured soe_explore_docs function
|
|
27
|
+
"""
|
|
28
|
+
return soe_explore_docs
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def soe_explore_docs(
|
|
32
|
+
path: str,
|
|
33
|
+
action: Literal["list", "read", "search", "tree", "get_tags"],
|
|
34
|
+
query: Optional[str] = None,
|
|
35
|
+
tag: Optional[str] = None
|
|
36
|
+
) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Explore SOE documentation using a file-system-like interface.
|
|
39
|
+
|
|
40
|
+
This tool lets you navigate and read the SOE documentation hierarchy.
|
|
41
|
+
Start with action='list' at path='/' to see available docs.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
path: Path to explore. Use '/' for root, 'docs/guide_01_basics.md' for a file,
|
|
45
|
+
or 'docs/guide_01_basics.md/Section Title' for a specific section.
|
|
46
|
+
action: What to do at the path:
|
|
47
|
+
- 'list': Show children (files in a dir, sections in a file)
|
|
48
|
+
- 'read': Get the content of a file or section
|
|
49
|
+
- 'tree': Show recursive structure from this path
|
|
50
|
+
- 'search': Find docs matching query/tag (path ignored)
|
|
51
|
+
- 'get_tags': List all available tags (path ignored)
|
|
52
|
+
query: Search term for 'search' action. Matches against path/title.
|
|
53
|
+
tag: Filter by tag for 'search' action. Use 'get_tags' to see available tags.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
For 'list': Lines like "[DIR] name/", "[FILE] name.md", "[SEC] Section Title"
|
|
57
|
+
For 'read': The markdown content of the file or section
|
|
58
|
+
For 'tree': Indented tree structure with [D]/[F]/[S] markers
|
|
59
|
+
For 'search': List of matching paths
|
|
60
|
+
For 'get_tags': List of available tags
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
soe_explore_docs(path="/", action="list") # See all docs
|
|
64
|
+
soe_explore_docs(path="docs/guide_01_basics.md", action="list") # See sections
|
|
65
|
+
soe_explore_docs(path="docs/guide_01_basics.md", action="read") # Read full file
|
|
66
|
+
soe_explore_docs(path="docs/guide_01_basics.md/Quick Start", action="read") # Read section
|
|
67
|
+
soe_explore_docs(path="/", action="search", query="agent") # Find agent-related docs
|
|
68
|
+
"""
|
|
69
|
+
# Normalize path
|
|
70
|
+
# Remove leading slash if present to match index keys (which are relative paths like 'soe/docs/...')
|
|
71
|
+
# But handle root '/' as well
|
|
72
|
+
clean_path = path.strip("/")
|
|
73
|
+
|
|
74
|
+
# Special case: "/" means the main docs directory
|
|
75
|
+
if clean_path == "" or path == "/":
|
|
76
|
+
clean_path = "soe/docs"
|
|
77
|
+
|
|
78
|
+
# Dispatch actions
|
|
79
|
+
if action == "list":
|
|
80
|
+
return _handle_list(clean_path)
|
|
81
|
+
elif action == "read":
|
|
82
|
+
return _handle_read(clean_path)
|
|
83
|
+
elif action == "tree":
|
|
84
|
+
return _handle_tree(clean_path)
|
|
85
|
+
elif action == "search":
|
|
86
|
+
return _handle_search(query, tag)
|
|
87
|
+
elif action == "get_tags":
|
|
88
|
+
return _handle_get_tags()
|
|
89
|
+
else:
|
|
90
|
+
return f"Error: Unknown action '{action}'"
|
|
91
|
+
|
|
92
|
+
def _handle_list(path: str) -> str:
|
|
93
|
+
# If path is empty/root, return root children
|
|
94
|
+
if path == "" or path == ".":
|
|
95
|
+
children = DOCS_INDEX.get("root_children", [])
|
|
96
|
+
return _format_list(children)
|
|
97
|
+
|
|
98
|
+
# Check if path exists in index
|
|
99
|
+
# Try exact match first
|
|
100
|
+
# Index keys usually end with / for dirs? My build script adds / for dirs.
|
|
101
|
+
|
|
102
|
+
# Try as directory
|
|
103
|
+
dir_key = path if path.endswith("/") else path + "/"
|
|
104
|
+
if dir_key in DOCS_INDEX["items"]:
|
|
105
|
+
item = DOCS_INDEX["items"][dir_key]
|
|
106
|
+
return _format_list(item.get("children", []))
|
|
107
|
+
|
|
108
|
+
# Try as file or section (no trailing slash)
|
|
109
|
+
file_key = path.rstrip("/")
|
|
110
|
+
if file_key in DOCS_INDEX["items"]:
|
|
111
|
+
item = DOCS_INDEX["items"][file_key]
|
|
112
|
+
return _format_list(item.get("children", []))
|
|
113
|
+
|
|
114
|
+
return f"Error: Path '{path}' not found in index."
|
|
115
|
+
|
|
116
|
+
def _format_list(children: List[str]) -> str:
|
|
117
|
+
if not children:
|
|
118
|
+
return "(empty)"
|
|
119
|
+
|
|
120
|
+
lines = []
|
|
121
|
+
for child in sorted(children):
|
|
122
|
+
item = DOCS_INDEX["items"][child] # Children are always valid in well-formed index
|
|
123
|
+
type_ = item.get("type", "unknown")
|
|
124
|
+
|
|
125
|
+
# Display name is the last part of the path
|
|
126
|
+
# For sections: docs/file.md/Section -> Section
|
|
127
|
+
# For files: docs/file.md -> file.md
|
|
128
|
+
display_name = child.rstrip("/").split("/")[-1]
|
|
129
|
+
|
|
130
|
+
if type_ == "dir":
|
|
131
|
+
lines.append(f"[DIR] {display_name}/")
|
|
132
|
+
elif type_ == "file":
|
|
133
|
+
lines.append(f"[FILE] {display_name}")
|
|
134
|
+
elif type_ == "section":
|
|
135
|
+
lines.append(f"[SEC] {display_name}")
|
|
136
|
+
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
|
|
139
|
+
def _handle_read(path: str) -> str:
|
|
140
|
+
# Normalize
|
|
141
|
+
key = path.rstrip("/")
|
|
142
|
+
|
|
143
|
+
# Check if it exists as a file/section (exact match on stripped key)
|
|
144
|
+
if key in DOCS_INDEX["items"]:
|
|
145
|
+
item = DOCS_INDEX["items"][key]
|
|
146
|
+
type_ = item.get("type")
|
|
147
|
+
|
|
148
|
+
if type_ == "dir":
|
|
149
|
+
return f"Error: '{path}' is a directory. Use action='list' or 'tree'."
|
|
150
|
+
|
|
151
|
+
# Check if it exists as a directory (key + "/")
|
|
152
|
+
elif (key + "/") in DOCS_INDEX["items"]:
|
|
153
|
+
return f"Error: '{path}' is a directory. Use action='list' or 'tree'."
|
|
154
|
+
|
|
155
|
+
else:
|
|
156
|
+
return f"Error: Path '{path}' not found."
|
|
157
|
+
|
|
158
|
+
# Get file path
|
|
159
|
+
file_path_str = item.get("file_path") or item.get("path") # For files, path is file_path
|
|
160
|
+
|
|
161
|
+
# Read file relative to SOE package root
|
|
162
|
+
full_path = SOE_ROOT / file_path_str
|
|
163
|
+
try:
|
|
164
|
+
content = full_path.read_text()
|
|
165
|
+
except FileNotFoundError:
|
|
166
|
+
return f"Error: File not found at '{full_path}'. The docs index may be out of date."
|
|
167
|
+
lines = content.splitlines()
|
|
168
|
+
|
|
169
|
+
if type_ == "file":
|
|
170
|
+
return content
|
|
171
|
+
|
|
172
|
+
# type_ == "section"
|
|
173
|
+
start = item["start_line"] - 1
|
|
174
|
+
end = item["end_line"]
|
|
175
|
+
if end == -1:
|
|
176
|
+
end = len(lines)
|
|
177
|
+
|
|
178
|
+
section_lines = lines[start:end]
|
|
179
|
+
return "\n".join(section_lines)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _handle_tree(path: str) -> str:
|
|
183
|
+
# Recursive listing
|
|
184
|
+
# We can use the index structure
|
|
185
|
+
|
|
186
|
+
# Normalize
|
|
187
|
+
if path == "" or path == ".":
|
|
188
|
+
roots = DOCS_INDEX.get("root_children", [])
|
|
189
|
+
return _build_tree_str(roots, indent=0)
|
|
190
|
+
|
|
191
|
+
key = path if path.endswith("/") else path + "/"
|
|
192
|
+
# Try dir
|
|
193
|
+
if key in DOCS_INDEX["items"]:
|
|
194
|
+
return _build_tree_str([key], indent=0)
|
|
195
|
+
|
|
196
|
+
# Try file
|
|
197
|
+
key = path.rstrip("/")
|
|
198
|
+
if key in DOCS_INDEX["items"]:
|
|
199
|
+
return _build_tree_str([key], indent=0)
|
|
200
|
+
|
|
201
|
+
return f"Error: Path '{path}' not found."
|
|
202
|
+
|
|
203
|
+
def _build_tree_str(paths: List[str], indent: int) -> str:
|
|
204
|
+
out = []
|
|
205
|
+
for p in sorted(paths):
|
|
206
|
+
item = DOCS_INDEX["items"].get(p)
|
|
207
|
+
if not item: continue
|
|
208
|
+
|
|
209
|
+
display_name = p.rstrip("/").split("/")[-1]
|
|
210
|
+
prefix = " " * indent
|
|
211
|
+
|
|
212
|
+
type_ = item.get("type")
|
|
213
|
+
marker = "[D]" if type_ == "dir" else "[F]" if type_ == "file" else "[S]"
|
|
214
|
+
|
|
215
|
+
out.append(f"{prefix}{marker} {display_name}")
|
|
216
|
+
|
|
217
|
+
# Recurse
|
|
218
|
+
children = item.get("children", [])
|
|
219
|
+
if children:
|
|
220
|
+
out.append(_build_tree_str(children, indent + 1))
|
|
221
|
+
|
|
222
|
+
return "\n".join(out)
|
|
223
|
+
|
|
224
|
+
def _handle_search(query: str, tag: str) -> str:
|
|
225
|
+
if not query and not tag:
|
|
226
|
+
return "Error: Provide 'query' or 'tag' for search."
|
|
227
|
+
|
|
228
|
+
# Tag filter
|
|
229
|
+
candidate_paths = set(DOCS_INDEX["items"].keys())
|
|
230
|
+
if tag:
|
|
231
|
+
if tag in DOCS_INDEX["tags"]:
|
|
232
|
+
candidate_paths = set(DOCS_INDEX["tags"][tag])
|
|
233
|
+
else:
|
|
234
|
+
return f"No results for tag '{tag}'"
|
|
235
|
+
|
|
236
|
+
# Text search - split query into words and match ANY word
|
|
237
|
+
# Also search in content_preview for better results
|
|
238
|
+
final_results = set()
|
|
239
|
+
|
|
240
|
+
if query:
|
|
241
|
+
# Split query into words, filter out common words
|
|
242
|
+
stop_words = {"and", "or", "the", "a", "an", "in", "on", "at", "to", "for", "of", "with", "how", "does", "do", "is", "are", "what"}
|
|
243
|
+
query_words = [w.lower() for w in query.split() if w.lower() not in stop_words and len(w) > 1]
|
|
244
|
+
|
|
245
|
+
if not query_words:
|
|
246
|
+
# Fall back to full query if all words were filtered
|
|
247
|
+
query_words = [query.lower()]
|
|
248
|
+
|
|
249
|
+
for p in candidate_paths:
|
|
250
|
+
item = DOCS_INDEX["items"].get(p, {})
|
|
251
|
+
|
|
252
|
+
# Search in path, title, and content_preview
|
|
253
|
+
searchable_text = p.lower()
|
|
254
|
+
if "title" in item:
|
|
255
|
+
searchable_text += " " + item["title"].lower()
|
|
256
|
+
if "content_preview" in item:
|
|
257
|
+
searchable_text += " " + item["content_preview"].lower()
|
|
258
|
+
|
|
259
|
+
# Match if ANY query word is found
|
|
260
|
+
for word in query_words:
|
|
261
|
+
if word in searchable_text:
|
|
262
|
+
final_results.add(p)
|
|
263
|
+
break
|
|
264
|
+
else:
|
|
265
|
+
# Tag only search
|
|
266
|
+
final_results = candidate_paths
|
|
267
|
+
|
|
268
|
+
if not final_results:
|
|
269
|
+
return f"No results found for: {query_words if query else tag}"
|
|
270
|
+
|
|
271
|
+
# Dedupe: skip sections if parent file is also in results
|
|
272
|
+
deduped = set()
|
|
273
|
+
for r in final_results:
|
|
274
|
+
parts = r.rsplit("/", 1)
|
|
275
|
+
if len(parts) == 2 and parts[0] in final_results:
|
|
276
|
+
continue # Skip section if parent file matches
|
|
277
|
+
deduped.add(r)
|
|
278
|
+
|
|
279
|
+
# Format results - limit to 20 most relevant
|
|
280
|
+
sorted_results = sorted(deduped)[:20]
|
|
281
|
+
result_text = "\n".join(sorted_results)
|
|
282
|
+
|
|
283
|
+
if len(deduped) > 20:
|
|
284
|
+
result_text += f"\n... and {len(deduped) - 20} more results"
|
|
285
|
+
|
|
286
|
+
return result_text
|
|
287
|
+
|
|
288
|
+
def _handle_get_tags() -> str:
|
|
289
|
+
tags = sorted(DOCS_INDEX.get("tags", {}).keys())
|
|
290
|
+
return f"Available tags: {tags}"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Built-in tool to list available tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Callable, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_soe_get_available_tools_tool(
|
|
7
|
+
execution_id: str,
|
|
8
|
+
backends,
|
|
9
|
+
tools_registry: Dict[str, Any] = None,
|
|
10
|
+
) -> Callable:
|
|
11
|
+
"""
|
|
12
|
+
Factory function to create soe_get_available_tools tool.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
execution_id: ID for the execution
|
|
16
|
+
backends: Backend services
|
|
17
|
+
tools_registry: Registry of user-provided tools
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Configured soe_get_available_tools function
|
|
21
|
+
"""
|
|
22
|
+
from . import BUILTIN_TOOLS
|
|
23
|
+
|
|
24
|
+
def soe_get_available_tools() -> Dict[str, List[str]]:
|
|
25
|
+
"""
|
|
26
|
+
Get all available tools that can be used in workflows.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
{
|
|
30
|
+
"builtin_tools": [...list of builtin tool names...],
|
|
31
|
+
"user_tools": [...list of user-provided tool names...]
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
builtin_names = list(BUILTIN_TOOLS.keys())
|
|
35
|
+
user_names = list(tools_registry.keys()) if tools_registry else []
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"builtin_tools": builtin_names,
|
|
39
|
+
"user_tools": user_names,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return soe_get_available_tools
|