flowent 0.3.1 → 0.3.3
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/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +82 -16
- package/backend/src/flowent/app.py +7 -2
- package/backend/src/flowent/mcp.py +4 -3
- package/backend/src/flowent/permissions.py +61 -39
- package/backend/src/flowent/routes/workflow_routes.py +9 -41
- package/backend/src/flowent/sandbox.py +63 -19
- package/backend/src/flowent/state/models.py +2 -3
- package/backend/src/flowent/state/schema.py +116 -0
- package/backend/src/flowent/static/assets/index-CCf0mo80.css +2 -0
- package/backend/src/flowent/static/assets/index-CROofCFl.js +102 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/tools.py +142 -35
- package/backend/src/flowent/usage.py +66 -0
- package/backend/src/flowent/workflow_service.py +93 -0
- package/backend/src/flowent/workflow_tools.py +271 -0
- package/backend/src/flowent/workflows.py +71 -3
- package/backend/src/flowent/workspace/context.py +14 -7
- package/backend/src/flowent/workspace/output.py +4 -1
- package/backend/src/flowent/workspace/runtime.py +164 -13
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-CCf0mo80.css +2 -0
- package/dist/frontend/assets/index-CROofCFl.js +102 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +8 -10
- package/backend/src/flowent/static/assets/index-BaZmIi2Y.js +0 -98
- package/backend/src/flowent/static/assets/index-EC37agAH.css +0 -2
- package/dist/frontend/assets/index-BaZmIi2Y.js +0 -98
- package/dist/frontend/assets/index-EC37agAH.css +0 -2
package/backend/pyproject.toml
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
7
9
|
from uuid import uuid4
|
|
8
10
|
|
|
9
11
|
from pydantic import BaseModel, ConfigDict
|
|
@@ -25,6 +27,8 @@ from flowent.tools import (
|
|
|
25
27
|
new_tool_item,
|
|
26
28
|
parse_tool_arguments,
|
|
27
29
|
run_tool_async,
|
|
30
|
+
text_tool_result,
|
|
31
|
+
tool_result_model_content,
|
|
28
32
|
tool_specs,
|
|
29
33
|
)
|
|
30
34
|
|
|
@@ -42,6 +46,10 @@ Use tools deliberately:
|
|
|
42
46
|
- Apply structured patches for file edits.
|
|
43
47
|
- Run shell commands for diagnostics, builds, tests, and operations that require the local environment.
|
|
44
48
|
- When a shell command needs to write outside the current workspace, declare each needed writable directory with sandbox_permissions set to with_additional_permissions and additional_permissions.file_system.write. Flowent reviews elevated permissions automatically, so keep the requested paths specific and tied to the task.
|
|
49
|
+
- Use workflow tools when the user asks to view, inspect, run, create, or modify saved workflows. List workflows first when you need the workflow id. Read a workflow before modifying it.
|
|
50
|
+
- When running a workflow and the user's current message contains the content to process, pass that content as the run_workflow input. Use inputs only when you need to target specific input node ids.
|
|
51
|
+
- When creating or updating a workflow, save a complete workflow object with valid node ids and edges. If saving fails, use the validation error as context and explain what needs to change.
|
|
52
|
+
- Do not delete workflows. If the user asks to delete a workflow, say that you cannot do that directly.
|
|
45
53
|
- Search the web only when current external information is needed.
|
|
46
54
|
- Update the plan when a task has multiple meaningful steps.
|
|
47
55
|
|
|
@@ -277,7 +285,12 @@ async def run_agent_stream(
|
|
|
277
285
|
arguments = parse_tool_arguments(tool_call.arguments)
|
|
278
286
|
except Exception as error:
|
|
279
287
|
arguments = {}
|
|
280
|
-
|
|
288
|
+
result = ToolResult(
|
|
289
|
+
result=text_tool_result(str(error)),
|
|
290
|
+
ok=False,
|
|
291
|
+
title=tool_call.name or "Tool failed",
|
|
292
|
+
)
|
|
293
|
+
result_content = tool_result_model_content(result)
|
|
281
294
|
tool_item = new_tool_item(tool_call.name, arguments)
|
|
282
295
|
logger.debug("Tool call argument parse failed name=%s", tool_call.name)
|
|
283
296
|
logger.log(TRACE_LEVEL, "Tool start item=%r", tool_item)
|
|
@@ -292,10 +305,9 @@ async def run_agent_stream(
|
|
|
292
305
|
event="tool_error",
|
|
293
306
|
data={
|
|
294
307
|
"id": tool_item["id"],
|
|
295
|
-
"
|
|
296
|
-
"data": {},
|
|
308
|
+
"result": result.result,
|
|
297
309
|
"status": "failed",
|
|
298
|
-
"title":
|
|
310
|
+
"title": result.title,
|
|
299
311
|
},
|
|
300
312
|
)
|
|
301
313
|
else:
|
|
@@ -309,15 +321,35 @@ async def run_agent_stream(
|
|
|
309
321
|
)
|
|
310
322
|
logger.log(TRACE_LEVEL, "Tool start item=%r", tool_item)
|
|
311
323
|
yield AgentStreamEvent(event="tool_start", data={"tool": tool_item})
|
|
324
|
+
tool_event_queue: asyncio.Queue[dict[str, object]] = asyncio.Queue()
|
|
325
|
+
|
|
326
|
+
async def emit_tool_event(
|
|
327
|
+
data: dict[str, object],
|
|
328
|
+
*,
|
|
329
|
+
queue: asyncio.Queue[dict[str, object]] = tool_event_queue,
|
|
330
|
+
tool_id: str = str(tool_item["id"]),
|
|
331
|
+
) -> None:
|
|
332
|
+
yield_data = {
|
|
333
|
+
"id": tool_id,
|
|
334
|
+
**data,
|
|
335
|
+
}
|
|
336
|
+
await queue.put(yield_data)
|
|
337
|
+
|
|
312
338
|
extra_result = (
|
|
313
339
|
await extra_tool_runner(tool_call.name, arguments)
|
|
314
340
|
if extra_tool_runner is not None
|
|
315
341
|
else None
|
|
316
342
|
)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
343
|
+
tool_result: ToolResult | None = (
|
|
344
|
+
extra_result if isinstance(extra_result, ToolResult) else None
|
|
345
|
+
)
|
|
346
|
+
if tool_result is None:
|
|
347
|
+
context = ToolContext(
|
|
348
|
+
cwd=cwd,
|
|
349
|
+
emit_event=emit_tool_event,
|
|
350
|
+
web_searcher=web_searcher,
|
|
351
|
+
)
|
|
352
|
+
tool_task: asyncio.Future[ToolResult] = asyncio.ensure_future(
|
|
321
353
|
tool_runner(
|
|
322
354
|
tool_call.name,
|
|
323
355
|
arguments,
|
|
@@ -330,27 +362,61 @@ async def run_agent_stream(
|
|
|
330
362
|
context,
|
|
331
363
|
)
|
|
332
364
|
)
|
|
333
|
-
|
|
365
|
+
pending_event_task: asyncio.Future[dict[str, object]] | None = None
|
|
366
|
+
try:
|
|
367
|
+
while True:
|
|
368
|
+
if pending_event_task is None:
|
|
369
|
+
pending_event_task = asyncio.create_task(
|
|
370
|
+
tool_event_queue.get()
|
|
371
|
+
)
|
|
372
|
+
done, _ = await asyncio.wait(
|
|
373
|
+
{
|
|
374
|
+
cast(asyncio.Future[object], tool_task),
|
|
375
|
+
cast(asyncio.Future[object], pending_event_task),
|
|
376
|
+
},
|
|
377
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
378
|
+
)
|
|
379
|
+
if pending_event_task in done:
|
|
380
|
+
yield AgentStreamEvent(
|
|
381
|
+
event="tool_update",
|
|
382
|
+
data=pending_event_task.result(),
|
|
383
|
+
)
|
|
384
|
+
pending_event_task = None
|
|
385
|
+
if tool_task in done:
|
|
386
|
+
if pending_event_task is not None:
|
|
387
|
+
pending_event_task.cancel()
|
|
388
|
+
break
|
|
389
|
+
except asyncio.CancelledError:
|
|
390
|
+
tool_task.cancel()
|
|
391
|
+
if pending_event_task is not None:
|
|
392
|
+
pending_event_task.cancel()
|
|
393
|
+
raise
|
|
394
|
+
tool_result = await tool_task
|
|
395
|
+
while not tool_event_queue.empty():
|
|
396
|
+
yield AgentStreamEvent(
|
|
397
|
+
event="tool_update",
|
|
398
|
+
data=tool_event_queue.get_nowait(),
|
|
399
|
+
)
|
|
400
|
+
result_content = tool_result_model_content(tool_result)
|
|
334
401
|
logger.debug(
|
|
335
402
|
"Tool call finished name=%s id=%s ok=%s",
|
|
336
403
|
tool_call.name,
|
|
337
404
|
tool_item["id"],
|
|
338
|
-
|
|
405
|
+
tool_result.ok,
|
|
339
406
|
)
|
|
340
407
|
logger.log(
|
|
341
408
|
TRACE_LEVEL,
|
|
342
409
|
"Tool result id=%s result=%r",
|
|
343
410
|
tool_item["id"],
|
|
344
|
-
|
|
411
|
+
tool_result.model_dump(),
|
|
345
412
|
)
|
|
346
413
|
yield AgentStreamEvent(
|
|
347
|
-
event="tool_done" if
|
|
414
|
+
event="tool_done" if tool_result.ok else "tool_error",
|
|
348
415
|
data={
|
|
349
416
|
"id": tool_item["id"],
|
|
350
|
-
"
|
|
351
|
-
"
|
|
352
|
-
"
|
|
353
|
-
"title": result.title,
|
|
417
|
+
"result": tool_result.result,
|
|
418
|
+
"status": "success" if tool_result.ok else "failed",
|
|
419
|
+
"title": tool_result.title,
|
|
354
420
|
},
|
|
355
421
|
)
|
|
356
422
|
conversation.append(tool_result_message(tool_call_id, result_content))
|
|
@@ -23,6 +23,7 @@ from flowent.routes.workspace import register_workspace_routes
|
|
|
23
23
|
from flowent.sandbox import ensure_sandbox_available
|
|
24
24
|
from flowent.storage import StateStore
|
|
25
25
|
from flowent.system_tools import ensure_ripgrep_available
|
|
26
|
+
from flowent.workflow_service import WorkflowService
|
|
26
27
|
from flowent.workspace.runtime import WorkspaceRuntime
|
|
27
28
|
|
|
28
29
|
logger = logging.getLogger("flowent.app")
|
|
@@ -57,6 +58,10 @@ def create_app(
|
|
|
57
58
|
store = StateStore()
|
|
58
59
|
compact_provider = LocalSummaryCompactProvider()
|
|
59
60
|
mcp_manager = McpManager(store=store, transport=mcp_transport)
|
|
61
|
+
workflow_service = WorkflowService(
|
|
62
|
+
chat_completion=chat_completion,
|
|
63
|
+
store=store,
|
|
64
|
+
)
|
|
60
65
|
|
|
61
66
|
static_dir = frontend_static_directory().resolve(strict=False)
|
|
62
67
|
logger.debug("Flowent app created serve_frontend=%s", serve_frontend)
|
|
@@ -69,6 +74,7 @@ def create_app(
|
|
|
69
74
|
cwd=cwd,
|
|
70
75
|
mcp_manager=mcp_manager,
|
|
71
76
|
store=store,
|
|
77
|
+
workflow_service=workflow_service,
|
|
72
78
|
)
|
|
73
79
|
|
|
74
80
|
telegram_bot_manager = TelegramBotManager(
|
|
@@ -121,8 +127,7 @@ def create_app(
|
|
|
121
127
|
)
|
|
122
128
|
register_workflow_routes(
|
|
123
129
|
app,
|
|
124
|
-
|
|
125
|
-
store=store,
|
|
130
|
+
workflow_service=workflow_service,
|
|
126
131
|
)
|
|
127
132
|
register_permission_routes(app, cwd=cwd, store=store)
|
|
128
133
|
register_workspace_routes(app, runtime=runtime, store=store)
|
|
@@ -662,11 +662,12 @@ class McpManager:
|
|
|
662
662
|
content = mcp_result_content(result)
|
|
663
663
|
server_name = self._server_names.get(server_id, server_id)
|
|
664
664
|
return ToolResult(
|
|
665
|
-
|
|
666
|
-
|
|
665
|
+
result={
|
|
666
|
+
"type": "mcp",
|
|
667
|
+
"output": content,
|
|
667
668
|
"server": server_name,
|
|
668
669
|
"tool": tool_name,
|
|
669
|
-
"
|
|
670
|
+
"raw_result": result,
|
|
670
671
|
},
|
|
671
672
|
ok=not mcp_result_is_error(result),
|
|
672
673
|
title=f"Calling {server_name}.{tool_name}",
|
|
@@ -14,12 +14,16 @@ from flowent.patch import affected_paths
|
|
|
14
14
|
from flowent.sandbox import SandboxError, SandboxRunner, path_is_within
|
|
15
15
|
from flowent.shell import shell_invocation
|
|
16
16
|
from flowent.tools import (
|
|
17
|
+
CommandOutputCollector,
|
|
17
18
|
ToolContext,
|
|
18
19
|
ToolResult,
|
|
20
|
+
command_tool_result,
|
|
19
21
|
number_argument,
|
|
20
22
|
patch_title_from_result,
|
|
21
23
|
run_tool_async,
|
|
24
|
+
text_tool_result,
|
|
22
25
|
tool_failure_content,
|
|
26
|
+
tool_result_model_content,
|
|
23
27
|
)
|
|
24
28
|
|
|
25
29
|
SANDBOX_WITH_ADDITIONAL_PERMISSIONS = "with_additional_permissions"
|
|
@@ -66,7 +70,7 @@ def validate_additional_permissions(arguments: dict[str, object]) -> ToolResult
|
|
|
66
70
|
return None
|
|
67
71
|
if sandbox_permissions != SANDBOX_WITH_ADDITIONAL_PERMISSIONS:
|
|
68
72
|
return ToolResult(
|
|
69
|
-
|
|
73
|
+
result=text_tool_result(
|
|
70
74
|
"additional_permissions requires sandbox_permissions to be "
|
|
71
75
|
"with_additional_permissions."
|
|
72
76
|
),
|
|
@@ -130,8 +134,10 @@ async def review_missing_write_paths(
|
|
|
130
134
|
return (
|
|
131
135
|
effective_paths,
|
|
132
136
|
ToolResult(
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
result=text_tool_result(
|
|
138
|
+
approval_denial_content(decision),
|
|
139
|
+
**review_data,
|
|
140
|
+
),
|
|
135
141
|
ok=False,
|
|
136
142
|
title="Denied by reviewer",
|
|
137
143
|
),
|
|
@@ -196,7 +202,7 @@ async def run_shell_command_with_permissions(
|
|
|
196
202
|
arguments, context, effective_paths
|
|
197
203
|
)
|
|
198
204
|
if approval_data is not None:
|
|
199
|
-
result =
|
|
205
|
+
result = tool_result_with_fields(result, approval_data)
|
|
200
206
|
if result.ok or not is_likely_sandbox_denied_result(result):
|
|
201
207
|
return result
|
|
202
208
|
review_request = ApprovalReviewRequest(
|
|
@@ -210,13 +216,16 @@ async def run_shell_command_with_permissions(
|
|
|
210
216
|
review_data = approval_result_data(review_request, decision)
|
|
211
217
|
if decision.decision == "denied":
|
|
212
218
|
return ToolResult(
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
result=text_tool_result(
|
|
220
|
+
approval_denial_content(decision),
|
|
221
|
+
previous_result=result.result,
|
|
222
|
+
**review_data,
|
|
223
|
+
),
|
|
215
224
|
ok=False,
|
|
216
225
|
title="Denied by reviewer",
|
|
217
226
|
)
|
|
218
227
|
retry_result = await shell_command_without_sandbox(arguments, context)
|
|
219
|
-
return
|
|
228
|
+
return tool_result_with_fields(retry_result, review_data)
|
|
220
229
|
|
|
221
230
|
|
|
222
231
|
async def run_apply_patch_with_permissions(
|
|
@@ -244,7 +253,7 @@ async def run_apply_patch_with_permissions(
|
|
|
244
253
|
|
|
245
254
|
result = await apply_patch_with_writable_paths(arguments, context, effective_paths)
|
|
246
255
|
if approval_data is not None:
|
|
247
|
-
result =
|
|
256
|
+
result = tool_result_with_fields(result, approval_data)
|
|
248
257
|
return result
|
|
249
258
|
|
|
250
259
|
|
|
@@ -268,18 +277,23 @@ async def apply_patch_with_writable_paths(
|
|
|
268
277
|
input_text=patch,
|
|
269
278
|
)
|
|
270
279
|
except SandboxError as error:
|
|
271
|
-
return ToolResult(
|
|
280
|
+
return ToolResult(
|
|
281
|
+
result=text_tool_result(str(error)), ok=False, title="Edit failed"
|
|
282
|
+
)
|
|
272
283
|
|
|
273
284
|
if result.exit_code != 0:
|
|
274
285
|
return ToolResult(
|
|
275
|
-
|
|
286
|
+
result=text_tool_result(tool_failure_content(result)),
|
|
276
287
|
ok=False,
|
|
277
288
|
title="Edit failed",
|
|
278
289
|
)
|
|
279
290
|
data = json.loads(result.stdout or "{}")
|
|
280
291
|
return ToolResult(
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
result={
|
|
293
|
+
"type": "patch",
|
|
294
|
+
"output": result.stdout,
|
|
295
|
+
**(data if isinstance(data, dict) else {}),
|
|
296
|
+
},
|
|
283
297
|
title=patch_title_from_result(data),
|
|
284
298
|
)
|
|
285
299
|
|
|
@@ -292,32 +306,38 @@ async def shell_command_with_writable_paths(
|
|
|
292
306
|
command = str(arguments["command"])
|
|
293
307
|
timeout_seconds = number_argument(arguments, "timeout_seconds", 30)
|
|
294
308
|
invocation = shell_invocation(command)
|
|
309
|
+
collector = CommandOutputCollector(command, context.emit_event)
|
|
295
310
|
result = await SandboxRunner(
|
|
296
311
|
cwd=context.cwd,
|
|
297
312
|
writable_roots=writable_paths,
|
|
298
|
-
).run_async(
|
|
313
|
+
).run_async(
|
|
314
|
+
invocation.args,
|
|
315
|
+
env=invocation.env,
|
|
316
|
+
on_stderr=collector.append_stderr,
|
|
317
|
+
on_stdout=collector.append_stdout,
|
|
318
|
+
timeout_seconds=timeout_seconds,
|
|
319
|
+
)
|
|
299
320
|
ok = result.exit_code == 0
|
|
300
|
-
content = result.stdout or result.stderr
|
|
301
321
|
return ToolResult(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
322
|
+
result=command_tool_result(
|
|
323
|
+
command=command,
|
|
324
|
+
exit_code=result.exit_code,
|
|
325
|
+
output_chunks=collector.output_chunks,
|
|
326
|
+
stderr=result.stderr or collector.stderr,
|
|
327
|
+
stdout=result.stdout or collector.stdout,
|
|
328
|
+
),
|
|
309
329
|
ok=ok,
|
|
310
330
|
title=f"Ran {command}",
|
|
311
331
|
)
|
|
312
332
|
|
|
313
333
|
|
|
314
334
|
def is_likely_sandbox_denied_result(result: ToolResult) -> bool:
|
|
315
|
-
|
|
316
|
-
exit_code = int_result_field(
|
|
335
|
+
payload = result.result
|
|
336
|
+
exit_code = int_result_field(payload.get("exit_code"))
|
|
317
337
|
if exit_code == 0:
|
|
318
338
|
return False
|
|
319
339
|
output = "\n".join(
|
|
320
|
-
str(
|
|
340
|
+
str(payload.get(name, "") or "") for name in ["stderr", "stdout", "output"]
|
|
321
341
|
).lower()
|
|
322
342
|
return any(
|
|
323
343
|
keyword in output
|
|
@@ -345,13 +365,17 @@ def int_result_field(value: object) -> int:
|
|
|
345
365
|
|
|
346
366
|
|
|
347
367
|
def tool_failure_text(result: ToolResult) -> str:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
368
|
+
payload = result.result
|
|
369
|
+
stderr = str(payload.get("stderr", "") or "").strip()
|
|
370
|
+
stdout = str(payload.get("stdout", "") or "").strip()
|
|
351
371
|
parts: list[str] = []
|
|
352
|
-
for part in [stderr, stdout
|
|
372
|
+
for part in [stderr, stdout]:
|
|
353
373
|
if part and part not in parts:
|
|
354
374
|
parts.append(part)
|
|
375
|
+
if not parts:
|
|
376
|
+
content = tool_result_model_content(result).strip()
|
|
377
|
+
if content:
|
|
378
|
+
parts.append(content)
|
|
355
379
|
return "\n".join(parts)
|
|
356
380
|
|
|
357
381
|
|
|
@@ -366,15 +390,13 @@ async def shell_command_without_sandbox(
|
|
|
366
390
|
invocation.args, env=invocation.env, timeout_seconds=timeout_seconds
|
|
367
391
|
)
|
|
368
392
|
ok = result.exit_code == 0
|
|
369
|
-
content = result.stdout or result.stderr
|
|
370
393
|
return ToolResult(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
},
|
|
394
|
+
result=command_tool_result(
|
|
395
|
+
command=command,
|
|
396
|
+
exit_code=result.exit_code,
|
|
397
|
+
stderr=result.stderr,
|
|
398
|
+
stdout=result.stdout,
|
|
399
|
+
),
|
|
378
400
|
ok=ok,
|
|
379
401
|
title=f"Ran {command}",
|
|
380
402
|
)
|
|
@@ -403,7 +425,7 @@ def approval_result_data(
|
|
|
403
425
|
}
|
|
404
426
|
|
|
405
427
|
|
|
406
|
-
def
|
|
407
|
-
result: ToolResult,
|
|
428
|
+
def tool_result_with_fields(
|
|
429
|
+
result: ToolResult, extra_fields: dict[str, object]
|
|
408
430
|
) -> ToolResult:
|
|
409
|
-
return result.model_copy(update={"
|
|
431
|
+
return result.model_copy(update={"result": {**result.result, **extra_fields}})
|
|
@@ -1,63 +1,31 @@
|
|
|
1
1
|
from fastapi import FastAPI, HTTPException
|
|
2
2
|
|
|
3
|
-
from flowent.
|
|
4
|
-
from flowent.
|
|
5
|
-
from flowent.
|
|
6
|
-
from flowent.workflows import (
|
|
7
|
-
WorkflowRunResponse,
|
|
8
|
-
run_workflow_definition,
|
|
9
|
-
validate_workflow_draft,
|
|
10
|
-
workflow_requires_connection,
|
|
11
|
-
)
|
|
3
|
+
from flowent.storage import StoredWorkflow
|
|
4
|
+
from flowent.workflow_service import WorkflowService
|
|
5
|
+
from flowent.workflows import WorkflowRunResponse
|
|
12
6
|
|
|
13
7
|
|
|
14
8
|
def register_workflow_routes(
|
|
15
9
|
app: FastAPI,
|
|
16
10
|
*,
|
|
17
|
-
|
|
18
|
-
store: StateStore,
|
|
11
|
+
workflow_service: WorkflowService,
|
|
19
12
|
) -> None:
|
|
20
13
|
@app.put("/api/workflows")
|
|
21
14
|
async def save_workflow(workflow: StoredWorkflow) -> StoredWorkflow:
|
|
22
15
|
try:
|
|
23
|
-
return
|
|
24
|
-
validate_workflow_draft(
|
|
25
|
-
workflow.model_copy(
|
|
26
|
-
update={"name": workflow.name.strip() or "Untitled Workflow"}
|
|
27
|
-
)
|
|
28
|
-
)
|
|
29
|
-
)
|
|
16
|
+
return workflow_service.save_workflow(workflow)
|
|
30
17
|
except ValueError as error:
|
|
31
18
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
32
19
|
|
|
33
20
|
@app.delete("/api/workflows/{workflow_id}")
|
|
34
21
|
async def delete_workflow(workflow_id: str) -> dict[str, bool]:
|
|
35
|
-
store.delete_workflow(workflow_id)
|
|
22
|
+
workflow_service.store.delete_workflow(workflow_id)
|
|
36
23
|
return {"ok": True}
|
|
37
24
|
|
|
38
25
|
@app.post("/api/workflows/{workflow_id}/run")
|
|
39
26
|
async def run_workflow(workflow_id: str) -> WorkflowRunResponse:
|
|
40
|
-
workflow = next(
|
|
41
|
-
(
|
|
42
|
-
current_workflow
|
|
43
|
-
for current_workflow in store.read_workflows()
|
|
44
|
-
if current_workflow.id == workflow_id
|
|
45
|
-
),
|
|
46
|
-
None,
|
|
47
|
-
)
|
|
48
|
-
if workflow is None:
|
|
49
|
-
raise HTTPException(status_code=404, detail="Workflow not found.")
|
|
50
27
|
try:
|
|
51
|
-
|
|
52
|
-
selected_connection(store.read_state())
|
|
53
|
-
if workflow_requires_connection(workflow.definition)
|
|
54
|
-
else None
|
|
55
|
-
)
|
|
56
|
-
return await run_workflow_definition(
|
|
57
|
-
completion=chat_completion,
|
|
58
|
-
connection=connection,
|
|
59
|
-
definition=workflow.definition,
|
|
60
|
-
workflow_id=workflow.id,
|
|
61
|
-
)
|
|
28
|
+
return await workflow_service.run_workflow(workflow_id)
|
|
62
29
|
except ValueError as error:
|
|
63
|
-
|
|
30
|
+
status_code = 404 if str(error) == "Workflow not found." else 400
|
|
31
|
+
raise HTTPException(status_code=status_code, detail=str(error)) from error
|