flowent 0.3.3 → 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 +1 -1
- package/backend/src/flowent/agent_runtime.py +220 -0
- package/backend/src/flowent/api_models.py +9 -1
- package/backend/src/flowent/app.py +2 -0
- package/backend/src/flowent/routes/workflow_routes.py +15 -3
- 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/workflow_service.py +24 -2
- package/backend/src/flowent/workflow_tools.py +16 -8
- package/backend/src/flowent/workflows.py +113 -18
- package/backend/src/flowent/workspace/runtime.py +50 -124
- 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-CCf0mo80.css +0 -2
- package/backend/src/flowent/static/assets/index-CROofCFl.js +0 -102
- package/dist/frontend/assets/index-CCf0mo80.css +0 -2
- package/dist/frontend/assets/index-CROofCFl.js +0 -102
|
@@ -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>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
6
|
+
from flowent.agent_runtime import FlowentAgentRuntime
|
|
5
7
|
from flowent.llm import CompletionCallable
|
|
8
|
+
from flowent.mcp import McpManager
|
|
6
9
|
from flowent.provider_connections import selected_connection
|
|
7
10
|
from flowent.storage import StateStore, StoredWorkflow, StoredWorkflowDefinition
|
|
8
11
|
from flowent.workflows import (
|
|
@@ -18,10 +21,21 @@ class WorkflowService:
|
|
|
18
21
|
self,
|
|
19
22
|
*,
|
|
20
23
|
chat_completion: CompletionCallable | None,
|
|
24
|
+
cwd: Path,
|
|
25
|
+
mcp_manager: McpManager,
|
|
21
26
|
store: StateStore,
|
|
22
27
|
) -> None:
|
|
23
28
|
self.chat_completion = chat_completion
|
|
29
|
+
self.cwd = cwd
|
|
30
|
+
self.mcp_manager = mcp_manager
|
|
24
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
|
+
)
|
|
25
39
|
|
|
26
40
|
def list_workflows(self) -> list[StoredWorkflow]:
|
|
27
41
|
return self.store.read_workflows()
|
|
@@ -54,6 +68,8 @@ class WorkflowService:
|
|
|
54
68
|
*,
|
|
55
69
|
default_input: str = "",
|
|
56
70
|
input_values: Mapping[str, str] | None = None,
|
|
71
|
+
timer_node_id: str = "",
|
|
72
|
+
workflow_depth: int = 0,
|
|
57
73
|
) -> WorkflowRunResponse:
|
|
58
74
|
workflow = self.get_workflow(workflow_id)
|
|
59
75
|
connection = (
|
|
@@ -62,11 +78,13 @@ class WorkflowService:
|
|
|
62
78
|
else None
|
|
63
79
|
)
|
|
64
80
|
return await run_workflow_definition(
|
|
65
|
-
completion=self.chat_completion,
|
|
66
81
|
connection=connection,
|
|
67
82
|
default_input=default_input,
|
|
68
83
|
definition=workflow.definition,
|
|
69
84
|
input_values=input_values,
|
|
85
|
+
runtime=self.agent_runtime,
|
|
86
|
+
timer_node_id=timer_node_id,
|
|
87
|
+
workflow_depth=workflow_depth,
|
|
70
88
|
workflow_id=workflow.id,
|
|
71
89
|
)
|
|
72
90
|
|
|
@@ -76,6 +94,8 @@ class WorkflowService:
|
|
|
76
94
|
default_input: str = "",
|
|
77
95
|
definition: StoredWorkflowDefinition,
|
|
78
96
|
input_values: Mapping[str, str] | None = None,
|
|
97
|
+
timer_node_id: str = "",
|
|
98
|
+
workflow_depth: int = 0,
|
|
79
99
|
workflow_id: str,
|
|
80
100
|
) -> WorkflowRunResponse:
|
|
81
101
|
connection = (
|
|
@@ -84,10 +104,12 @@ class WorkflowService:
|
|
|
84
104
|
else None
|
|
85
105
|
)
|
|
86
106
|
return await run_workflow_definition(
|
|
87
|
-
completion=self.chat_completion,
|
|
88
107
|
connection=connection,
|
|
89
108
|
default_input=default_input,
|
|
90
109
|
definition=definition,
|
|
91
110
|
input_values=input_values,
|
|
111
|
+
runtime=self.agent_runtime,
|
|
112
|
+
timer_node_id=timer_node_id,
|
|
113
|
+
workflow_depth=workflow_depth,
|
|
92
114
|
workflow_id=workflow_id,
|
|
93
115
|
)
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
3
6
|
from pydantic import ValidationError
|
|
4
7
|
|
|
5
8
|
from flowent.storage import StoredWorkflow
|
|
6
9
|
from flowent.tools import ToolResult, text_tool_result
|
|
7
|
-
from flowent.workflow_service import WorkflowService
|
|
8
10
|
from flowent.workflows import WorkflowRunResponse
|
|
9
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from flowent.workflow_service import WorkflowService
|
|
14
|
+
|
|
15
|
+
MAX_WORKFLOW_TOOL_DEPTH = 3
|
|
16
|
+
|
|
10
17
|
|
|
11
18
|
def workflow_tool_specs() -> list[dict[str, object]]:
|
|
12
19
|
workflow_schema: dict[str, object] = {"type": "object"}
|
|
@@ -92,8 +99,9 @@ def workflow_tool_title(name: str) -> str | None:
|
|
|
92
99
|
|
|
93
100
|
|
|
94
101
|
class WorkflowAgentTools:
|
|
95
|
-
def __init__(self, service: WorkflowService) -> None:
|
|
102
|
+
def __init__(self, service: WorkflowService, *, workflow_depth: int = 0) -> None:
|
|
96
103
|
self.service = service
|
|
104
|
+
self.workflow_depth = workflow_depth
|
|
97
105
|
|
|
98
106
|
async def run_tool(
|
|
99
107
|
self, name: str, arguments: dict[str, object]
|
|
@@ -157,12 +165,15 @@ class WorkflowAgentTools:
|
|
|
157
165
|
)
|
|
158
166
|
|
|
159
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.")
|
|
160
170
|
workflow_id = str(arguments["workflow_id"])
|
|
161
171
|
workflow = self.service.get_workflow(workflow_id)
|
|
162
172
|
result = await self.service.run_workflow(
|
|
163
173
|
workflow_id,
|
|
164
174
|
default_input=string_argument(arguments, "input"),
|
|
165
175
|
input_values=string_map_argument(arguments, "inputs"),
|
|
176
|
+
workflow_depth=self.workflow_depth + 1,
|
|
166
177
|
)
|
|
167
178
|
output = workflow_run_output(workflow.name, result)
|
|
168
179
|
return ToolResult(
|
|
@@ -184,12 +195,9 @@ class WorkflowAgentTools:
|
|
|
184
195
|
|
|
185
196
|
def create_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
186
197
|
workflow = workflow_argument(arguments)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
saved = self.service.save_workflow(workflow)
|
|
191
|
-
else:
|
|
192
|
-
raise ValueError("Workflow already exists.")
|
|
198
|
+
saved = self.service.save_workflow(
|
|
199
|
+
workflow.model_copy(update={"id": str(uuid4())})
|
|
200
|
+
)
|
|
193
201
|
return saved_workflow_result(saved, "Created")
|
|
194
202
|
|
|
195
203
|
def update_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import re
|
|
3
5
|
import sys
|
|
@@ -5,15 +7,12 @@ import tempfile
|
|
|
5
7
|
from collections import defaultdict, deque
|
|
6
8
|
from collections.abc import Mapping
|
|
7
9
|
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
8
11
|
|
|
9
12
|
from pydantic import BaseModel, ConfigDict, Field
|
|
10
13
|
|
|
11
|
-
from flowent.
|
|
12
|
-
|
|
13
|
-
CompletionCallable,
|
|
14
|
-
ProviderConnection,
|
|
15
|
-
complete_chat,
|
|
16
|
-
)
|
|
14
|
+
from flowent.context import runtime_context_messages
|
|
15
|
+
from flowent.llm import ProviderConnection
|
|
17
16
|
from flowent.sandbox import SandboxRunner
|
|
18
17
|
from flowent.storage import (
|
|
19
18
|
StoredWorkflow,
|
|
@@ -22,6 +21,9 @@ from flowent.storage import (
|
|
|
22
21
|
StoredWorkflowNode,
|
|
23
22
|
)
|
|
24
23
|
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from flowent.agent_runtime import FlowentAgentRuntime
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
class WorkflowNodeRunResult(BaseModel):
|
|
27
29
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -41,6 +43,13 @@ class WorkflowRunResponse(BaseModel):
|
|
|
41
43
|
workflow_id: str
|
|
42
44
|
|
|
43
45
|
|
|
46
|
+
class WorkflowRunRequestValues(BaseModel):
|
|
47
|
+
model_config = ConfigDict(extra="forbid")
|
|
48
|
+
|
|
49
|
+
default_input: str = ""
|
|
50
|
+
input_values: dict[str, str] = Field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
44
53
|
PLACEHOLDER_PATTERN = re.compile(r"\{\{\s*([A-Za-z0-9_.-]+)\.output\s*\}\}")
|
|
45
54
|
PYTHON_CODE_RUNNER = r"""
|
|
46
55
|
import contextlib
|
|
@@ -106,8 +115,8 @@ def validate_workflow_definition(definition: StoredWorkflowDefinition) -> list[s
|
|
|
106
115
|
raise ValueError("Workflow node ids must not be empty.")
|
|
107
116
|
if len(set(node_ids)) != len(node_ids):
|
|
108
117
|
raise ValueError("Workflow node ids must be unique.")
|
|
109
|
-
if not any(node.type
|
|
110
|
-
raise ValueError("Workflow needs an input node.")
|
|
118
|
+
if not any(node.type in {"input", "timer"} for node in definition.nodes):
|
|
119
|
+
raise ValueError("Workflow needs an input or timer node.")
|
|
111
120
|
if not any(node.type == "output" for node in definition.nodes):
|
|
112
121
|
raise ValueError("Workflow needs an output node.")
|
|
113
122
|
|
|
@@ -129,6 +138,43 @@ def workflow_requires_connection(definition: StoredWorkflowDefinition) -> bool:
|
|
|
129
138
|
return any(node.type == "agent" for node in definition.nodes)
|
|
130
139
|
|
|
131
140
|
|
|
141
|
+
def timer_run_node_ids(
|
|
142
|
+
definition: StoredWorkflowDefinition, timer_node_id: str
|
|
143
|
+
) -> set[str]:
|
|
144
|
+
nodes = {node.id: node for node in definition.nodes}
|
|
145
|
+
timer_node = nodes.get(timer_node_id)
|
|
146
|
+
if timer_node is None or timer_node.type != "timer":
|
|
147
|
+
raise ValueError("Timer node not found.")
|
|
148
|
+
|
|
149
|
+
outgoing: dict[str, list[str]] = defaultdict(list)
|
|
150
|
+
incoming: dict[str, list[str]] = defaultdict(list)
|
|
151
|
+
for edge in definition.edges:
|
|
152
|
+
outgoing[edge.source].append(edge.target)
|
|
153
|
+
incoming[edge.target].append(edge.source)
|
|
154
|
+
|
|
155
|
+
active = {timer_node_id}
|
|
156
|
+
queue = deque([timer_node_id])
|
|
157
|
+
while queue:
|
|
158
|
+
node_id = queue.popleft()
|
|
159
|
+
for target in outgoing[node_id]:
|
|
160
|
+
if target not in active:
|
|
161
|
+
active.add(target)
|
|
162
|
+
queue.append(target)
|
|
163
|
+
|
|
164
|
+
queue = deque(active)
|
|
165
|
+
while queue:
|
|
166
|
+
node_id = queue.popleft()
|
|
167
|
+
for source in incoming[node_id]:
|
|
168
|
+
source_node = nodes[source]
|
|
169
|
+
if source_node.type == "timer" and source != timer_node_id:
|
|
170
|
+
continue
|
|
171
|
+
if source not in active:
|
|
172
|
+
active.add(source)
|
|
173
|
+
queue.append(source)
|
|
174
|
+
|
|
175
|
+
return active
|
|
176
|
+
|
|
177
|
+
|
|
132
178
|
def topological_node_ids(definition: StoredWorkflowDefinition) -> list[str]:
|
|
133
179
|
node_ids = [node.id for node in definition.nodes]
|
|
134
180
|
outgoing: dict[str, list[str]] = defaultdict(list)
|
|
@@ -162,39 +208,73 @@ def topological_node_ids(definition: StoredWorkflowDefinition) -> list[str]:
|
|
|
162
208
|
|
|
163
209
|
async def run_workflow_definition(
|
|
164
210
|
*,
|
|
165
|
-
completion: CompletionCallable | None,
|
|
166
211
|
connection: ProviderConnection | None,
|
|
167
212
|
definition: StoredWorkflowDefinition,
|
|
168
213
|
default_input: str = "",
|
|
169
214
|
input_values: Mapping[str, str] | None = None,
|
|
215
|
+
runtime: FlowentAgentRuntime | None = None,
|
|
216
|
+
timer_node_id: str = "",
|
|
217
|
+
workflow_depth: int = 0,
|
|
170
218
|
workflow_id: str,
|
|
171
219
|
) -> WorkflowRunResponse:
|
|
172
220
|
ordered_ids = validate_workflow_definition(definition)
|
|
173
221
|
if workflow_requires_connection(definition) and connection is None:
|
|
174
222
|
raise ValueError("Choose a provider and model before running.")
|
|
175
223
|
|
|
224
|
+
return await run_workflow_once(
|
|
225
|
+
connection=connection,
|
|
226
|
+
definition=definition,
|
|
227
|
+
input_values=WorkflowRunRequestValues(
|
|
228
|
+
default_input=default_input,
|
|
229
|
+
input_values=dict(input_values or {}),
|
|
230
|
+
),
|
|
231
|
+
ordered_ids=ordered_ids,
|
|
232
|
+
runtime=runtime,
|
|
233
|
+
timer_node_id=timer_node_id,
|
|
234
|
+
workflow_depth=workflow_depth,
|
|
235
|
+
workflow_id=workflow_id,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def run_workflow_once(
|
|
240
|
+
*,
|
|
241
|
+
connection: ProviderConnection | None,
|
|
242
|
+
definition: StoredWorkflowDefinition,
|
|
243
|
+
input_values: WorkflowRunRequestValues,
|
|
244
|
+
ordered_ids: list[str],
|
|
245
|
+
runtime: FlowentAgentRuntime | None = None,
|
|
246
|
+
timer_node_id: str = "",
|
|
247
|
+
workflow_depth: int = 0,
|
|
248
|
+
workflow_id: str,
|
|
249
|
+
) -> WorkflowRunResponse:
|
|
176
250
|
nodes = {node.id: node for node in definition.nodes}
|
|
177
251
|
incoming_edges = edges_by_target(definition.edges)
|
|
252
|
+
active_node_ids = (
|
|
253
|
+
timer_run_node_ids(definition, timer_node_id) if timer_node_id else None
|
|
254
|
+
)
|
|
178
255
|
results: dict[str, WorkflowNodeRunResult] = {
|
|
179
256
|
node.id: WorkflowNodeRunResult(id=node.id, status="pending")
|
|
180
257
|
for node in definition.nodes
|
|
181
258
|
}
|
|
182
259
|
outputs: dict[str, str] = {}
|
|
183
260
|
named_outputs: dict[str, str] = {}
|
|
184
|
-
remaining_default_input = default_input
|
|
261
|
+
remaining_default_input = input_values.default_input
|
|
185
262
|
|
|
186
263
|
for node_id in ordered_ids:
|
|
187
264
|
node = nodes[node_id]
|
|
265
|
+
if active_node_ids is not None and node.id not in active_node_ids:
|
|
266
|
+
continue
|
|
188
267
|
results[node.id] = WorkflowNodeRunResult(id=node.id, status="running")
|
|
189
268
|
try:
|
|
190
269
|
output = await run_node(
|
|
191
|
-
completion=completion,
|
|
192
270
|
connection=connection,
|
|
193
271
|
default_input=remaining_default_input,
|
|
194
|
-
input_values=input_values
|
|
272
|
+
input_values=input_values.input_values,
|
|
195
273
|
incoming_edges=incoming_edges[node.id],
|
|
196
274
|
node=node,
|
|
197
275
|
outputs=outputs,
|
|
276
|
+
runtime=runtime,
|
|
277
|
+
workflow_depth=workflow_depth,
|
|
198
278
|
)
|
|
199
279
|
if node.type == "input" and remaining_default_input:
|
|
200
280
|
remaining_default_input = ""
|
|
@@ -229,13 +309,14 @@ async def run_workflow_definition(
|
|
|
229
309
|
|
|
230
310
|
async def run_node(
|
|
231
311
|
*,
|
|
232
|
-
completion: CompletionCallable | None,
|
|
233
312
|
connection: ProviderConnection | None,
|
|
234
313
|
default_input: str,
|
|
235
314
|
input_values: Mapping[str, str],
|
|
236
315
|
incoming_edges: list[StoredWorkflowEdge],
|
|
237
316
|
node: StoredWorkflowNode,
|
|
238
317
|
outputs: Mapping[str, str],
|
|
318
|
+
runtime: FlowentAgentRuntime | None = None,
|
|
319
|
+
workflow_depth: int = 0,
|
|
239
320
|
) -> str:
|
|
240
321
|
if node.type == "input":
|
|
241
322
|
if node.id in input_values:
|
|
@@ -246,17 +327,25 @@ async def run_node(
|
|
|
246
327
|
if node.type == "agent":
|
|
247
328
|
if connection is None:
|
|
248
329
|
raise ValueError("Choose a provider and model before running.")
|
|
330
|
+
if runtime is None:
|
|
331
|
+
raise ValueError("Agent runtime is not available.")
|
|
249
332
|
prompt = render_template(
|
|
250
333
|
node_data_text(node, "prompt")
|
|
251
334
|
or joined_upstream_outputs(incoming_edges, outputs),
|
|
252
335
|
outputs,
|
|
253
336
|
)
|
|
254
|
-
|
|
255
|
-
connection,
|
|
256
|
-
[
|
|
257
|
-
|
|
337
|
+
result = await runtime.complete(
|
|
338
|
+
connection=connection,
|
|
339
|
+
messages=[
|
|
340
|
+
*runtime_context_messages(
|
|
341
|
+
runtime.cwd, runtime.store.read_state().settings.agent_prompt
|
|
342
|
+
),
|
|
343
|
+
{"role": "user", "content": prompt},
|
|
344
|
+
],
|
|
345
|
+
user_request=prompt,
|
|
346
|
+
workflow_depth=workflow_depth,
|
|
258
347
|
)
|
|
259
|
-
return
|
|
348
|
+
return result.content
|
|
260
349
|
if node.type == "merge":
|
|
261
350
|
upstream = upstream_outputs(incoming_edges, outputs)
|
|
262
351
|
if node_data_text(node, "merge_strategy") == "json":
|
|
@@ -264,6 +353,8 @@ async def run_node(
|
|
|
264
353
|
return "\n".join(output for output in upstream if output)
|
|
265
354
|
if node.type == "code":
|
|
266
355
|
return await run_code_node(node, upstream_outputs(incoming_edges, outputs))
|
|
356
|
+
if node.type == "timer":
|
|
357
|
+
return timer_payload(node)
|
|
267
358
|
if node.type == "output":
|
|
268
359
|
return joined_upstream_outputs(incoming_edges, outputs)
|
|
269
360
|
raise ValueError("Node type is not supported.")
|
|
@@ -313,6 +404,10 @@ def node_output_key(node: StoredWorkflowNode) -> str:
|
|
|
313
404
|
return node_data_text(node, "output_key") or node.id
|
|
314
405
|
|
|
315
406
|
|
|
407
|
+
def timer_payload(node: StoredWorkflowNode) -> str:
|
|
408
|
+
return node_data_text(node, "payload") or "Timer fired."
|
|
409
|
+
|
|
410
|
+
|
|
316
411
|
def upstream_outputs(
|
|
317
412
|
incoming_edges: list[StoredWorkflowEdge],
|
|
318
413
|
outputs: Mapping[str, str],
|
|
@@ -10,14 +10,13 @@ from uuid import uuid4
|
|
|
10
10
|
|
|
11
11
|
from fastapi import HTTPException
|
|
12
12
|
|
|
13
|
-
from flowent.agent import AgentContextUpdate
|
|
14
|
-
from flowent.
|
|
13
|
+
from flowent.agent import AgentContextUpdate
|
|
14
|
+
from flowent.agent_runtime import FlowentAgentRuntime
|
|
15
15
|
from flowent.compact import CompactInput, CompactProvider
|
|
16
16
|
from flowent.context import runtime_context_messages
|
|
17
17
|
from flowent.llm import ChatMessage, CompletionCallable, ProviderConnection
|
|
18
18
|
from flowent.logging import TRACE_LEVEL
|
|
19
19
|
from flowent.mcp import McpManager
|
|
20
|
-
from flowent.permissions import run_tool_with_path_permissions
|
|
21
20
|
from flowent.provider_connections import selected_connection
|
|
22
21
|
from flowent.skills import explicit_skill_messages
|
|
23
22
|
from flowent.storage import (
|
|
@@ -27,7 +26,7 @@ from flowent.storage import (
|
|
|
27
26
|
StoredState,
|
|
28
27
|
StoredToolItem,
|
|
29
28
|
)
|
|
30
|
-
from flowent.tools import
|
|
29
|
+
from flowent.tools import text_tool_result
|
|
31
30
|
from flowent.usage import (
|
|
32
31
|
TokenUsage,
|
|
33
32
|
TokenUsageInfo,
|
|
@@ -37,11 +36,6 @@ from flowent.usage import (
|
|
|
37
36
|
recompute_context_usage,
|
|
38
37
|
)
|
|
39
38
|
from flowent.workflow_service import WorkflowService
|
|
40
|
-
from flowent.workflow_tools import (
|
|
41
|
-
WorkflowAgentTools,
|
|
42
|
-
workflow_tool_specs,
|
|
43
|
-
workflow_tool_title,
|
|
44
|
-
)
|
|
45
39
|
from flowent.workspace.context import (
|
|
46
40
|
COMPACTED_CONTEXT_MARKER,
|
|
47
41
|
OPTIMIZED_CONTEXT_MARKER,
|
|
@@ -97,38 +91,27 @@ class WorkspaceRuntime:
|
|
|
97
91
|
self.chat_completion = chat_completion
|
|
98
92
|
self.compact_provider = compact_provider
|
|
99
93
|
self.cwd = cwd
|
|
100
|
-
self.mcp_manager = mcp_manager
|
|
101
94
|
self.store = store
|
|
102
95
|
self.workflow_service = workflow_service
|
|
96
|
+
self.agent_runtime = FlowentAgentRuntime(
|
|
97
|
+
chat_completion=chat_completion,
|
|
98
|
+
cwd=cwd,
|
|
99
|
+
mcp_manager=mcp_manager,
|
|
100
|
+
store=store,
|
|
101
|
+
workflow_service=workflow_service,
|
|
102
|
+
)
|
|
103
103
|
self.active_response: WorkspaceResponse | None = None
|
|
104
104
|
self.generation = 0
|
|
105
105
|
self.active_compact_task: WorkspaceCompactTask | None = None
|
|
106
106
|
|
|
107
107
|
def extra_tool_specs(self) -> list[Mapping[str, object]]:
|
|
108
|
-
return
|
|
109
|
-
*workflow_tool_specs(),
|
|
110
|
-
*list(self.mcp_manager.tool_specs()),
|
|
111
|
-
]
|
|
108
|
+
return self.agent_runtime.extra_tool_specs()
|
|
112
109
|
|
|
113
110
|
def model_tool_specs(self) -> list[Mapping[str, object]]:
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
def workflow_tools(self) -> WorkflowAgentTools:
|
|
117
|
-
return WorkflowAgentTools(self.workflow_service)
|
|
118
|
-
|
|
119
|
-
async def run_extra_tool(
|
|
120
|
-
self,
|
|
121
|
-
workflow_tools: WorkflowAgentTools,
|
|
122
|
-
name: str,
|
|
123
|
-
arguments: dict[str, object],
|
|
124
|
-
) -> ToolResult | None:
|
|
125
|
-
workflow_result = await workflow_tools.run_tool(name, arguments)
|
|
126
|
-
if workflow_result is not None:
|
|
127
|
-
return workflow_result
|
|
128
|
-
return await self.mcp_manager.run_tool(name, arguments)
|
|
111
|
+
return self.agent_runtime.model_tool_specs()
|
|
129
112
|
|
|
130
113
|
def extra_tool_title(self, name: str) -> str | None:
|
|
131
|
-
return
|
|
114
|
+
return self.agent_runtime.extra_tool_title(name)
|
|
132
115
|
|
|
133
116
|
def request_messages_for_content(
|
|
134
117
|
self,
|
|
@@ -263,7 +246,6 @@ class WorkspaceRuntime:
|
|
|
263
246
|
)
|
|
264
247
|
next_messages = [*state.messages, user_message]
|
|
265
248
|
self.store.save_messages(next_messages)
|
|
266
|
-
extra_tool_specs = self.extra_tool_specs()
|
|
267
249
|
model_tool_specs = self.model_tool_specs()
|
|
268
250
|
model_history: list[ChatMessage | Mapping[str, object]] = [
|
|
269
251
|
*runtime_context_messages(self.cwd, state.settings.agent_prompt),
|
|
@@ -297,49 +279,11 @@ class WorkspaceRuntime:
|
|
|
297
279
|
current_output_index = 0
|
|
298
280
|
latest_usage_output_index: int | None = None
|
|
299
281
|
|
|
300
|
-
async
|
|
301
|
-
|
|
302
|
-
connection,
|
|
303
|
-
request.model_copy(
|
|
304
|
-
update={
|
|
305
|
-
"transcript": approval_transcript(next_messages),
|
|
306
|
-
"user_request": content,
|
|
307
|
-
}
|
|
308
|
-
),
|
|
309
|
-
completion=self.chat_completion,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
async def tool_runner(
|
|
313
|
-
name: str,
|
|
314
|
-
arguments: dict[str, object],
|
|
315
|
-
context: ToolContext,
|
|
316
|
-
):
|
|
317
|
-
return await run_tool_with_path_permissions(
|
|
318
|
-
name,
|
|
319
|
-
arguments,
|
|
320
|
-
context,
|
|
321
|
-
review_approval=review_tool_approval,
|
|
322
|
-
writable_paths=[
|
|
323
|
-
Path(path.path) for path in self.store.read_writable_paths()
|
|
324
|
-
],
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
workflow_tools = self.workflow_tools()
|
|
328
|
-
|
|
329
|
-
async def extra_tool_runner(
|
|
330
|
-
name: str, arguments: dict[str, object]
|
|
331
|
-
) -> ToolResult | None:
|
|
332
|
-
return await self.run_extra_tool(workflow_tools, name, arguments)
|
|
333
|
-
|
|
334
|
-
async for event in run_agent_stream(
|
|
335
|
-
completion=self.chat_completion,
|
|
282
|
+
async for event in self.agent_runtime.stream(
|
|
283
|
+
approval_transcript=approval_transcript(next_messages),
|
|
336
284
|
connection=connection,
|
|
337
|
-
cwd=self.cwd,
|
|
338
|
-
extra_tool_runner=extra_tool_runner,
|
|
339
|
-
extra_tool_specs=extra_tool_specs,
|
|
340
|
-
extra_tool_title=self.extra_tool_title,
|
|
341
285
|
messages=request_messages,
|
|
342
|
-
|
|
286
|
+
user_request=content,
|
|
343
287
|
):
|
|
344
288
|
if event.event == "start":
|
|
345
289
|
event_id = event.data.get("id")
|
|
@@ -656,16 +600,37 @@ class WorkspaceRuntime:
|
|
|
656
600
|
*base_request_messages,
|
|
657
601
|
*model_visible_assistant_output_messages(trimmed_message),
|
|
658
602
|
]
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
603
|
+
try:
|
|
604
|
+
response = self._start_response_from_messages(
|
|
605
|
+
content=previous_user_message.content,
|
|
606
|
+
initial_assistant_message=trimmed_message,
|
|
607
|
+
next_messages=next_messages,
|
|
608
|
+
output_start_index=assistant_retry_output_start_index(trimmed_message),
|
|
609
|
+
request_messages=request_messages,
|
|
610
|
+
state=state_before_assistant,
|
|
611
|
+
usage_request_messages=base_request_messages,
|
|
612
|
+
user_message=previous_user_message,
|
|
613
|
+
)
|
|
614
|
+
except HTTPException as error:
|
|
615
|
+
error_detail = str(error.detail or "")
|
|
616
|
+
assistant_output = AssistantOutputBuilder.from_message(trimmed_message)
|
|
617
|
+
assistant_output.append_error(
|
|
618
|
+
run_error_output_item(trimmed_message.id, error_detail).model_copy(
|
|
619
|
+
update={"id": error_id}
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
failed_message = StoredMessage(
|
|
623
|
+
author="assistant",
|
|
624
|
+
content=assistant_output.content,
|
|
625
|
+
groups=assistant_output.groups,
|
|
626
|
+
id=trimmed_message.id,
|
|
627
|
+
status="failed",
|
|
628
|
+
thinking=assistant_output.thinking,
|
|
629
|
+
tools=list(assistant_output.tools.values()),
|
|
630
|
+
usage_info=self.store.read_usage_info(),
|
|
631
|
+
)
|
|
632
|
+
self.store.save_messages([*previous_messages, failed_message])
|
|
633
|
+
raise
|
|
669
634
|
return next_messages, response
|
|
670
635
|
|
|
671
636
|
def _start_response_from_messages(
|
|
@@ -770,7 +735,6 @@ class WorkspaceRuntime:
|
|
|
770
735
|
turn_usage_info: TokenUsageInfo | None = None
|
|
771
736
|
current_output_index = 0
|
|
772
737
|
latest_usage_output_index: int | None = None
|
|
773
|
-
extra_tool_specs = self.extra_tool_specs()
|
|
774
738
|
model_tool_specs = self.model_tool_specs()
|
|
775
739
|
if request_messages is None:
|
|
776
740
|
current_request_messages = self.request_messages_for_content(
|
|
@@ -853,33 +817,6 @@ class WorkspaceRuntime:
|
|
|
853
817
|
else current_request_messages
|
|
854
818
|
)
|
|
855
819
|
|
|
856
|
-
async def review_tool_approval(request: ApprovalReviewRequest):
|
|
857
|
-
return await review_approval_request(
|
|
858
|
-
connection,
|
|
859
|
-
request.model_copy(
|
|
860
|
-
update={
|
|
861
|
-
"transcript": approval_transcript(next_messages),
|
|
862
|
-
"user_request": content,
|
|
863
|
-
}
|
|
864
|
-
),
|
|
865
|
-
completion=self.chat_completion,
|
|
866
|
-
)
|
|
867
|
-
|
|
868
|
-
async def tool_runner(
|
|
869
|
-
name: str,
|
|
870
|
-
arguments: dict[str, object],
|
|
871
|
-
context: ToolContext,
|
|
872
|
-
):
|
|
873
|
-
return await run_tool_with_path_permissions(
|
|
874
|
-
name,
|
|
875
|
-
arguments,
|
|
876
|
-
context,
|
|
877
|
-
review_approval=review_tool_approval,
|
|
878
|
-
writable_paths=[
|
|
879
|
-
Path(path.path) for path in self.store.read_writable_paths()
|
|
880
|
-
],
|
|
881
|
-
)
|
|
882
|
-
|
|
883
820
|
async def context_compactor(
|
|
884
821
|
conversation: Sequence[Mapping[str, object]],
|
|
885
822
|
) -> AgentContextUpdate | None:
|
|
@@ -926,23 +863,12 @@ class WorkspaceRuntime:
|
|
|
926
863
|
},
|
|
927
864
|
)
|
|
928
865
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
async def extra_tool_runner(
|
|
932
|
-
name: str, arguments: dict[str, object]
|
|
933
|
-
) -> ToolResult | None:
|
|
934
|
-
return await self.run_extra_tool(workflow_tools, name, arguments)
|
|
935
|
-
|
|
936
|
-
async for event in run_agent_stream(
|
|
937
|
-
completion=self.chat_completion,
|
|
866
|
+
async for event in self.agent_runtime.stream(
|
|
867
|
+
approval_transcript=approval_transcript(next_messages),
|
|
938
868
|
connection=connection,
|
|
939
869
|
context_compactor=context_compactor,
|
|
940
|
-
cwd=self.cwd,
|
|
941
|
-
extra_tool_runner=extra_tool_runner,
|
|
942
|
-
extra_tool_specs=extra_tool_specs,
|
|
943
|
-
extra_tool_title=self.extra_tool_title,
|
|
944
870
|
messages=current_request_messages,
|
|
945
|
-
|
|
871
|
+
user_request=content,
|
|
946
872
|
):
|
|
947
873
|
if not is_current_generation() or response.discard_on_cancel:
|
|
948
874
|
raise asyncio.CancelledError
|