flowent 0.3.0 → 0.3.2
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 +22 -15
- package/backend/src/flowent/api_models.py +13 -8
- package/backend/src/flowent/llm.py +50 -6
- package/backend/src/flowent/mcp.py +4 -3
- package/backend/src/flowent/permissions.py +51 -38
- package/backend/src/flowent/routes/providers.py +33 -10
- package/backend/src/flowent/routes/system.py +5 -6
- package/backend/src/flowent/routes/workspace.py +33 -23
- package/backend/src/flowent/state/models.py +4 -4
- package/backend/src/flowent/state/schema.py +121 -0
- package/backend/src/flowent/state/store.py +9 -3
- package/backend/src/flowent/static/assets/index-BX18a4Jz.js +100 -0
- package/backend/src/flowent/static/assets/index-EC37agAH.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/tools.py +84 -33
- package/backend/src/flowent/usage.py +66 -0
- package/backend/src/flowent/workspace/context.py +140 -47
- package/backend/src/flowent/workspace/events.py +5 -7
- package/backend/src/flowent/workspace/output.py +129 -4
- package/backend/src/flowent/workspace/runtime.py +393 -185
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-BX18a4Jz.js +100 -0
- package/dist/frontend/assets/index-EC37agAH.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +8 -10
- package/backend/src/flowent/static/assets/index-CvWZZMtK.css +0 -2
- package/backend/src/flowent/static/assets/index-ma2v8oW7.js +0 -90
- package/dist/frontend/assets/index-CvWZZMtK.css +0 -2
- package/dist/frontend/assets/index-ma2v8oW7.js +0 -90
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
from collections.abc import Mapping, Sequence
|
|
4
|
-
from typing import Literal
|
|
5
4
|
|
|
6
5
|
from fastapi import HTTPException
|
|
7
6
|
|
|
@@ -13,10 +12,11 @@ from flowent.storage import (
|
|
|
13
12
|
StoredSettings,
|
|
14
13
|
StoredState,
|
|
15
14
|
)
|
|
15
|
+
from flowent.tools import tool_result_model_content
|
|
16
16
|
from flowent.usage import (
|
|
17
17
|
TokenUsageInfo,
|
|
18
18
|
current_model_context_window,
|
|
19
|
-
|
|
19
|
+
estimated_token_usage_for_request,
|
|
20
20
|
recompute_context_usage,
|
|
21
21
|
)
|
|
22
22
|
from flowent.workspace.output import error_context_summary, message_error_items
|
|
@@ -49,16 +49,18 @@ def auto_compact_token_limit(context_window: int) -> int:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def should_auto_compact(
|
|
52
|
-
messages:
|
|
52
|
+
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
53
53
|
*,
|
|
54
54
|
context_window: int,
|
|
55
|
+
tools: Sequence[Mapping[str, object]] = (),
|
|
55
56
|
) -> bool:
|
|
56
57
|
token_limit = auto_compact_token_limit(context_window)
|
|
57
58
|
if token_limit <= 0:
|
|
58
59
|
return False
|
|
59
60
|
return (
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
estimated_token_usage_for_request(
|
|
62
|
+
model_request_messages_data(messages),
|
|
63
|
+
tools=tools,
|
|
62
64
|
).total_tokens
|
|
63
65
|
>= token_limit
|
|
64
66
|
)
|
|
@@ -84,17 +86,19 @@ def update_context_usage_for_response(
|
|
|
84
86
|
messages: Sequence[Mapping[str, object]],
|
|
85
87
|
output_content: str,
|
|
86
88
|
output_tools: Sequence[Mapping[str, object]] = (),
|
|
89
|
+
request_tools: Sequence[Mapping[str, object]] = (),
|
|
87
90
|
model_context_window: int,
|
|
88
91
|
) -> TokenUsageInfo:
|
|
89
92
|
return recompute_context_usage(
|
|
90
93
|
usage_info,
|
|
91
|
-
|
|
94
|
+
estimated_token_usage_for_request(
|
|
92
95
|
[
|
|
93
96
|
*model_visible_messages_for_usage(messages),
|
|
94
97
|
*model_visible_response_messages_for_usage(
|
|
95
98
|
output_content, output_tools
|
|
96
99
|
),
|
|
97
100
|
],
|
|
101
|
+
tools=request_tools,
|
|
98
102
|
).total_tokens,
|
|
99
103
|
model_context_window=model_context_window,
|
|
100
104
|
)
|
|
@@ -108,6 +112,8 @@ def model_visible_response_messages_for_usage(
|
|
|
108
112
|
for index, tool in enumerate(output_tools):
|
|
109
113
|
tool_id = str(tool.get("id") or f"call_{index}")
|
|
110
114
|
arguments = tool.get("arguments")
|
|
115
|
+
result_payload = tool.get("result")
|
|
116
|
+
tool_result = result_payload if isinstance(result_payload, dict) else {}
|
|
111
117
|
visible_messages.append(
|
|
112
118
|
{
|
|
113
119
|
"role": "assistant",
|
|
@@ -131,7 +137,7 @@ def model_visible_response_messages_for_usage(
|
|
|
131
137
|
{
|
|
132
138
|
"role": "tool",
|
|
133
139
|
"tool_call_id": tool_id,
|
|
134
|
-
"content":
|
|
140
|
+
"content": tool_result_model_content(tool_result),
|
|
135
141
|
}
|
|
136
142
|
)
|
|
137
143
|
if output_content:
|
|
@@ -139,12 +145,129 @@ def model_visible_response_messages_for_usage(
|
|
|
139
145
|
return visible_messages
|
|
140
146
|
|
|
141
147
|
|
|
148
|
+
def model_visible_assistant_output_messages(
|
|
149
|
+
message: StoredMessage,
|
|
150
|
+
) -> list[dict[str, object]]:
|
|
151
|
+
visible_messages: list[dict[str, object]] = []
|
|
152
|
+
for group in message.groups:
|
|
153
|
+
group_content = "".join(
|
|
154
|
+
item.content for item in group.items if item.type == "text"
|
|
155
|
+
)
|
|
156
|
+
group_tools = [item.tool for item in group.items if item.type == "tool"]
|
|
157
|
+
if not group_tools:
|
|
158
|
+
if group_content:
|
|
159
|
+
visible_messages.append({"role": "assistant", "content": group_content})
|
|
160
|
+
continue
|
|
161
|
+
visible_messages.append(
|
|
162
|
+
{
|
|
163
|
+
"role": "assistant",
|
|
164
|
+
"content": group_content or None,
|
|
165
|
+
"tool_calls": [
|
|
166
|
+
{
|
|
167
|
+
"id": tool.id,
|
|
168
|
+
"type": "function",
|
|
169
|
+
"function": {
|
|
170
|
+
"name": tool.name,
|
|
171
|
+
"arguments": json.dumps(
|
|
172
|
+
tool.arguments or {},
|
|
173
|
+
ensure_ascii=False,
|
|
174
|
+
),
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
for tool in group_tools
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
visible_messages.extend(
|
|
182
|
+
{
|
|
183
|
+
"role": "tool",
|
|
184
|
+
"tool_call_id": tool.id,
|
|
185
|
+
"content": tool_result_model_content(tool.result or {}),
|
|
186
|
+
}
|
|
187
|
+
for tool in group_tools
|
|
188
|
+
if tool.status != "running"
|
|
189
|
+
)
|
|
190
|
+
if not visible_messages and message.content:
|
|
191
|
+
visible_messages.append({"role": "assistant", "content": message.content})
|
|
192
|
+
return visible_messages
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def model_visible_workspace_message(message: StoredMessage) -> list[dict[str, object]]:
|
|
196
|
+
if message.author == "user":
|
|
197
|
+
return [{"role": "user", "content": message.content}]
|
|
198
|
+
if message.author != "assistant":
|
|
199
|
+
raise HTTPException(status_code=400, detail="Message history is invalid.")
|
|
200
|
+
errors = message_error_items(message)
|
|
201
|
+
if errors:
|
|
202
|
+
return [
|
|
203
|
+
{"role": "assistant", "content": error_context_summary(error)}
|
|
204
|
+
for error in errors
|
|
205
|
+
]
|
|
206
|
+
return model_visible_assistant_output_messages(message)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def model_visible_workspace_messages(
|
|
210
|
+
messages: Sequence[StoredMessage],
|
|
211
|
+
) -> list[dict[str, object]]:
|
|
212
|
+
visible_messages: list[dict[str, object]] = []
|
|
213
|
+
for message in messages:
|
|
214
|
+
visible_messages.extend(model_visible_workspace_message(message))
|
|
215
|
+
return visible_messages
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def compact_prompt_chat_message(message: Mapping[str, object]) -> ChatMessage:
|
|
219
|
+
role_value = message.get("role")
|
|
220
|
+
content = str(message.get("content") or "")
|
|
221
|
+
if role_value == "system":
|
|
222
|
+
return ChatMessage(role="system", content=content)
|
|
223
|
+
if role_value == "assistant":
|
|
224
|
+
tool_calls = message.get("tool_calls")
|
|
225
|
+
if tool_calls:
|
|
226
|
+
return ChatMessage(
|
|
227
|
+
role="assistant",
|
|
228
|
+
content=(
|
|
229
|
+
f"Tool call: {json.dumps(tool_calls, ensure_ascii=False)}"
|
|
230
|
+
if not content
|
|
231
|
+
else f"{content}\nTool call: {json.dumps(tool_calls, ensure_ascii=False)}"
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
return ChatMessage(role="assistant", content=content)
|
|
235
|
+
if role_value == "tool":
|
|
236
|
+
return ChatMessage(role="user", content=f"Tool result: {content}")
|
|
237
|
+
return ChatMessage(role="user", content=content)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def model_request_message_data(
|
|
241
|
+
message: ChatMessage | Mapping[str, object],
|
|
242
|
+
) -> dict[str, object]:
|
|
243
|
+
if isinstance(message, ChatMessage):
|
|
244
|
+
return message.model_dump()
|
|
245
|
+
return dict(message)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def model_request_messages_data(
|
|
249
|
+
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
250
|
+
) -> list[dict[str, object]]:
|
|
251
|
+
return [model_request_message_data(message) for message in messages]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def compact_prompt_chat_messages(
|
|
255
|
+
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
256
|
+
) -> list[ChatMessage]:
|
|
257
|
+
return [
|
|
258
|
+
message
|
|
259
|
+
if isinstance(message, ChatMessage)
|
|
260
|
+
else compact_prompt_chat_message(message)
|
|
261
|
+
for message in messages
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
|
|
142
265
|
def usage_info_for_model(
|
|
143
266
|
usage_info: TokenUsageInfo | None,
|
|
144
267
|
model_context_window: int,
|
|
145
268
|
) -> TokenUsageInfo | None:
|
|
146
269
|
if usage_info is None:
|
|
147
|
-
return
|
|
270
|
+
return TokenUsageInfo(model_context_window=model_context_window)
|
|
148
271
|
return usage_info.model_copy(update={"model_context_window": model_context_window})
|
|
149
272
|
|
|
150
273
|
|
|
@@ -183,11 +306,13 @@ def workspace_chat_messages(
|
|
|
183
306
|
messages: list[StoredMessage],
|
|
184
307
|
compacted_context: str = "",
|
|
185
308
|
checkpoint: StoredCompactionCheckpoint | None = None,
|
|
186
|
-
) -> list[
|
|
187
|
-
chat_messages: list[
|
|
309
|
+
) -> list[dict[str, object]]:
|
|
310
|
+
chat_messages: list[dict[str, object]] = []
|
|
188
311
|
|
|
189
312
|
if checkpoint is not None:
|
|
190
|
-
chat_messages.extend(
|
|
313
|
+
chat_messages.extend(
|
|
314
|
+
model_request_messages_data(checkpoint.replacement_history)
|
|
315
|
+
)
|
|
191
316
|
visible_messages = transcript_messages_after(
|
|
192
317
|
messages,
|
|
193
318
|
checkpoint.source_message_id,
|
|
@@ -195,26 +320,7 @@ def workspace_chat_messages(
|
|
|
195
320
|
for message in visible_messages:
|
|
196
321
|
if message.author == "system" and is_context_marker(message):
|
|
197
322
|
continue
|
|
198
|
-
|
|
199
|
-
raise HTTPException(
|
|
200
|
-
status_code=400, detail="Message history is invalid."
|
|
201
|
-
)
|
|
202
|
-
if message.author == "assistant":
|
|
203
|
-
errors = message_error_items(message)
|
|
204
|
-
if errors:
|
|
205
|
-
chat_messages.extend(
|
|
206
|
-
ChatMessage(
|
|
207
|
-
role="assistant", content=error_context_summary(error)
|
|
208
|
-
)
|
|
209
|
-
for error in errors
|
|
210
|
-
)
|
|
211
|
-
continue
|
|
212
|
-
checkpoint_role: Literal["user", "assistant"] = (
|
|
213
|
-
"user" if message.author == "user" else "assistant"
|
|
214
|
-
)
|
|
215
|
-
chat_messages.append(
|
|
216
|
-
ChatMessage(role=checkpoint_role, content=message.content)
|
|
217
|
-
)
|
|
323
|
+
chat_messages.extend(model_visible_workspace_message(message))
|
|
218
324
|
return chat_messages
|
|
219
325
|
|
|
220
326
|
marker_index = latest_compacted_context_index(messages)
|
|
@@ -223,8 +329,8 @@ def workspace_chat_messages(
|
|
|
223
329
|
if compacted_context and marker_index is not None:
|
|
224
330
|
chat_messages.extend(
|
|
225
331
|
[
|
|
226
|
-
|
|
227
|
-
|
|
332
|
+
{"role": "user", "content": COMPACTED_CONTEXT_MARKER},
|
|
333
|
+
{"role": "assistant", "content": compacted_context},
|
|
228
334
|
]
|
|
229
335
|
)
|
|
230
336
|
visible_messages = messages[marker_index + 1 :]
|
|
@@ -232,18 +338,5 @@ def workspace_chat_messages(
|
|
|
232
338
|
for message in visible_messages:
|
|
233
339
|
if message.author == "system" and is_context_marker(message):
|
|
234
340
|
continue
|
|
235
|
-
|
|
236
|
-
raise HTTPException(status_code=400, detail="Message history is invalid.")
|
|
237
|
-
if message.author == "assistant":
|
|
238
|
-
errors = message_error_items(message)
|
|
239
|
-
if errors:
|
|
240
|
-
chat_messages.extend(
|
|
241
|
-
ChatMessage(role="assistant", content=error_context_summary(error))
|
|
242
|
-
for error in errors
|
|
243
|
-
)
|
|
244
|
-
continue
|
|
245
|
-
role: Literal["user", "assistant"] = (
|
|
246
|
-
"user" if message.author == "user" else "assistant"
|
|
247
|
-
)
|
|
248
|
-
chat_messages.append(ChatMessage(role=role, content=message.content))
|
|
341
|
+
chat_messages.extend(model_visible_workspace_message(message))
|
|
249
342
|
return chat_messages
|
|
@@ -3,19 +3,17 @@ import copy
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Literal
|
|
6
|
-
from uuid import uuid4
|
|
7
6
|
|
|
8
7
|
from flowent.storage import StoredMessage
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
@dataclass
|
|
12
|
-
class
|
|
11
|
+
class WorkspaceResponse:
|
|
13
12
|
condition: asyncio.Condition
|
|
14
13
|
active_output: Literal["text", "thinking"] | None = None
|
|
15
14
|
discard_on_cancel: bool = False
|
|
16
15
|
events: list[tuple[int, str, dict[str, object]]] = field(default_factory=list)
|
|
17
16
|
generation: int = 0
|
|
18
|
-
id: str = field(default_factory=lambda: str(uuid4()))
|
|
19
17
|
is_done: bool = False
|
|
20
18
|
latest_snapshot: StoredMessage | None = None
|
|
21
19
|
task: asyncio.Task[None] | None = None
|
|
@@ -50,12 +48,12 @@ def append_or_replace_message(
|
|
|
50
48
|
]
|
|
51
49
|
|
|
52
50
|
|
|
53
|
-
def
|
|
54
|
-
|
|
51
|
+
def response_snapshot_data_at(
|
|
52
|
+
response: WorkspaceResponse, event_index: int
|
|
55
53
|
) -> dict[str, object] | None:
|
|
56
54
|
snapshot_event_index = 0
|
|
57
55
|
snapshot: dict[str, object] | None = None
|
|
58
|
-
for current_event_index, event, data in
|
|
56
|
+
for current_event_index, event, data in response.events:
|
|
59
57
|
if current_event_index > event_index:
|
|
60
58
|
break
|
|
61
59
|
if event != "snapshot":
|
|
@@ -78,7 +76,7 @@ def run_snapshot_data_at(
|
|
|
78
76
|
snapshot = copy.deepcopy(message)
|
|
79
77
|
if snapshot is None:
|
|
80
78
|
return None
|
|
81
|
-
for current_event_index, event, data in
|
|
79
|
+
for current_event_index, event, data in response.events:
|
|
82
80
|
if current_event_index <= snapshot_event_index:
|
|
83
81
|
continue
|
|
84
82
|
if current_event_index > event_index:
|
|
@@ -7,11 +7,13 @@ from flowent.storage import (
|
|
|
7
7
|
StoredAssistantOutputGroup,
|
|
8
8
|
StoredErrorOutputItem,
|
|
9
9
|
StoredMessage,
|
|
10
|
+
StoredOutputItem,
|
|
10
11
|
StoredTextOutputItem,
|
|
11
12
|
StoredThinkingOutputItem,
|
|
12
13
|
StoredToolItem,
|
|
13
14
|
StoredToolOutputItem,
|
|
14
15
|
)
|
|
16
|
+
from flowent.tools import tool_result_model_content
|
|
15
17
|
|
|
16
18
|
APPROVAL_TRANSCRIPT_MESSAGE_LIMIT = 12
|
|
17
19
|
APPROVAL_TRANSCRIPT_TEXT_LIMIT = 2_000
|
|
@@ -83,7 +85,9 @@ def approval_transcript(
|
|
|
83
85
|
if content:
|
|
84
86
|
entries.append(ApprovalTranscriptEntry(role=role, content=content))
|
|
85
87
|
for tool in message.tools:
|
|
86
|
-
tool_content = approval_transcript_text(
|
|
88
|
+
tool_content = approval_transcript_text(
|
|
89
|
+
tool_result_model_content(tool.result or {})
|
|
90
|
+
)
|
|
87
91
|
if tool_content:
|
|
88
92
|
entries.append(
|
|
89
93
|
ApprovalTranscriptEntry(
|
|
@@ -108,6 +112,42 @@ class AssistantOutputBuilder:
|
|
|
108
112
|
self.error_item_index = 0
|
|
109
113
|
self.tools: dict[str, StoredToolItem] = {}
|
|
110
114
|
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_message(cls, message: StoredMessage) -> "AssistantOutputBuilder":
|
|
117
|
+
builder = cls(message.id)
|
|
118
|
+
builder.content = message.content
|
|
119
|
+
builder.groups = message.groups
|
|
120
|
+
builder.thinking = message.thinking
|
|
121
|
+
builder.tools = {tool.id: tool for tool in message.tools}
|
|
122
|
+
builder.text_item_index = sum(
|
|
123
|
+
1 for group in message.groups for item in group.items if item.type == "text"
|
|
124
|
+
)
|
|
125
|
+
builder.thinking_item_index = sum(
|
|
126
|
+
1
|
|
127
|
+
for group in message.groups
|
|
128
|
+
for item in group.items
|
|
129
|
+
if item.type == "thinking"
|
|
130
|
+
)
|
|
131
|
+
builder.error_item_index = sum(
|
|
132
|
+
1
|
|
133
|
+
for group in message.groups
|
|
134
|
+
for item in group.items
|
|
135
|
+
if item.type == "error"
|
|
136
|
+
)
|
|
137
|
+
latest_item = next(
|
|
138
|
+
(
|
|
139
|
+
item
|
|
140
|
+
for group in reversed(message.groups)
|
|
141
|
+
for item in reversed(group.items)
|
|
142
|
+
),
|
|
143
|
+
None,
|
|
144
|
+
)
|
|
145
|
+
if latest_item is not None and latest_item.type == "text":
|
|
146
|
+
builder.text_item_id = latest_item.id
|
|
147
|
+
if latest_item is not None and latest_item.type == "thinking":
|
|
148
|
+
builder.thinking_item_id = latest_item.id
|
|
149
|
+
return builder
|
|
150
|
+
|
|
111
151
|
def set_assistant_id(self, assistant_id: str) -> None:
|
|
112
152
|
self.assistant_id = assistant_id
|
|
113
153
|
|
|
@@ -218,9 +258,29 @@ class AssistantOutputBuilder:
|
|
|
218
258
|
def has_output(self) -> bool:
|
|
219
259
|
return any(group.items for group in self.groups)
|
|
220
260
|
|
|
221
|
-
def apply_done_message(
|
|
222
|
-
|
|
223
|
-
|
|
261
|
+
def apply_done_message(
|
|
262
|
+
self,
|
|
263
|
+
message: dict[str, object],
|
|
264
|
+
*,
|
|
265
|
+
content_prefix: str = "",
|
|
266
|
+
thinking_prefix: str = "",
|
|
267
|
+
) -> None:
|
|
268
|
+
message_content = str(message.get("content") or "")
|
|
269
|
+
message_thinking = str(message.get("thinking") or "")
|
|
270
|
+
final_content = message_content or self.content
|
|
271
|
+
final_thinking = message_thinking or self.thinking
|
|
272
|
+
if (
|
|
273
|
+
content_prefix
|
|
274
|
+
and message_content
|
|
275
|
+
and not message_content.startswith(content_prefix)
|
|
276
|
+
):
|
|
277
|
+
final_content = f"{content_prefix}{message_content}"
|
|
278
|
+
if (
|
|
279
|
+
thinking_prefix
|
|
280
|
+
and message_thinking
|
|
281
|
+
and not message_thinking.startswith(thinking_prefix)
|
|
282
|
+
):
|
|
283
|
+
final_thinking = f"{thinking_prefix}{message_thinking}"
|
|
224
284
|
self._append_missing_done_text(final_content)
|
|
225
285
|
self._append_missing_done_thinking(final_thinking)
|
|
226
286
|
self.content = final_content
|
|
@@ -272,3 +332,68 @@ class AssistantOutputBuilder:
|
|
|
272
332
|
self.groups[-1] = self.groups[-1].model_copy(
|
|
273
333
|
update={"items": [*self.groups[-1].items, item]}
|
|
274
334
|
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def trim_assistant_message_at_error(
|
|
338
|
+
message: StoredMessage,
|
|
339
|
+
error_id: str,
|
|
340
|
+
*,
|
|
341
|
+
status: str,
|
|
342
|
+
) -> StoredMessage | None:
|
|
343
|
+
next_groups: list[StoredAssistantOutputGroup] = []
|
|
344
|
+
found_error = False
|
|
345
|
+
for group in message.groups:
|
|
346
|
+
next_items: list[StoredOutputItem] = []
|
|
347
|
+
for item in group.items:
|
|
348
|
+
if item.type == "error" and item.id == error_id:
|
|
349
|
+
found_error = True
|
|
350
|
+
break
|
|
351
|
+
next_items.append(item)
|
|
352
|
+
if found_error:
|
|
353
|
+
if next_items:
|
|
354
|
+
next_groups.append(group.model_copy(update={"items": next_items}))
|
|
355
|
+
break
|
|
356
|
+
next_groups.append(group)
|
|
357
|
+
|
|
358
|
+
if not found_error:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
text_content = "".join(
|
|
362
|
+
item.content
|
|
363
|
+
for group in next_groups
|
|
364
|
+
for item in group.items
|
|
365
|
+
if item.type == "text"
|
|
366
|
+
)
|
|
367
|
+
thinking_content = "".join(
|
|
368
|
+
item.content
|
|
369
|
+
for group in next_groups
|
|
370
|
+
for item in group.items
|
|
371
|
+
if item.type == "thinking"
|
|
372
|
+
)
|
|
373
|
+
tools = [
|
|
374
|
+
item.tool
|
|
375
|
+
for group in next_groups
|
|
376
|
+
for item in group.items
|
|
377
|
+
if item.type == "tool"
|
|
378
|
+
]
|
|
379
|
+
return message.model_copy(
|
|
380
|
+
update={
|
|
381
|
+
"content": text_content,
|
|
382
|
+
"groups": next_groups,
|
|
383
|
+
"status": status,
|
|
384
|
+
"thinking": thinking_content,
|
|
385
|
+
"tools": tools,
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def assistant_retry_output_start_index(message: StoredMessage) -> int:
|
|
391
|
+
prefix = f"{message.id}-group-"
|
|
392
|
+
indexes: list[int] = []
|
|
393
|
+
for group in message.groups:
|
|
394
|
+
if not group.id.startswith(prefix):
|
|
395
|
+
continue
|
|
396
|
+
raw_index = group.id.removeprefix(prefix)
|
|
397
|
+
if raw_index.isdigit():
|
|
398
|
+
indexes.append(int(raw_index))
|
|
399
|
+
return max(indexes, default=1)
|