flowent 0.3.4 → 0.3.6
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 +5 -0
- package/backend/src/flowent/agent_runtime.py +14 -0
- package/backend/src/flowent/state/schema.py +8 -0
- package/backend/src/flowent/state/store.py +57 -0
- package/backend/src/flowent/static/assets/{index-D3WSbctU.js → index-AHH9ayZ2.js} +34 -34
- package/backend/src/flowent/static/assets/index-DROV_zPr.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/workflows.py +15 -4
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/{index-D3WSbctU.js → index-AHH9ayZ2.js} +34 -34
- package/dist/frontend/assets/index-DROV_zPr.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-ByGH1ZWH.css +0 -2
- package/dist/frontend/assets/index-ByGH1ZWH.css +0 -2
package/backend/pyproject.toml
CHANGED
|
@@ -120,6 +120,8 @@ async def run_agent_stream(
|
|
|
120
120
|
*,
|
|
121
121
|
completion: CompletionCallable | None,
|
|
122
122
|
connection: ProviderConnection,
|
|
123
|
+
conversation_recorder: Callable[[Sequence[Mapping[str, object]]], None]
|
|
124
|
+
| None = None,
|
|
123
125
|
cwd: Path,
|
|
124
126
|
messages: Sequence[Mapping[str, object]],
|
|
125
127
|
extra_tool_runner: Callable[[str, dict[str, object]], Awaitable[ToolResult | None]]
|
|
@@ -252,6 +254,9 @@ async def run_agent_stream(
|
|
|
252
254
|
if not tool_calls:
|
|
253
255
|
if not final_content and not final_thinking:
|
|
254
256
|
raise RuntimeError(EMPTY_MODEL_RESPONSE_ERROR)
|
|
257
|
+
conversation.append({"role": "assistant", "content": final_content})
|
|
258
|
+
if conversation_recorder is not None:
|
|
259
|
+
conversation_recorder([dict(message) for message in conversation])
|
|
255
260
|
logger.info(
|
|
256
261
|
"Agent response completed id=%s rounds=%s content_length=%s thinking_length=%s decision=final_response",
|
|
257
262
|
assistant_id,
|
|
@@ -44,6 +44,7 @@ class AgentMcpManager(Protocol):
|
|
|
44
44
|
@dataclass(frozen=True)
|
|
45
45
|
class AgentRunResult:
|
|
46
46
|
content: str
|
|
47
|
+
history: Sequence[Mapping[str, object]] = ()
|
|
47
48
|
thinking: str = ""
|
|
48
49
|
|
|
49
50
|
|
|
@@ -124,6 +125,8 @@ class FlowentAgentRuntime:
|
|
|
124
125
|
*,
|
|
125
126
|
approval_transcript: Sequence[ApprovalTranscriptEntry] = (),
|
|
126
127
|
connection: ProviderConnection,
|
|
128
|
+
conversation_recorder: Callable[[Sequence[Mapping[str, object]]], None]
|
|
129
|
+
| None = None,
|
|
127
130
|
context_compactor: Callable[
|
|
128
131
|
[Sequence[Mapping[str, object]]], Awaitable[AgentContextUpdate | None]
|
|
129
132
|
]
|
|
@@ -179,6 +182,7 @@ class FlowentAgentRuntime:
|
|
|
179
182
|
async for event in run_agent_stream(
|
|
180
183
|
completion=self.chat_completion,
|
|
181
184
|
connection=connection,
|
|
185
|
+
conversation_recorder=conversation_recorder,
|
|
182
186
|
context_compactor=context_compactor,
|
|
183
187
|
cwd=self.cwd,
|
|
184
188
|
extra_tool_runner=extra_tool_runner,
|
|
@@ -197,13 +201,22 @@ class FlowentAgentRuntime:
|
|
|
197
201
|
approval_transcript: Sequence[ApprovalTranscriptEntry] = (),
|
|
198
202
|
connection: ProviderConnection,
|
|
199
203
|
include_workflow_tools: bool = True,
|
|
204
|
+
history_start_index: int = 0,
|
|
200
205
|
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
201
206
|
user_request: str,
|
|
202
207
|
workflow_depth: int = 0,
|
|
203
208
|
) -> AgentRunResult:
|
|
209
|
+
recorded_conversation: list[Mapping[str, object]] = []
|
|
210
|
+
|
|
211
|
+
def record_conversation(
|
|
212
|
+
conversation: Sequence[Mapping[str, object]],
|
|
213
|
+
) -> None:
|
|
214
|
+
recorded_conversation[:] = [dict(message) for message in conversation]
|
|
215
|
+
|
|
204
216
|
async for event in self.stream(
|
|
205
217
|
approval_transcript=approval_transcript,
|
|
206
218
|
connection=connection,
|
|
219
|
+
conversation_recorder=record_conversation,
|
|
207
220
|
include_workflow_tools=include_workflow_tools,
|
|
208
221
|
messages=messages,
|
|
209
222
|
user_request=user_request,
|
|
@@ -215,6 +228,7 @@ class FlowentAgentRuntime:
|
|
|
215
228
|
if isinstance(message, Mapping):
|
|
216
229
|
return AgentRunResult(
|
|
217
230
|
content=str(message.get("content") or ""),
|
|
231
|
+
history=recorded_conversation[history_start_index:],
|
|
218
232
|
thinking=str(message.get("thinking") or ""),
|
|
219
233
|
)
|
|
220
234
|
raise RuntimeError("Agent did not return a final response.")
|
|
@@ -13,6 +13,14 @@ def migrate(connection: sqlite3.Connection) -> None:
|
|
|
13
13
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
+
CREATE TABLE IF NOT EXISTS workflow_agent_histories (
|
|
17
|
+
workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
18
|
+
node_id TEXT NOT NULL,
|
|
19
|
+
messages TEXT NOT NULL DEFAULT '[]',
|
|
20
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
21
|
+
PRIMARY KEY (workflow_id, node_id)
|
|
22
|
+
);
|
|
23
|
+
|
|
16
24
|
CREATE TABLE IF NOT EXISTS mcp_servers (
|
|
17
25
|
id TEXT PRIMARY KEY,
|
|
18
26
|
name TEXT NOT NULL,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sqlite3
|
|
3
|
+
from collections.abc import Mapping
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
from flowent.llm import ChatMessage, ReasoningEffort
|
|
@@ -172,6 +173,50 @@ class StateStore:
|
|
|
172
173
|
with self.connect() as connection:
|
|
173
174
|
return self._read_workflows(connection)
|
|
174
175
|
|
|
176
|
+
def read_workflow_agent_history(
|
|
177
|
+
self, workflow_id: str, node_id: str
|
|
178
|
+
) -> list[dict[str, object]]:
|
|
179
|
+
with self.connect() as connection:
|
|
180
|
+
row = connection.execute(
|
|
181
|
+
"""
|
|
182
|
+
SELECT messages
|
|
183
|
+
FROM workflow_agent_histories
|
|
184
|
+
WHERE workflow_id = ? AND node_id = ?
|
|
185
|
+
""",
|
|
186
|
+
(workflow_id, node_id),
|
|
187
|
+
).fetchone()
|
|
188
|
+
if row is None:
|
|
189
|
+
return []
|
|
190
|
+
return workflow_agent_history_messages(row["messages"])
|
|
191
|
+
|
|
192
|
+
def save_workflow_agent_history(
|
|
193
|
+
self,
|
|
194
|
+
workflow_id: str,
|
|
195
|
+
node_id: str,
|
|
196
|
+
messages: list[Mapping[str, object]],
|
|
197
|
+
) -> list[dict[str, object]]:
|
|
198
|
+
stored_messages = [dict(message) for message in messages]
|
|
199
|
+
with self.connect() as connection:
|
|
200
|
+
connection.execute(
|
|
201
|
+
"""
|
|
202
|
+
INSERT INTO workflow_agent_histories (
|
|
203
|
+
workflow_id,
|
|
204
|
+
node_id,
|
|
205
|
+
messages
|
|
206
|
+
)
|
|
207
|
+
VALUES (?, ?, ?)
|
|
208
|
+
ON CONFLICT(workflow_id, node_id) DO UPDATE SET
|
|
209
|
+
messages = excluded.messages,
|
|
210
|
+
updated_at = unixepoch()
|
|
211
|
+
""",
|
|
212
|
+
(
|
|
213
|
+
workflow_id,
|
|
214
|
+
node_id,
|
|
215
|
+
json.dumps(stored_messages, ensure_ascii=False),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
return stored_messages
|
|
219
|
+
|
|
175
220
|
def save_workflow(self, workflow: StoredWorkflow) -> StoredWorkflow:
|
|
176
221
|
with self.connect() as connection:
|
|
177
222
|
connection.execute(
|
|
@@ -1017,3 +1062,15 @@ class StateStore:
|
|
|
1017
1062
|
"""
|
|
1018
1063
|
)
|
|
1019
1064
|
]
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def workflow_agent_history_messages(value: str) -> list[dict[str, object]]:
|
|
1068
|
+
parsed = json.loads(value or "[]")
|
|
1069
|
+
if not isinstance(parsed, list):
|
|
1070
|
+
raise ValueError("Workflow agent history must be a list.")
|
|
1071
|
+
messages: list[dict[str, object]] = []
|
|
1072
|
+
for item in parsed:
|
|
1073
|
+
if not isinstance(item, dict):
|
|
1074
|
+
raise ValueError("Workflow agent history messages must be objects.")
|
|
1075
|
+
messages.append(dict(item))
|
|
1076
|
+
return messages
|