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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
from flowent.storage import StoredWorkflow
|
|
6
|
+
from flowent.tools import ToolResult, text_tool_result
|
|
7
|
+
from flowent.workflow_service import WorkflowService
|
|
8
|
+
from flowent.workflows import WorkflowRunResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def workflow_tool_specs() -> list[dict[str, object]]:
|
|
12
|
+
workflow_schema: dict[str, object] = {"type": "object"}
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
"type": "function",
|
|
16
|
+
"function": {
|
|
17
|
+
"name": "list_workflows",
|
|
18
|
+
"description": "List saved workflows with their ids, names, node counts, and edge counts.",
|
|
19
|
+
"parameters": {"type": "object", "properties": {}},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"type": "function",
|
|
24
|
+
"function": {
|
|
25
|
+
"name": "get_workflow",
|
|
26
|
+
"description": "Read a saved workflow definition by id before answering questions or editing it.",
|
|
27
|
+
"parameters": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {"workflow_id": {"type": "string"}},
|
|
30
|
+
"required": ["workflow_id"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "function",
|
|
36
|
+
"function": {
|
|
37
|
+
"name": "run_workflow",
|
|
38
|
+
"description": "Run a saved workflow. Pass input when the user's current message contains the content the workflow should process.",
|
|
39
|
+
"parameters": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"workflow_id": {"type": "string"},
|
|
43
|
+
"input": {"type": "string"},
|
|
44
|
+
"inputs": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": {"type": "string"},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
"required": ["workflow_id"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"type": "function",
|
|
55
|
+
"function": {
|
|
56
|
+
"name": "create_workflow",
|
|
57
|
+
"description": "Create a workflow. workflow must include id, name, and definition with version, nodes, and edges.",
|
|
58
|
+
"parameters": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {"workflow": workflow_schema},
|
|
61
|
+
"required": ["workflow"],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"type": "function",
|
|
67
|
+
"function": {
|
|
68
|
+
"name": "update_workflow",
|
|
69
|
+
"description": "Replace an existing workflow. Read it first and provide the complete updated workflow object.",
|
|
70
|
+
"parameters": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {"workflow": workflow_schema},
|
|
73
|
+
"required": ["workflow"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def workflow_tool_title(name: str) -> str | None:
|
|
81
|
+
if name == "list_workflows":
|
|
82
|
+
return "Listing workflows"
|
|
83
|
+
if name == "get_workflow":
|
|
84
|
+
return "Reading workflow"
|
|
85
|
+
if name == "run_workflow":
|
|
86
|
+
return "Running workflow"
|
|
87
|
+
if name == "create_workflow":
|
|
88
|
+
return "Creating workflow"
|
|
89
|
+
if name == "update_workflow":
|
|
90
|
+
return "Updating workflow"
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WorkflowAgentTools:
|
|
95
|
+
def __init__(self, service: WorkflowService) -> None:
|
|
96
|
+
self.service = service
|
|
97
|
+
|
|
98
|
+
async def run_tool(
|
|
99
|
+
self, name: str, arguments: dict[str, object]
|
|
100
|
+
) -> ToolResult | None:
|
|
101
|
+
title = workflow_tool_title(name)
|
|
102
|
+
if title is None:
|
|
103
|
+
return None
|
|
104
|
+
try:
|
|
105
|
+
if name == "list_workflows":
|
|
106
|
+
return self.list_workflows()
|
|
107
|
+
if name == "get_workflow":
|
|
108
|
+
return self.get_workflow(arguments)
|
|
109
|
+
if name == "run_workflow":
|
|
110
|
+
return await self.run_workflow(arguments)
|
|
111
|
+
if name == "create_workflow":
|
|
112
|
+
return self.create_workflow(arguments)
|
|
113
|
+
if name == "update_workflow":
|
|
114
|
+
return self.update_workflow(arguments)
|
|
115
|
+
except Exception as error:
|
|
116
|
+
return ToolResult(
|
|
117
|
+
result=text_tool_result(str(error) or "Workflow tool failed."),
|
|
118
|
+
ok=False,
|
|
119
|
+
title=title,
|
|
120
|
+
)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def list_workflows(self) -> ToolResult:
|
|
124
|
+
workflows = self.service.list_workflows()
|
|
125
|
+
summaries = [workflow_summary(workflow) for workflow in workflows]
|
|
126
|
+
if summaries:
|
|
127
|
+
output = "\n".join(
|
|
128
|
+
f"{summary['id']}: {summary['name']} ({summary['node_count']} nodes, {summary['edge_count']} edges)"
|
|
129
|
+
for summary in summaries
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
output = "No workflows are saved."
|
|
133
|
+
return ToolResult(
|
|
134
|
+
result={
|
|
135
|
+
"type": "workflow_list",
|
|
136
|
+
"output": output,
|
|
137
|
+
"workflows": summaries,
|
|
138
|
+
},
|
|
139
|
+
title="Listed workflows",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
143
|
+
workflow = self.service.get_workflow(str(arguments["workflow_id"]))
|
|
144
|
+
summary = workflow_summary(workflow)
|
|
145
|
+
output = (
|
|
146
|
+
f"{workflow.name} has {summary['node_count']} nodes and "
|
|
147
|
+
f"{summary['edge_count']} edges."
|
|
148
|
+
)
|
|
149
|
+
return ToolResult(
|
|
150
|
+
result={
|
|
151
|
+
"type": "workflow",
|
|
152
|
+
"output": output,
|
|
153
|
+
"summary": summary,
|
|
154
|
+
"workflow": workflow.model_dump(mode="json"),
|
|
155
|
+
},
|
|
156
|
+
title=f"Read {workflow.name}",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def run_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
160
|
+
workflow_id = str(arguments["workflow_id"])
|
|
161
|
+
workflow = self.service.get_workflow(workflow_id)
|
|
162
|
+
result = await self.service.run_workflow(
|
|
163
|
+
workflow_id,
|
|
164
|
+
default_input=string_argument(arguments, "input"),
|
|
165
|
+
input_values=string_map_argument(arguments, "inputs"),
|
|
166
|
+
)
|
|
167
|
+
output = workflow_run_output(workflow.name, result)
|
|
168
|
+
return ToolResult(
|
|
169
|
+
result={
|
|
170
|
+
"type": "workflow_run",
|
|
171
|
+
"node_results": [
|
|
172
|
+
node_result.model_dump(mode="json")
|
|
173
|
+
for node_result in result.node_results
|
|
174
|
+
],
|
|
175
|
+
"output": output,
|
|
176
|
+
"outputs": dict(result.outputs),
|
|
177
|
+
"status": result.status,
|
|
178
|
+
"workflow_id": workflow.id,
|
|
179
|
+
"workflow_name": workflow.name,
|
|
180
|
+
},
|
|
181
|
+
ok=result.status == "success",
|
|
182
|
+
title=f"Ran {workflow.name}",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def create_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
186
|
+
workflow = workflow_argument(arguments)
|
|
187
|
+
try:
|
|
188
|
+
self.service.get_workflow(workflow.id)
|
|
189
|
+
except ValueError:
|
|
190
|
+
saved = self.service.save_workflow(workflow)
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError("Workflow already exists.")
|
|
193
|
+
return saved_workflow_result(saved, "Created")
|
|
194
|
+
|
|
195
|
+
def update_workflow(self, arguments: dict[str, object]) -> ToolResult:
|
|
196
|
+
workflow = workflow_argument(arguments)
|
|
197
|
+
self.service.get_workflow(workflow.id)
|
|
198
|
+
saved = self.service.save_workflow(workflow)
|
|
199
|
+
return saved_workflow_result(saved, "Updated")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def string_argument(arguments: dict[str, object], name: str) -> str:
|
|
203
|
+
value = arguments.get(name, "")
|
|
204
|
+
if value is None:
|
|
205
|
+
return ""
|
|
206
|
+
return value if isinstance(value, str) else str(value)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def string_map_argument(
|
|
210
|
+
arguments: dict[str, object], name: str
|
|
211
|
+
) -> dict[str, str] | None:
|
|
212
|
+
value = arguments.get(name)
|
|
213
|
+
if not isinstance(value, dict):
|
|
214
|
+
return None
|
|
215
|
+
return {str(key): str(item) for key, item in value.items()}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def workflow_argument(arguments: dict[str, object]) -> StoredWorkflow:
|
|
219
|
+
value = arguments.get("workflow")
|
|
220
|
+
if not isinstance(value, dict):
|
|
221
|
+
raise ValueError("Workflow must be an object.")
|
|
222
|
+
try:
|
|
223
|
+
return StoredWorkflow.model_validate(value)
|
|
224
|
+
except ValidationError as error:
|
|
225
|
+
raise ValueError(error.errors()[0]["msg"]) from error
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def workflow_summary(workflow: StoredWorkflow) -> dict[str, object]:
|
|
229
|
+
return {
|
|
230
|
+
"edge_count": len(workflow.definition.edges),
|
|
231
|
+
"id": workflow.id,
|
|
232
|
+
"name": workflow.name,
|
|
233
|
+
"node_count": len(workflow.definition.nodes),
|
|
234
|
+
"nodes": [
|
|
235
|
+
{"id": node.id, "name": node.name, "type": node.type}
|
|
236
|
+
for node in workflow.definition.nodes
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def workflow_run_output(name: str, result: WorkflowRunResponse) -> str:
|
|
242
|
+
if result.status == "success":
|
|
243
|
+
if result.outputs:
|
|
244
|
+
rendered_outputs = "\n".join(
|
|
245
|
+
f"{key}: {value}" for key, value in result.outputs.items()
|
|
246
|
+
)
|
|
247
|
+
return f"{name} completed.\n{rendered_outputs}"
|
|
248
|
+
return f"{name} completed."
|
|
249
|
+
failures = [
|
|
250
|
+
f"{node_result.id}: {node_result.error}"
|
|
251
|
+
for node_result in result.node_results
|
|
252
|
+
if node_result.status == "failed"
|
|
253
|
+
]
|
|
254
|
+
return f"{name} failed.\n" + ("\n".join(failures) or "No failure details.")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def saved_workflow_result(workflow: StoredWorkflow, action: str) -> ToolResult:
|
|
258
|
+
summary = workflow_summary(workflow)
|
|
259
|
+
output = (
|
|
260
|
+
f"{action} {workflow.name} with {summary['node_count']} nodes and "
|
|
261
|
+
f"{summary['edge_count']} edges."
|
|
262
|
+
)
|
|
263
|
+
return ToolResult(
|
|
264
|
+
result={
|
|
265
|
+
"type": "workflow",
|
|
266
|
+
"output": output,
|
|
267
|
+
"summary": summary,
|
|
268
|
+
"workflow": workflow.model_dump(mode="json"),
|
|
269
|
+
},
|
|
270
|
+
title=f"{action} {workflow.name}",
|
|
271
|
+
)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import re
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
3
5
|
from collections import defaultdict, deque
|
|
4
6
|
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
5
8
|
|
|
6
9
|
from pydantic import BaseModel, ConfigDict, Field
|
|
7
10
|
|
|
@@ -11,6 +14,7 @@ from flowent.llm import (
|
|
|
11
14
|
ProviderConnection,
|
|
12
15
|
complete_chat,
|
|
13
16
|
)
|
|
17
|
+
from flowent.sandbox import SandboxRunner
|
|
14
18
|
from flowent.storage import (
|
|
15
19
|
StoredWorkflow,
|
|
16
20
|
StoredWorkflowDefinition,
|
|
@@ -38,6 +42,31 @@ class WorkflowRunResponse(BaseModel):
|
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
PLACEHOLDER_PATTERN = re.compile(r"\{\{\s*([A-Za-z0-9_.-]+)\.output\s*\}\}")
|
|
45
|
+
PYTHON_CODE_RUNNER = r"""
|
|
46
|
+
import contextlib
|
|
47
|
+
import io
|
|
48
|
+
import json
|
|
49
|
+
import sys
|
|
50
|
+
|
|
51
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
52
|
+
namespace = {
|
|
53
|
+
"input": payload.get("input", ""),
|
|
54
|
+
"inputs": payload.get("inputs", []),
|
|
55
|
+
"output": "",
|
|
56
|
+
}
|
|
57
|
+
stdout = io.StringIO()
|
|
58
|
+
with contextlib.redirect_stdout(stdout):
|
|
59
|
+
exec(str(payload.get("code", "")), namespace)
|
|
60
|
+
captured = stdout.getvalue()
|
|
61
|
+
result = namespace.get("output")
|
|
62
|
+
if result is None:
|
|
63
|
+
result = ""
|
|
64
|
+
if result == "" and captured:
|
|
65
|
+
result = captured.rstrip("\n")
|
|
66
|
+
if not isinstance(result, str):
|
|
67
|
+
result = json.dumps(result, ensure_ascii=False)
|
|
68
|
+
print(result, end="")
|
|
69
|
+
"""
|
|
41
70
|
|
|
42
71
|
|
|
43
72
|
def validate_workflow(workflow: StoredWorkflow) -> StoredWorkflow:
|
|
@@ -136,6 +165,8 @@ async def run_workflow_definition(
|
|
|
136
165
|
completion: CompletionCallable | None,
|
|
137
166
|
connection: ProviderConnection | None,
|
|
138
167
|
definition: StoredWorkflowDefinition,
|
|
168
|
+
default_input: str = "",
|
|
169
|
+
input_values: Mapping[str, str] | None = None,
|
|
139
170
|
workflow_id: str,
|
|
140
171
|
) -> WorkflowRunResponse:
|
|
141
172
|
ordered_ids = validate_workflow_definition(definition)
|
|
@@ -150,6 +181,7 @@ async def run_workflow_definition(
|
|
|
150
181
|
}
|
|
151
182
|
outputs: dict[str, str] = {}
|
|
152
183
|
named_outputs: dict[str, str] = {}
|
|
184
|
+
remaining_default_input = default_input
|
|
153
185
|
|
|
154
186
|
for node_id in ordered_ids:
|
|
155
187
|
node = nodes[node_id]
|
|
@@ -158,10 +190,14 @@ async def run_workflow_definition(
|
|
|
158
190
|
output = await run_node(
|
|
159
191
|
completion=completion,
|
|
160
192
|
connection=connection,
|
|
193
|
+
default_input=remaining_default_input,
|
|
194
|
+
input_values=input_values or {},
|
|
161
195
|
incoming_edges=incoming_edges[node.id],
|
|
162
196
|
node=node,
|
|
163
197
|
outputs=outputs,
|
|
164
198
|
)
|
|
199
|
+
if node.type == "input" and remaining_default_input:
|
|
200
|
+
remaining_default_input = ""
|
|
165
201
|
except Exception as error:
|
|
166
202
|
results[node.id] = WorkflowNodeRunResult(
|
|
167
203
|
error=str(error) or "Node could not be completed.",
|
|
@@ -195,11 +231,17 @@ async def run_node(
|
|
|
195
231
|
*,
|
|
196
232
|
completion: CompletionCallable | None,
|
|
197
233
|
connection: ProviderConnection | None,
|
|
234
|
+
default_input: str,
|
|
235
|
+
input_values: Mapping[str, str],
|
|
198
236
|
incoming_edges: list[StoredWorkflowEdge],
|
|
199
237
|
node: StoredWorkflowNode,
|
|
200
238
|
outputs: Mapping[str, str],
|
|
201
239
|
) -> str:
|
|
202
240
|
if node.type == "input":
|
|
241
|
+
if node.id in input_values:
|
|
242
|
+
return input_values[node.id]
|
|
243
|
+
if default_input:
|
|
244
|
+
return default_input
|
|
203
245
|
return node_data_text(node, "default_value")
|
|
204
246
|
if node.type == "agent":
|
|
205
247
|
if connection is None:
|
|
@@ -220,11 +262,35 @@ async def run_node(
|
|
|
220
262
|
if node_data_text(node, "merge_strategy") == "json":
|
|
221
263
|
return merge_json_outputs(upstream)
|
|
222
264
|
return "\n".join(output for output in upstream if output)
|
|
265
|
+
if node.type == "code":
|
|
266
|
+
return await run_code_node(node, upstream_outputs(incoming_edges, outputs))
|
|
223
267
|
if node.type == "output":
|
|
224
268
|
return joined_upstream_outputs(incoming_edges, outputs)
|
|
225
269
|
raise ValueError("Node type is not supported.")
|
|
226
270
|
|
|
227
271
|
|
|
272
|
+
async def run_code_node(node: StoredWorkflowNode, upstream: list[str]) -> str:
|
|
273
|
+
code = node_data_text(node, "code")
|
|
274
|
+
if not code.strip():
|
|
275
|
+
return joined_text(upstream)
|
|
276
|
+
with tempfile.TemporaryDirectory(prefix="flowent-workflow-code-") as code_dir:
|
|
277
|
+
result = await SandboxRunner(timeout_seconds=10, cwd=Path(code_dir)).run_async(
|
|
278
|
+
[sys.executable, "-I", "-c", PYTHON_CODE_RUNNER],
|
|
279
|
+
input_text=json.dumps(
|
|
280
|
+
{
|
|
281
|
+
"code": code,
|
|
282
|
+
"input": joined_text(upstream),
|
|
283
|
+
"inputs": upstream,
|
|
284
|
+
},
|
|
285
|
+
ensure_ascii=False,
|
|
286
|
+
),
|
|
287
|
+
timeout_seconds=10,
|
|
288
|
+
)
|
|
289
|
+
if result.exit_code != 0:
|
|
290
|
+
raise ValueError((result.stderr or result.stdout).strip() or "Code failed.")
|
|
291
|
+
return result.stdout
|
|
292
|
+
|
|
293
|
+
|
|
228
294
|
def edges_by_target(
|
|
229
295
|
edges: list[StoredWorkflowEdge],
|
|
230
296
|
) -> dict[str, list[StoredWorkflowEdge]]:
|
|
@@ -258,9 +324,11 @@ def joined_upstream_outputs(
|
|
|
258
324
|
incoming_edges: list[StoredWorkflowEdge],
|
|
259
325
|
outputs: Mapping[str, str],
|
|
260
326
|
) -> str:
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
|
|
327
|
+
return joined_text(upstream_outputs(incoming_edges, outputs))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def joined_text(values: list[str]) -> str:
|
|
331
|
+
return "\n".join(value for value in values if value)
|
|
264
332
|
|
|
265
333
|
|
|
266
334
|
def render_template(template: str, outputs: Mapping[str, str]) -> str:
|
|
@@ -12,10 +12,11 @@ from flowent.storage import (
|
|
|
12
12
|
StoredSettings,
|
|
13
13
|
StoredState,
|
|
14
14
|
)
|
|
15
|
+
from flowent.tools import tool_result_model_content
|
|
15
16
|
from flowent.usage import (
|
|
16
17
|
TokenUsageInfo,
|
|
17
18
|
current_model_context_window,
|
|
18
|
-
|
|
19
|
+
estimated_token_usage_for_request,
|
|
19
20
|
recompute_context_usage,
|
|
20
21
|
)
|
|
21
22
|
from flowent.workspace.output import error_context_summary, message_error_items
|
|
@@ -51,13 +52,15 @@ def should_auto_compact(
|
|
|
51
52
|
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
52
53
|
*,
|
|
53
54
|
context_window: int,
|
|
55
|
+
tools: Sequence[Mapping[str, object]] = (),
|
|
54
56
|
) -> bool:
|
|
55
57
|
token_limit = auto_compact_token_limit(context_window)
|
|
56
58
|
if token_limit <= 0:
|
|
57
59
|
return False
|
|
58
60
|
return (
|
|
59
|
-
|
|
60
|
-
model_request_messages_data(messages)
|
|
61
|
+
estimated_token_usage_for_request(
|
|
62
|
+
model_request_messages_data(messages),
|
|
63
|
+
tools=tools,
|
|
61
64
|
).total_tokens
|
|
62
65
|
>= token_limit
|
|
63
66
|
)
|
|
@@ -83,17 +86,19 @@ def update_context_usage_for_response(
|
|
|
83
86
|
messages: Sequence[Mapping[str, object]],
|
|
84
87
|
output_content: str,
|
|
85
88
|
output_tools: Sequence[Mapping[str, object]] = (),
|
|
89
|
+
request_tools: Sequence[Mapping[str, object]] = (),
|
|
86
90
|
model_context_window: int,
|
|
87
91
|
) -> TokenUsageInfo:
|
|
88
92
|
return recompute_context_usage(
|
|
89
93
|
usage_info,
|
|
90
|
-
|
|
94
|
+
estimated_token_usage_for_request(
|
|
91
95
|
[
|
|
92
96
|
*model_visible_messages_for_usage(messages),
|
|
93
97
|
*model_visible_response_messages_for_usage(
|
|
94
98
|
output_content, output_tools
|
|
95
99
|
),
|
|
96
100
|
],
|
|
101
|
+
tools=request_tools,
|
|
97
102
|
).total_tokens,
|
|
98
103
|
model_context_window=model_context_window,
|
|
99
104
|
)
|
|
@@ -107,6 +112,8 @@ def model_visible_response_messages_for_usage(
|
|
|
107
112
|
for index, tool in enumerate(output_tools):
|
|
108
113
|
tool_id = str(tool.get("id") or f"call_{index}")
|
|
109
114
|
arguments = tool.get("arguments")
|
|
115
|
+
result_payload = tool.get("result")
|
|
116
|
+
tool_result = result_payload if isinstance(result_payload, dict) else {}
|
|
110
117
|
visible_messages.append(
|
|
111
118
|
{
|
|
112
119
|
"role": "assistant",
|
|
@@ -130,7 +137,7 @@ def model_visible_response_messages_for_usage(
|
|
|
130
137
|
{
|
|
131
138
|
"role": "tool",
|
|
132
139
|
"tool_call_id": tool_id,
|
|
133
|
-
"content":
|
|
140
|
+
"content": tool_result_model_content(tool_result),
|
|
134
141
|
}
|
|
135
142
|
)
|
|
136
143
|
if output_content:
|
|
@@ -175,7 +182,7 @@ def model_visible_assistant_output_messages(
|
|
|
175
182
|
{
|
|
176
183
|
"role": "tool",
|
|
177
184
|
"tool_call_id": tool.id,
|
|
178
|
-
"content": tool.
|
|
185
|
+
"content": tool_result_model_content(tool.result or {}),
|
|
179
186
|
}
|
|
180
187
|
for tool in group_tools
|
|
181
188
|
if tool.status != "running"
|
|
@@ -260,7 +267,7 @@ def usage_info_for_model(
|
|
|
260
267
|
model_context_window: int,
|
|
261
268
|
) -> TokenUsageInfo | None:
|
|
262
269
|
if usage_info is None:
|
|
263
|
-
return
|
|
270
|
+
return TokenUsageInfo(model_context_window=model_context_window)
|
|
264
271
|
return usage_info.model_copy(update={"model_context_window": model_context_window})
|
|
265
272
|
|
|
266
273
|
|
|
@@ -13,6 +13,7 @@ from flowent.storage import (
|
|
|
13
13
|
StoredToolItem,
|
|
14
14
|
StoredToolOutputItem,
|
|
15
15
|
)
|
|
16
|
+
from flowent.tools import tool_result_model_content
|
|
16
17
|
|
|
17
18
|
APPROVAL_TRANSCRIPT_MESSAGE_LIMIT = 12
|
|
18
19
|
APPROVAL_TRANSCRIPT_TEXT_LIMIT = 2_000
|
|
@@ -84,7 +85,9 @@ def approval_transcript(
|
|
|
84
85
|
if content:
|
|
85
86
|
entries.append(ApprovalTranscriptEntry(role=role, content=content))
|
|
86
87
|
for tool in message.tools:
|
|
87
|
-
tool_content = approval_transcript_text(
|
|
88
|
+
tool_content = approval_transcript_text(
|
|
89
|
+
tool_result_model_content(tool.result or {})
|
|
90
|
+
)
|
|
88
91
|
if tool_content:
|
|
89
92
|
entries.append(
|
|
90
93
|
ApprovalTranscriptEntry(
|