flowent 0.3.2 → 0.3.4
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.
- package/README.md +3 -0
- package/backend/README.md +3 -0
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +61 -2
- package/backend/src/flowent/agent_runtime.py +220 -0
- package/backend/src/flowent/api_models.py +9 -1
- package/backend/src/flowent/app.py +9 -2
- package/backend/src/flowent/permissions.py +12 -3
- package/backend/src/flowent/routes/workflow_routes.py +22 -42
- package/backend/src/flowent/sandbox.py +63 -19
- package/backend/src/flowent/state/models.py +1 -1
- package/backend/src/flowent/static/assets/index-ByGH1ZWH.css +2 -0
- package/backend/src/flowent/static/assets/index-D3WSbctU.js +98 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/tools.py +60 -4
- package/backend/src/flowent/workflow_service.py +115 -0
- package/backend/src/flowent/workflow_tools.py +279 -0
- package/backend/src/flowent/workflows.py +182 -19
- package/backend/src/flowent/workspace/runtime.py +81 -93
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-ByGH1ZWH.css +2 -0
- package/dist/frontend/assets/index-D3WSbctU.js +98 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-BX18a4Jz.js +0 -100
- package/backend/src/flowent/static/assets/index-EC37agAH.css +0 -2
- package/dist/frontend/assets/index-BX18a4Jz.js +0 -100
- package/dist/frontend/assets/index-EC37agAH.css +0 -2
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Flowent</title>
|
|
8
8
|
<meta name="description" content="Flowent application" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-D3WSbctU.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-ByGH1ZWH.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -6,7 +6,7 @@ import subprocess
|
|
|
6
6
|
import sys
|
|
7
7
|
import urllib.parse
|
|
8
8
|
import urllib.request
|
|
9
|
-
from collections.abc import Callable, Sequence
|
|
9
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from uuid import uuid4
|
|
@@ -28,9 +28,57 @@ class ToolResult(BaseModel):
|
|
|
28
28
|
title: str
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
ToolEventEmitter = Callable[[dict[str, object]], Awaitable[None]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CommandOutputCollector:
|
|
35
|
+
def __init__(
|
|
36
|
+
self, command: str, emit_event: ToolEventEmitter | None = None
|
|
37
|
+
) -> None:
|
|
38
|
+
self.command = command
|
|
39
|
+
self.emit_event = emit_event
|
|
40
|
+
self.output_chunks: list[dict[str, str]] = []
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def stdout(self) -> str:
|
|
44
|
+
return "".join(
|
|
45
|
+
item["content"] for item in self.output_chunks if item["stream"] == "stdout"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def stderr(self) -> str:
|
|
50
|
+
return "".join(
|
|
51
|
+
item["content"] for item in self.output_chunks if item["stream"] == "stderr"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def result(self) -> dict[str, object]:
|
|
55
|
+
return {
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": self.command,
|
|
58
|
+
"output_chunks": [dict(item) for item in self.output_chunks],
|
|
59
|
+
"stderr": self.stderr,
|
|
60
|
+
"stdout": self.stdout,
|
|
61
|
+
"output": self.stdout or self.stderr,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async def append(self, stream: str, content: str) -> None:
|
|
65
|
+
if not content:
|
|
66
|
+
return
|
|
67
|
+
self.output_chunks.append({"stream": stream, "content": content})
|
|
68
|
+
if self.emit_event is not None:
|
|
69
|
+
await self.emit_event({"result": self.result(), "status": "running"})
|
|
70
|
+
|
|
71
|
+
async def append_stderr(self, content: str) -> None:
|
|
72
|
+
await self.append("stderr", content)
|
|
73
|
+
|
|
74
|
+
async def append_stdout(self, content: str) -> None:
|
|
75
|
+
await self.append("stdout", content)
|
|
76
|
+
|
|
77
|
+
|
|
31
78
|
@dataclass(frozen=True)
|
|
32
79
|
class ToolContext:
|
|
33
80
|
cwd: Path
|
|
81
|
+
emit_event: ToolEventEmitter | None = None
|
|
34
82
|
web_searcher: Callable[[str], Sequence[dict[str, str]]] | None = None
|
|
35
83
|
|
|
36
84
|
|
|
@@ -42,6 +90,7 @@ def command_tool_result(
|
|
|
42
90
|
*,
|
|
43
91
|
command: str,
|
|
44
92
|
exit_code: int,
|
|
93
|
+
output_chunks: list[dict[str, str]] | None = None,
|
|
45
94
|
stderr: str,
|
|
46
95
|
stdout: str,
|
|
47
96
|
) -> dict[str, object]:
|
|
@@ -49,6 +98,7 @@ def command_tool_result(
|
|
|
49
98
|
"type": "command",
|
|
50
99
|
"command": command,
|
|
51
100
|
"exit_code": exit_code,
|
|
101
|
+
"output_chunks": [dict(item) for item in output_chunks or []],
|
|
52
102
|
"stderr": stderr,
|
|
53
103
|
"stdout": stdout,
|
|
54
104
|
"output": stdout or stderr,
|
|
@@ -454,16 +504,22 @@ async def shell_command_async(
|
|
|
454
504
|
command = str(arguments["command"])
|
|
455
505
|
timeout_seconds = number_argument(arguments, "timeout_seconds", 30)
|
|
456
506
|
invocation = shell_invocation(command)
|
|
507
|
+
collector = CommandOutputCollector(command, context.emit_event)
|
|
457
508
|
result = await SandboxRunner(cwd=context.cwd).run_async(
|
|
458
|
-
invocation.args,
|
|
509
|
+
invocation.args,
|
|
510
|
+
env=invocation.env,
|
|
511
|
+
on_stderr=collector.append_stderr,
|
|
512
|
+
on_stdout=collector.append_stdout,
|
|
513
|
+
timeout_seconds=timeout_seconds,
|
|
459
514
|
)
|
|
460
515
|
ok = result.exit_code == 0
|
|
461
516
|
return ToolResult(
|
|
462
517
|
result=command_tool_result(
|
|
463
518
|
command=command,
|
|
464
519
|
exit_code=result.exit_code,
|
|
465
|
-
|
|
466
|
-
|
|
520
|
+
output_chunks=collector.output_chunks,
|
|
521
|
+
stderr=result.stderr or collector.stderr,
|
|
522
|
+
stdout=result.stdout or collector.stdout,
|
|
467
523
|
),
|
|
468
524
|
ok=ok,
|
|
469
525
|
title=f"Ran {command}",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from flowent.agent_runtime import FlowentAgentRuntime
|
|
7
|
+
from flowent.llm import CompletionCallable
|
|
8
|
+
from flowent.mcp import McpManager
|
|
9
|
+
from flowent.provider_connections import selected_connection
|
|
10
|
+
from flowent.storage import StateStore, StoredWorkflow, StoredWorkflowDefinition
|
|
11
|
+
from flowent.workflows import (
|
|
12
|
+
WorkflowRunResponse,
|
|
13
|
+
run_workflow_definition,
|
|
14
|
+
validate_workflow_draft,
|
|
15
|
+
workflow_requires_connection,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkflowService:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
chat_completion: CompletionCallable | None,
|
|
24
|
+
cwd: Path,
|
|
25
|
+
mcp_manager: McpManager,
|
|
26
|
+
store: StateStore,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.chat_completion = chat_completion
|
|
29
|
+
self.cwd = cwd
|
|
30
|
+
self.mcp_manager = mcp_manager
|
|
31
|
+
self.store = store
|
|
32
|
+
self.agent_runtime = FlowentAgentRuntime(
|
|
33
|
+
chat_completion=chat_completion,
|
|
34
|
+
cwd=cwd,
|
|
35
|
+
mcp_manager=mcp_manager,
|
|
36
|
+
store=store,
|
|
37
|
+
workflow_service=self,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def list_workflows(self) -> list[StoredWorkflow]:
|
|
41
|
+
return self.store.read_workflows()
|
|
42
|
+
|
|
43
|
+
def get_workflow(self, workflow_id: str) -> StoredWorkflow:
|
|
44
|
+
workflow = next(
|
|
45
|
+
(
|
|
46
|
+
current_workflow
|
|
47
|
+
for current_workflow in self.store.read_workflows()
|
|
48
|
+
if current_workflow.id == workflow_id
|
|
49
|
+
),
|
|
50
|
+
None,
|
|
51
|
+
)
|
|
52
|
+
if workflow is None:
|
|
53
|
+
raise ValueError("Workflow not found.")
|
|
54
|
+
return workflow
|
|
55
|
+
|
|
56
|
+
def save_workflow(self, workflow: StoredWorkflow) -> StoredWorkflow:
|
|
57
|
+
return self.store.save_workflow(
|
|
58
|
+
validate_workflow_draft(
|
|
59
|
+
workflow.model_copy(
|
|
60
|
+
update={"name": workflow.name.strip() or "Untitled Workflow"}
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def run_workflow(
|
|
66
|
+
self,
|
|
67
|
+
workflow_id: str,
|
|
68
|
+
*,
|
|
69
|
+
default_input: str = "",
|
|
70
|
+
input_values: Mapping[str, str] | None = None,
|
|
71
|
+
timer_node_id: str = "",
|
|
72
|
+
workflow_depth: int = 0,
|
|
73
|
+
) -> WorkflowRunResponse:
|
|
74
|
+
workflow = self.get_workflow(workflow_id)
|
|
75
|
+
connection = (
|
|
76
|
+
selected_connection(self.store.read_state())
|
|
77
|
+
if workflow_requires_connection(workflow.definition)
|
|
78
|
+
else None
|
|
79
|
+
)
|
|
80
|
+
return await run_workflow_definition(
|
|
81
|
+
connection=connection,
|
|
82
|
+
default_input=default_input,
|
|
83
|
+
definition=workflow.definition,
|
|
84
|
+
input_values=input_values,
|
|
85
|
+
runtime=self.agent_runtime,
|
|
86
|
+
timer_node_id=timer_node_id,
|
|
87
|
+
workflow_depth=workflow_depth,
|
|
88
|
+
workflow_id=workflow.id,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
async def run_workflow_definition(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
default_input: str = "",
|
|
95
|
+
definition: StoredWorkflowDefinition,
|
|
96
|
+
input_values: Mapping[str, str] | None = None,
|
|
97
|
+
timer_node_id: str = "",
|
|
98
|
+
workflow_depth: int = 0,
|
|
99
|
+
workflow_id: str,
|
|
100
|
+
) -> WorkflowRunResponse:
|
|
101
|
+
connection = (
|
|
102
|
+
selected_connection(self.store.read_state())
|
|
103
|
+
if workflow_requires_connection(definition)
|
|
104
|
+
else None
|
|
105
|
+
)
|
|
106
|
+
return await run_workflow_definition(
|
|
107
|
+
connection=connection,
|
|
108
|
+
default_input=default_input,
|
|
109
|
+
definition=definition,
|
|
110
|
+
input_values=input_values,
|
|
111
|
+
runtime=self.agent_runtime,
|
|
112
|
+
timer_node_id=timer_node_id,
|
|
113
|
+
workflow_depth=workflow_depth,
|
|
114
|
+
workflow_id=workflow_id,
|
|
115
|
+
)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from flowent.storage import StoredWorkflow
|
|
9
|
+
from flowent.tools import ToolResult, text_tool_result
|
|
10
|
+
from flowent.workflows import WorkflowRunResponse
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from flowent.workflow_service import WorkflowService
|
|
14
|
+
|
|
15
|
+
MAX_WORKFLOW_TOOL_DEPTH = 3
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def workflow_tool_specs() -> list[dict[str, object]]:
|
|
19
|
+
workflow_schema: dict[str, object] = {"type": "object"}
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
"type": "function",
|
|
23
|
+
"function": {
|
|
24
|
+
"name": "list_workflows",
|
|
25
|
+
"description": "List saved workflows with their ids, names, node counts, and edge counts.",
|
|
26
|
+
"parameters": {"type": "object", "properties": {}},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "function",
|
|
31
|
+
"function": {
|
|
32
|
+
"name": "get_workflow",
|
|
33
|
+
"description": "Read a saved workflow definition by id before answering questions or editing it.",
|
|
34
|
+
"parameters": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {"workflow_id": {"type": "string"}},
|
|
37
|
+
"required": ["workflow_id"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "function",
|
|
43
|
+
"function": {
|
|
44
|
+
"name": "run_workflow",
|
|
45
|
+
"description": "Run a saved workflow. Pass input when the user's current message contains the content the workflow should process.",
|
|
46
|
+
"parameters": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"workflow_id": {"type": "string"},
|
|
50
|
+
"input": {"type": "string"},
|
|
51
|
+
"inputs": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": {"type": "string"},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"required": ["workflow_id"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "function",
|
|
62
|
+
"function": {
|
|
63
|
+
"name": "create_workflow",
|
|
64
|
+
"description": "Create a workflow. workflow must include id, name, and definition with version, nodes, and edges.",
|
|
65
|
+
"parameters": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {"workflow": workflow_schema},
|
|
68
|
+
"required": ["workflow"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "function",
|
|
74
|
+
"function": {
|
|
75
|
+
"name": "update_workflow",
|
|
76
|
+
"description": "Replace an existing workflow. Read it first and provide the complete updated workflow object.",
|
|
77
|
+
"parameters": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {"workflow": workflow_schema},
|
|
80
|
+
"required": ["workflow"],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def workflow_tool_title(name: str) -> str | None:
|
|
88
|
+
if name == "list_workflows":
|
|
89
|
+
return "Listing workflows"
|
|
90
|
+
if name == "get_workflow":
|
|
91
|
+
return "Reading workflow"
|
|
92
|
+
if name == "run_workflow":
|
|
93
|
+
return "Running workflow"
|
|
94
|
+
if name == "create_workflow":
|
|
95
|
+
return "Creating workflow"
|
|
96
|
+
if name == "update_workflow":
|
|
97
|
+
return "Updating workflow"
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class WorkflowAgentTools:
|
|
102
|
+
def __init__(self, service: WorkflowService, *, workflow_depth: int = 0) -> None:
|
|
103
|
+
self.service = service
|
|
104
|
+
self.workflow_depth = workflow_depth
|
|
105
|
+
|
|
106
|
+
async def run_tool(
|
|
107
|
+
self, name: str, arguments: dict[str, object]
|
|
108
|
+
) -> ToolResult | None:
|
|
109
|
+
title = workflow_tool_title(name)
|
|
110
|
+
if title is None:
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
if name == "list_workflows":
|
|
114
|
+
return self.list_workflows()
|
|
115
|
+
if name == "get_workflow":
|
|
116
|
+
return self.get_workflow(arguments)
|
|
117
|
+
if name == "run_workflow":
|
|
118
|
+
return await self.run_workflow(arguments)
|
|
119
|
+
if name == "create_workflow":
|
|
120
|
+
return self.create_workflow(arguments)
|
|
121
|
+
if name == "update_workflow":
|
|
122
|
+
return self.update_workflow(arguments)
|
|
123
|
+
except Exception as error:
|
|
124
|
+
return ToolResult(
|
|
125
|
+
result=text_tool_result(str(error) or "Workflow tool failed."),
|
|
126
|
+
ok=False,
|
|
127
|
+
title=title,
|
|
128
|
+
)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def list_workflows(self) -> ToolResult:
|
|
132
|
+
workflows = self.service.list_workflows()
|
|
133
|
+
summaries = [workflow_summary(workflow) for workflow in workflows]
|
|
134
|
+
if summaries:
|
|
135
|
+
output = "\n".join(
|
|
136
|
+
f"{summary['id']}: {summary['name']} ({summary['node_count']} nodes, {summary['edge_count']} edges)"
|
|
137
|
+
for summary in summaries
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
output = "No workflows are saved."
|
|
141
|
+
return ToolResult(
|
|
142
|
+
result={
|
|
143
|
+
"type": "workflow_list",
|
|
144
|
+
"output": output,
|
|
145
|
+
"workflows": summaries,
|
|
146
|
+
},
|
|
147
|
+
title="Listed workflows",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def get_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
151
|
+
workflow = self.service.get_workflow(str(arguments["workflow_id"]))
|
|
152
|
+
summary = workflow_summary(workflow)
|
|
153
|
+
output = (
|
|
154
|
+
f"{workflow.name} has {summary['node_count']} nodes and "
|
|
155
|
+
f"{summary['edge_count']} edges."
|
|
156
|
+
)
|
|
157
|
+
return ToolResult(
|
|
158
|
+
result={
|
|
159
|
+
"type": "workflow",
|
|
160
|
+
"output": output,
|
|
161
|
+
"summary": summary,
|
|
162
|
+
"workflow": workflow.model_dump(mode="json"),
|
|
163
|
+
},
|
|
164
|
+
title=f"Read {workflow.name}",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def run_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
168
|
+
if self.workflow_depth >= MAX_WORKFLOW_TOOL_DEPTH:
|
|
169
|
+
raise ValueError("Workflow nesting is too deep.")
|
|
170
|
+
workflow_id = str(arguments["workflow_id"])
|
|
171
|
+
workflow = self.service.get_workflow(workflow_id)
|
|
172
|
+
result = await self.service.run_workflow(
|
|
173
|
+
workflow_id,
|
|
174
|
+
default_input=string_argument(arguments, "input"),
|
|
175
|
+
input_values=string_map_argument(arguments, "inputs"),
|
|
176
|
+
workflow_depth=self.workflow_depth + 1,
|
|
177
|
+
)
|
|
178
|
+
output = workflow_run_output(workflow.name, result)
|
|
179
|
+
return ToolResult(
|
|
180
|
+
result={
|
|
181
|
+
"type": "workflow_run",
|
|
182
|
+
"node_results": [
|
|
183
|
+
node_result.model_dump(mode="json")
|
|
184
|
+
for node_result in result.node_results
|
|
185
|
+
],
|
|
186
|
+
"output": output,
|
|
187
|
+
"outputs": dict(result.outputs),
|
|
188
|
+
"status": result.status,
|
|
189
|
+
"workflow_id": workflow.id,
|
|
190
|
+
"workflow_name": workflow.name,
|
|
191
|
+
},
|
|
192
|
+
ok=result.status == "success",
|
|
193
|
+
title=f"Ran {workflow.name}",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def create_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
197
|
+
workflow = workflow_argument(arguments)
|
|
198
|
+
saved = self.service.save_workflow(
|
|
199
|
+
workflow.model_copy(update={"id": str(uuid4())})
|
|
200
|
+
)
|
|
201
|
+
return saved_workflow_result(saved, "Created")
|
|
202
|
+
|
|
203
|
+
def update_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
204
|
+
workflow = workflow_argument(arguments)
|
|
205
|
+
self.service.get_workflow(workflow.id)
|
|
206
|
+
saved = self.service.save_workflow(workflow)
|
|
207
|
+
return saved_workflow_result(saved, "Updated")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def string_argument(arguments: dict[str, object], name: str) -> str:
|
|
211
|
+
value = arguments.get(name, "")
|
|
212
|
+
if value is None:
|
|
213
|
+
return ""
|
|
214
|
+
return value if isinstance(value, str) else str(value)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def string_map_argument(
|
|
218
|
+
arguments: dict[str, object], name: str
|
|
219
|
+
) -> dict[str, str] | None:
|
|
220
|
+
value = arguments.get(name)
|
|
221
|
+
if not isinstance(value, dict):
|
|
222
|
+
return None
|
|
223
|
+
return {str(key): str(item) for key, item in value.items()}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def workflow_argument(arguments: dict[str, object]) -> StoredWorkflow:
|
|
227
|
+
value = arguments.get("workflow")
|
|
228
|
+
if not isinstance(value, dict):
|
|
229
|
+
raise ValueError("Workflow must be an object.")
|
|
230
|
+
try:
|
|
231
|
+
return StoredWorkflow.model_validate(value)
|
|
232
|
+
except ValidationError as error:
|
|
233
|
+
raise ValueError(error.errors()[0]["msg"]) from error
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def workflow_summary(workflow: StoredWorkflow) -> dict[str, object]:
|
|
237
|
+
return {
|
|
238
|
+
"edge_count": len(workflow.definition.edges),
|
|
239
|
+
"id": workflow.id,
|
|
240
|
+
"name": workflow.name,
|
|
241
|
+
"node_count": len(workflow.definition.nodes),
|
|
242
|
+
"nodes": [
|
|
243
|
+
{"id": node.id, "name": node.name, "type": node.type}
|
|
244
|
+
for node in workflow.definition.nodes
|
|
245
|
+
],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def workflow_run_output(name: str, result: WorkflowRunResponse) -> str:
|
|
250
|
+
if result.status == "success":
|
|
251
|
+
if result.outputs:
|
|
252
|
+
rendered_outputs = "\n".join(
|
|
253
|
+
f"{key}: {value}" for key, value in result.outputs.items()
|
|
254
|
+
)
|
|
255
|
+
return f"{name} completed.\n{rendered_outputs}"
|
|
256
|
+
return f"{name} completed."
|
|
257
|
+
failures = [
|
|
258
|
+
f"{node_result.id}: {node_result.error}"
|
|
259
|
+
for node_result in result.node_results
|
|
260
|
+
if node_result.status == "failed"
|
|
261
|
+
]
|
|
262
|
+
return f"{name} failed.\n" + ("\n".join(failures) or "No failure details.")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def saved_workflow_result(workflow: StoredWorkflow, action: str) -> ToolResult:
|
|
266
|
+
summary = workflow_summary(workflow)
|
|
267
|
+
output = (
|
|
268
|
+
f"{action} {workflow.name} with {summary['node_count']} nodes and "
|
|
269
|
+
f"{summary['edge_count']} edges."
|
|
270
|
+
)
|
|
271
|
+
return ToolResult(
|
|
272
|
+
result={
|
|
273
|
+
"type": "workflow",
|
|
274
|
+
"output": output,
|
|
275
|
+
"summary": summary,
|
|
276
|
+
"workflow": workflow.model_dump(mode="json"),
|
|
277
|
+
},
|
|
278
|
+
title=f"{action} {workflow.name}",
|
|
279
|
+
)
|