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
|
@@ -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-CROofCFl.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CCf0mo80.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -6,12 +6,12 @@ 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
|
|
13
13
|
|
|
14
|
-
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
15
|
|
|
16
16
|
from flowent.network import flowent_user_agent
|
|
17
17
|
from flowent.patch import affected_paths
|
|
@@ -23,18 +23,107 @@ from flowent.system_tools import ensure_ripgrep_available
|
|
|
23
23
|
class ToolResult(BaseModel):
|
|
24
24
|
model_config = ConfigDict(extra="forbid")
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
data: dict[str, object] = {}
|
|
26
|
+
result: dict[str, object] = Field(default_factory=dict)
|
|
28
27
|
ok: bool = True
|
|
29
28
|
title: str
|
|
30
29
|
|
|
31
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
|
+
|
|
32
78
|
@dataclass(frozen=True)
|
|
33
79
|
class ToolContext:
|
|
34
80
|
cwd: Path
|
|
81
|
+
emit_event: ToolEventEmitter | None = None
|
|
35
82
|
web_searcher: Callable[[str], Sequence[dict[str, str]]] | None = None
|
|
36
83
|
|
|
37
84
|
|
|
85
|
+
def text_tool_result(text: str, **metadata: object) -> dict[str, object]:
|
|
86
|
+
return {"type": "text", "text": text, **metadata}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def command_tool_result(
|
|
90
|
+
*,
|
|
91
|
+
command: str,
|
|
92
|
+
exit_code: int,
|
|
93
|
+
output_chunks: list[dict[str, str]] | None = None,
|
|
94
|
+
stderr: str,
|
|
95
|
+
stdout: str,
|
|
96
|
+
) -> dict[str, object]:
|
|
97
|
+
return {
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": command,
|
|
100
|
+
"exit_code": exit_code,
|
|
101
|
+
"output_chunks": [dict(item) for item in output_chunks or []],
|
|
102
|
+
"stderr": stderr,
|
|
103
|
+
"stdout": stdout,
|
|
104
|
+
"output": stdout or stderr,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def tool_result_model_content(result: ToolResult | dict[str, object]) -> str:
|
|
109
|
+
payload = result.result if isinstance(result, ToolResult) else result
|
|
110
|
+
result_type = payload.get("type")
|
|
111
|
+
if result_type == "command":
|
|
112
|
+
output = str(payload.get("output") or "")
|
|
113
|
+
metadata: dict[str, object] = {}
|
|
114
|
+
if "exit_code" in payload:
|
|
115
|
+
metadata["exit_code"] = payload["exit_code"]
|
|
116
|
+
return json.dumps(
|
|
117
|
+
{"output": output, "metadata": metadata},
|
|
118
|
+
ensure_ascii=False,
|
|
119
|
+
)
|
|
120
|
+
for key in ("text", "output"):
|
|
121
|
+
value = payload.get(key)
|
|
122
|
+
if value is not None:
|
|
123
|
+
return str(value)
|
|
124
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
125
|
+
|
|
126
|
+
|
|
38
127
|
def tool_specs() -> list[dict[str, object]]:
|
|
39
128
|
return [
|
|
40
129
|
{
|
|
@@ -222,7 +311,7 @@ def run_tool(
|
|
|
222
311
|
title = (
|
|
223
312
|
"Edit failed" if name == "apply_patch" else tool_call_title(name, arguments)
|
|
224
313
|
)
|
|
225
|
-
return ToolResult(
|
|
314
|
+
return ToolResult(result=text_tool_result(str(error)), ok=False, title=title)
|
|
226
315
|
|
|
227
316
|
|
|
228
317
|
async def run_tool_async(
|
|
@@ -238,7 +327,7 @@ async def run_tool_async(
|
|
|
238
327
|
title = (
|
|
239
328
|
"Edit failed" if name == "apply_patch" else tool_call_title(name, arguments)
|
|
240
329
|
)
|
|
241
|
-
return ToolResult(
|
|
330
|
+
return ToolResult(result=text_tool_result(str(error)), ok=False, title=title)
|
|
242
331
|
|
|
243
332
|
|
|
244
333
|
def integer_argument(arguments: dict[str, object], name: str, default: int) -> int:
|
|
@@ -266,7 +355,10 @@ def read_file(arguments: dict[str, object], context: ToolContext) -> ToolResult:
|
|
|
266
355
|
lines = path.read_text(errors="replace").splitlines()
|
|
267
356
|
selected = lines[offset : offset + limit]
|
|
268
357
|
content = "\n".join(selected)
|
|
269
|
-
return ToolResult(
|
|
358
|
+
return ToolResult(
|
|
359
|
+
result=text_tool_result(content, path=str(path)),
|
|
360
|
+
title=f"Read {path}",
|
|
361
|
+
)
|
|
270
362
|
|
|
271
363
|
|
|
272
364
|
def list_dir(arguments: dict[str, object], context: ToolContext) -> ToolResult:
|
|
@@ -279,7 +371,8 @@ def list_dir(arguments: dict[str, object], context: ToolContext) -> ToolResult:
|
|
|
279
371
|
f"{entry.name}/" if entry.is_dir() else entry.name for entry in entries[:limit]
|
|
280
372
|
]
|
|
281
373
|
return ToolResult(
|
|
282
|
-
|
|
374
|
+
result=text_tool_result("\n".join(rendered), path=str(path)),
|
|
375
|
+
title=f"Listed {path}",
|
|
283
376
|
)
|
|
284
377
|
|
|
285
378
|
|
|
@@ -297,8 +390,7 @@ def grep_files(arguments: dict[str, object], context: ToolContext) -> ToolResult
|
|
|
297
390
|
)
|
|
298
391
|
output = completed.stdout or completed.stderr
|
|
299
392
|
return ToolResult(
|
|
300
|
-
|
|
301
|
-
data={"path": str(path), "pattern": pattern},
|
|
393
|
+
result=text_tool_result(output[:20000], path=str(path), pattern=pattern),
|
|
302
394
|
title=f"Searched {pattern}",
|
|
303
395
|
)
|
|
304
396
|
|
|
@@ -317,8 +409,11 @@ def apply_patch_tool(arguments: dict[str, object], context: ToolContext) -> Tool
|
|
|
317
409
|
raise SandboxError(tool_failure_content(result))
|
|
318
410
|
data = json.loads(result.stdout or "{}")
|
|
319
411
|
return ToolResult(
|
|
320
|
-
|
|
321
|
-
|
|
412
|
+
result={
|
|
413
|
+
"type": "patch",
|
|
414
|
+
"output": result.stdout,
|
|
415
|
+
**(data if isinstance(data, dict) else {}),
|
|
416
|
+
},
|
|
322
417
|
title=patch_title_from_result(data),
|
|
323
418
|
)
|
|
324
419
|
|
|
@@ -339,8 +434,11 @@ async def apply_patch_tool_async(
|
|
|
339
434
|
raise SandboxError(tool_failure_content(result))
|
|
340
435
|
data = json.loads(result.stdout or "{}")
|
|
341
436
|
return ToolResult(
|
|
342
|
-
|
|
343
|
-
|
|
437
|
+
result={
|
|
438
|
+
"type": "patch",
|
|
439
|
+
"output": result.stdout,
|
|
440
|
+
**(data if isinstance(data, dict) else {}),
|
|
441
|
+
},
|
|
344
442
|
title=patch_title_from_result(data),
|
|
345
443
|
)
|
|
346
444
|
|
|
@@ -388,15 +486,13 @@ def shell_command(arguments: dict[str, object], context: ToolContext) -> ToolRes
|
|
|
388
486
|
invocation.args, env=invocation.env, timeout_seconds=timeout_seconds
|
|
389
487
|
)
|
|
390
488
|
ok = result.exit_code == 0
|
|
391
|
-
content = result.stdout or result.stderr
|
|
392
489
|
return ToolResult(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
},
|
|
490
|
+
result=command_tool_result(
|
|
491
|
+
command=command,
|
|
492
|
+
exit_code=result.exit_code,
|
|
493
|
+
stderr=result.stderr,
|
|
494
|
+
stdout=result.stdout,
|
|
495
|
+
),
|
|
400
496
|
ok=ok,
|
|
401
497
|
title=f"Ran {command}",
|
|
402
498
|
)
|
|
@@ -408,19 +504,23 @@ async def shell_command_async(
|
|
|
408
504
|
command = str(arguments["command"])
|
|
409
505
|
timeout_seconds = number_argument(arguments, "timeout_seconds", 30)
|
|
410
506
|
invocation = shell_invocation(command)
|
|
507
|
+
collector = CommandOutputCollector(command, context.emit_event)
|
|
411
508
|
result = await SandboxRunner(cwd=context.cwd).run_async(
|
|
412
|
-
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,
|
|
413
514
|
)
|
|
414
515
|
ok = result.exit_code == 0
|
|
415
|
-
content = result.stdout or result.stderr
|
|
416
516
|
return ToolResult(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
517
|
+
result=command_tool_result(
|
|
518
|
+
command=command,
|
|
519
|
+
exit_code=result.exit_code,
|
|
520
|
+
output_chunks=collector.output_chunks,
|
|
521
|
+
stderr=result.stderr or collector.stderr,
|
|
522
|
+
stdout=result.stdout or collector.stdout,
|
|
523
|
+
),
|
|
424
524
|
ok=ok,
|
|
425
525
|
title=f"Ran {command}",
|
|
426
526
|
)
|
|
@@ -430,8 +530,11 @@ def update_plan(arguments: dict[str, object]) -> ToolResult:
|
|
|
430
530
|
items = arguments.get("items", [])
|
|
431
531
|
content = json.dumps(items, ensure_ascii=False)
|
|
432
532
|
return ToolResult(
|
|
433
|
-
|
|
434
|
-
|
|
533
|
+
result={
|
|
534
|
+
"type": "plan",
|
|
535
|
+
"items": items if isinstance(items, list) else [],
|
|
536
|
+
"output": content,
|
|
537
|
+
},
|
|
435
538
|
title="Updated plan",
|
|
436
539
|
)
|
|
437
540
|
|
|
@@ -473,8 +576,12 @@ def web_search(arguments: dict[str, object], context: ToolContext) -> ToolResult
|
|
|
473
576
|
for result in results
|
|
474
577
|
)
|
|
475
578
|
return ToolResult(
|
|
476
|
-
|
|
477
|
-
|
|
579
|
+
result={
|
|
580
|
+
"type": "web_search",
|
|
581
|
+
"output": content or "No results.",
|
|
582
|
+
"query": query,
|
|
583
|
+
"results": results,
|
|
584
|
+
},
|
|
478
585
|
title=f"Searched web for {query}",
|
|
479
586
|
)
|
|
480
587
|
|
|
@@ -148,6 +148,9 @@ def current_model_context_window(model_name: str | None = None) -> int:
|
|
|
148
148
|
|
|
149
149
|
def model_context_window_for(model_name: str | None = None) -> int:
|
|
150
150
|
candidates = normalized_model_name_candidates(model_name)
|
|
151
|
+
metadata_context_window = litellm_input_context_window_for(candidates)
|
|
152
|
+
if metadata_context_window is not None:
|
|
153
|
+
return metadata_context_window
|
|
151
154
|
for candidate in candidates:
|
|
152
155
|
context_window = MODEL_CONTEXT_WINDOWS.get(candidate)
|
|
153
156
|
if context_window is not None:
|
|
@@ -159,6 +162,22 @@ def model_context_window_for(model_name: str | None = None) -> int:
|
|
|
159
162
|
return DEFAULT_MODEL_CONTEXT_WINDOW
|
|
160
163
|
|
|
161
164
|
|
|
165
|
+
def litellm_input_context_window_for(candidates: Sequence[str]) -> int | None:
|
|
166
|
+
try:
|
|
167
|
+
from litellm import model_cost
|
|
168
|
+
except Exception:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
for candidate in candidates:
|
|
172
|
+
metadata = model_cost.get(candidate)
|
|
173
|
+
if metadata is None:
|
|
174
|
+
continue
|
|
175
|
+
context_window = first_int_value(value_at(metadata, "max_input_tokens"))
|
|
176
|
+
if context_window is not None and context_window > 0:
|
|
177
|
+
return context_window
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
162
181
|
def normalized_model_name_candidates(model_name: str | None) -> tuple[str, ...]:
|
|
163
182
|
if model_name is None:
|
|
164
183
|
return ()
|
|
@@ -261,6 +280,53 @@ def estimated_token_usage_for_messages(
|
|
|
261
280
|
)
|
|
262
281
|
|
|
263
282
|
|
|
283
|
+
def estimated_token_usage_for_request(
|
|
284
|
+
messages: Sequence[Mapping[str, object]],
|
|
285
|
+
*,
|
|
286
|
+
output_content: str = "",
|
|
287
|
+
tools: Sequence[Mapping[str, object]] = (),
|
|
288
|
+
) -> TokenUsage:
|
|
289
|
+
message_usage = estimated_token_usage_for_messages(
|
|
290
|
+
messages,
|
|
291
|
+
output_content=output_content,
|
|
292
|
+
)
|
|
293
|
+
tool_tokens = sum(
|
|
294
|
+
approximate_token_count(json.dumps(tool, ensure_ascii=False)) for tool in tools
|
|
295
|
+
)
|
|
296
|
+
input_tokens = message_usage.input_tokens + tool_tokens
|
|
297
|
+
return TokenUsage(
|
|
298
|
+
input_tokens=input_tokens,
|
|
299
|
+
output_tokens=message_usage.output_tokens,
|
|
300
|
+
total_tokens=input_tokens + message_usage.output_tokens,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def full_context_usage(
|
|
305
|
+
usage_info: TokenUsageInfo | None,
|
|
306
|
+
*,
|
|
307
|
+
model_context_window: int,
|
|
308
|
+
) -> TokenUsageInfo:
|
|
309
|
+
info = usage_info or TokenUsageInfo(model_context_window=model_context_window)
|
|
310
|
+
return TokenUsageInfo(
|
|
311
|
+
total_token_usage=info.total_token_usage,
|
|
312
|
+
last_token_usage=TokenUsage(total_tokens=max(0, model_context_window)),
|
|
313
|
+
model_context_window=model_context_window,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def is_context_window_error(error: BaseException) -> bool:
|
|
318
|
+
message = str(error).lower()
|
|
319
|
+
return any(
|
|
320
|
+
marker in message
|
|
321
|
+
for marker in (
|
|
322
|
+
"context window",
|
|
323
|
+
"context_length_exceeded",
|
|
324
|
+
"maximum context length",
|
|
325
|
+
"too many tokens",
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
264
330
|
def estimate_mapping_message_tokens(message: Mapping[str, object]) -> int:
|
|
265
331
|
total = approximate_token_count(string_content(message.get("content")))
|
|
266
332
|
tool_calls = message.get("tool_calls")
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
from flowent.llm import CompletionCallable
|
|
6
|
+
from flowent.provider_connections import selected_connection
|
|
7
|
+
from flowent.storage import StateStore, StoredWorkflow, StoredWorkflowDefinition
|
|
8
|
+
from flowent.workflows import (
|
|
9
|
+
WorkflowRunResponse,
|
|
10
|
+
run_workflow_definition,
|
|
11
|
+
validate_workflow_draft,
|
|
12
|
+
workflow_requires_connection,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WorkflowService:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
chat_completion: CompletionCallable | None,
|
|
21
|
+
store: StateStore,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.chat_completion = chat_completion
|
|
24
|
+
self.store = store
|
|
25
|
+
|
|
26
|
+
def list_workflows(self) -> list[StoredWorkflow]:
|
|
27
|
+
return self.store.read_workflows()
|
|
28
|
+
|
|
29
|
+
def get_workflow(self, workflow_id: str) -> StoredWorkflow:
|
|
30
|
+
workflow = next(
|
|
31
|
+
(
|
|
32
|
+
current_workflow
|
|
33
|
+
for current_workflow in self.store.read_workflows()
|
|
34
|
+
if current_workflow.id == workflow_id
|
|
35
|
+
),
|
|
36
|
+
None,
|
|
37
|
+
)
|
|
38
|
+
if workflow is None:
|
|
39
|
+
raise ValueError("Workflow not found.")
|
|
40
|
+
return workflow
|
|
41
|
+
|
|
42
|
+
def save_workflow(self, workflow: StoredWorkflow) -> StoredWorkflow:
|
|
43
|
+
return self.store.save_workflow(
|
|
44
|
+
validate_workflow_draft(
|
|
45
|
+
workflow.model_copy(
|
|
46
|
+
update={"name": workflow.name.strip() or "Untitled Workflow"}
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def run_workflow(
|
|
52
|
+
self,
|
|
53
|
+
workflow_id: str,
|
|
54
|
+
*,
|
|
55
|
+
default_input: str = "",
|
|
56
|
+
input_values: Mapping[str, str] | None = None,
|
|
57
|
+
) -> WorkflowRunResponse:
|
|
58
|
+
workflow = self.get_workflow(workflow_id)
|
|
59
|
+
connection = (
|
|
60
|
+
selected_connection(self.store.read_state())
|
|
61
|
+
if workflow_requires_connection(workflow.definition)
|
|
62
|
+
else None
|
|
63
|
+
)
|
|
64
|
+
return await run_workflow_definition(
|
|
65
|
+
completion=self.chat_completion,
|
|
66
|
+
connection=connection,
|
|
67
|
+
default_input=default_input,
|
|
68
|
+
definition=workflow.definition,
|
|
69
|
+
input_values=input_values,
|
|
70
|
+
workflow_id=workflow.id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def run_workflow_definition(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
default_input: str = "",
|
|
77
|
+
definition: StoredWorkflowDefinition,
|
|
78
|
+
input_values: Mapping[str, str] | None = None,
|
|
79
|
+
workflow_id: str,
|
|
80
|
+
) -> WorkflowRunResponse:
|
|
81
|
+
connection = (
|
|
82
|
+
selected_connection(self.store.read_state())
|
|
83
|
+
if workflow_requires_connection(definition)
|
|
84
|
+
else None
|
|
85
|
+
)
|
|
86
|
+
return await run_workflow_definition(
|
|
87
|
+
completion=self.chat_completion,
|
|
88
|
+
connection=connection,
|
|
89
|
+
default_input=default_input,
|
|
90
|
+
definition=definition,
|
|
91
|
+
input_values=input_values,
|
|
92
|
+
workflow_id=workflow_id,
|
|
93
|
+
)
|