flowent 0.3.3 → 0.3.5
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/README.md +3 -0
- package/backend/README.md +3 -0
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +6 -1
- package/backend/src/flowent/agent_runtime.py +234 -0
- package/backend/src/flowent/api_models.py +9 -1
- package/backend/src/flowent/app.py +2 -0
- package/backend/src/flowent/routes/workflow_routes.py +15 -3
- package/backend/src/flowent/state/models.py +1 -1
- 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-CvyP2CdF.css +2 -0
- package/backend/src/flowent/static/assets/index-DAwMtAnm.js +98 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/workflow_service.py +24 -2
- package/backend/src/flowent/workflow_tools.py +16 -8
- package/backend/src/flowent/workflows.py +124 -18
- package/backend/src/flowent/workspace/runtime.py +50 -124
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-CvyP2CdF.css +2 -0
- package/dist/frontend/assets/index-DAwMtAnm.js +98 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-CCf0mo80.css +0 -2
- package/backend/src/flowent/static/assets/index-CROofCFl.js +0 -102
- package/dist/frontend/assets/index-CCf0mo80.css +0 -2
- package/dist/frontend/assets/index-CROofCFl.js +0 -102
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://www.npmjs.com/package/flowent"><img src="https://img.shields.io/npm/v/flowent.svg?style=flat-square&label=npm" alt="npm version" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/flowent"><img src="https://img.shields.io/npm/dm/flowent.svg?style=flat-square&label=npm" alt="npm monthly downloads" /></a>
|
|
7
8
|
<a href="https://pypi.org/project/flowent/"><img src="https://img.shields.io/pypi/v/flowent.svg?style=flat-square&label=PyPI" alt="PyPI version" /></a>
|
|
9
|
+
<a href="https://pypi.org/project/flowent/"><img src="https://img.shields.io/pypi/dm/flowent.svg?style=flat-square&label=PyPI" alt="PyPI monthly downloads" /></a>
|
|
10
|
+
<br />
|
|
8
11
|
<a href="https://github.com/ImFeH2/flowent/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/flowent.svg?style=flat-square&label=License" alt="License" /></a>
|
|
9
12
|
<a href="https://github.com/ImFeH2/flowent/actions/workflows/ci.yml"><img src="https://github.com/ImFeH2/flowent/workflows/CI/badge.svg" alt="CI" /></a>
|
|
10
13
|
<a href="https://github.com/ImFeH2/flowent/actions/workflows/release.yml"><img src="https://github.com/ImFeH2/flowent/workflows/Release/badge.svg" alt="Release" /></a>
|
package/backend/README.md
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://www.npmjs.com/package/flowent"><img src="https://img.shields.io/npm/v/flowent.svg?style=flat-square&label=npm" alt="npm version" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/flowent"><img src="https://img.shields.io/npm/dm/flowent.svg?style=flat-square&label=npm" alt="npm monthly downloads" /></a>
|
|
7
8
|
<a href="https://pypi.org/project/flowent/"><img src="https://img.shields.io/pypi/v/flowent.svg?style=flat-square&label=PyPI" alt="PyPI version" /></a>
|
|
9
|
+
<a href="https://pypi.org/project/flowent/"><img src="https://img.shields.io/pypi/dm/flowent.svg?style=flat-square&label=PyPI" alt="PyPI monthly downloads" /></a>
|
|
10
|
+
<br />
|
|
8
11
|
<a href="https://github.com/ImFeH2/flowent/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/flowent.svg?style=flat-square&label=License" alt="License" /></a>
|
|
9
12
|
<a href="https://github.com/ImFeH2/flowent/actions/workflows/ci.yml"><img src="https://github.com/ImFeH2/flowent/workflows/CI/badge.svg" alt="CI" /></a>
|
|
10
13
|
<a href="https://github.com/ImFeH2/flowent/actions/workflows/release.yml"><img src="https://github.com/ImFeH2/flowent/workflows/Release/badge.svg" alt="Release" /></a>
|
package/backend/pyproject.toml
CHANGED
|
@@ -47,7 +47,7 @@ Use tools deliberately:
|
|
|
47
47
|
- Run shell commands for diagnostics, builds, tests, and operations that require the local environment.
|
|
48
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
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
|
|
50
|
+
- When running a workflow and the user's current message contains one plain value to process, pass that content as the run_workflow input. When a workflow has multiple input nodes or the user provides separate values, use inputs with the exact input node ids from get_workflow.
|
|
51
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
52
|
- Do not delete workflows. If the user asks to delete a workflow, say that you cannot do that directly.
|
|
53
53
|
- Search the web only when current external information is needed.
|
|
@@ -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,
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import (
|
|
4
|
+
AsyncIterator,
|
|
5
|
+
Awaitable,
|
|
6
|
+
Callable,
|
|
7
|
+
Mapping,
|
|
8
|
+
Sequence,
|
|
9
|
+
)
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Protocol
|
|
13
|
+
|
|
14
|
+
from flowent.agent import AgentContextUpdate, AgentStreamEvent, run_agent_stream
|
|
15
|
+
from flowent.approval import (
|
|
16
|
+
ApprovalReviewRequest,
|
|
17
|
+
ApprovalTranscriptEntry,
|
|
18
|
+
review_approval_request,
|
|
19
|
+
)
|
|
20
|
+
from flowent.llm import ChatMessage, CompletionCallable, ProviderConnection
|
|
21
|
+
from flowent.permissions import run_tool_with_path_permissions
|
|
22
|
+
from flowent.storage import StateStore
|
|
23
|
+
from flowent.tools import ToolContext, ToolResult, tool_specs
|
|
24
|
+
from flowent.workflow_tools import (
|
|
25
|
+
WorkflowAgentTools,
|
|
26
|
+
workflow_tool_specs,
|
|
27
|
+
workflow_tool_title,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from flowent.workflow_service import WorkflowService
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentMcpManager(Protocol):
|
|
35
|
+
def tool_specs(self) -> Sequence[Mapping[str, object]]: ...
|
|
36
|
+
|
|
37
|
+
def tool_title(self, name: str) -> str | None: ...
|
|
38
|
+
|
|
39
|
+
async def run_tool(
|
|
40
|
+
self, name: str, arguments: dict[str, object]
|
|
41
|
+
) -> ToolResult | None: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class AgentRunResult:
|
|
46
|
+
content: str
|
|
47
|
+
history: Sequence[Mapping[str, object]] = ()
|
|
48
|
+
thinking: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FlowentAgentRuntime:
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
chat_completion: CompletionCallable | None,
|
|
56
|
+
cwd: Path,
|
|
57
|
+
mcp_manager: AgentMcpManager | None,
|
|
58
|
+
store: StateStore,
|
|
59
|
+
workflow_service: WorkflowService | None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.chat_completion = chat_completion
|
|
62
|
+
self.cwd = cwd
|
|
63
|
+
self.mcp_manager = mcp_manager
|
|
64
|
+
self.store = store
|
|
65
|
+
self.workflow_service = workflow_service
|
|
66
|
+
|
|
67
|
+
def extra_tool_specs(
|
|
68
|
+
self, *, include_workflow_tools: bool = True
|
|
69
|
+
) -> list[Mapping[str, object]]:
|
|
70
|
+
return [
|
|
71
|
+
*(
|
|
72
|
+
workflow_tool_specs()
|
|
73
|
+
if include_workflow_tools and self.workflow_service is not None
|
|
74
|
+
else []
|
|
75
|
+
),
|
|
76
|
+
*list(
|
|
77
|
+
self.mcp_manager.tool_specs() if self.mcp_manager is not None else []
|
|
78
|
+
),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
def model_tool_specs(
|
|
82
|
+
self, *, include_workflow_tools: bool = True
|
|
83
|
+
) -> list[Mapping[str, object]]:
|
|
84
|
+
return [
|
|
85
|
+
*tool_specs(),
|
|
86
|
+
*self.extra_tool_specs(include_workflow_tools=include_workflow_tools),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
def extra_tool_title(self, name: str) -> str | None:
|
|
90
|
+
return (
|
|
91
|
+
workflow_tool_title(name) if self.workflow_service is not None else None
|
|
92
|
+
) or (
|
|
93
|
+
self.mcp_manager.tool_title(name) if self.mcp_manager is not None else None
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def workflow_tools(self, workflow_depth: int = 0) -> WorkflowAgentTools | None:
|
|
97
|
+
if self.workflow_service is None:
|
|
98
|
+
return None
|
|
99
|
+
return WorkflowAgentTools(
|
|
100
|
+
self.workflow_service,
|
|
101
|
+
workflow_depth=workflow_depth,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
async def run_extra_tool(
|
|
105
|
+
self,
|
|
106
|
+
name: str,
|
|
107
|
+
arguments: dict[str, object],
|
|
108
|
+
*,
|
|
109
|
+
include_workflow_tools: bool = True,
|
|
110
|
+
workflow_depth: int = 0,
|
|
111
|
+
) -> ToolResult | None:
|
|
112
|
+
workflow_tools = (
|
|
113
|
+
self.workflow_tools(workflow_depth) if include_workflow_tools else None
|
|
114
|
+
)
|
|
115
|
+
if workflow_tools is not None:
|
|
116
|
+
workflow_result = await workflow_tools.run_tool(name, arguments)
|
|
117
|
+
if workflow_result is not None:
|
|
118
|
+
return workflow_result
|
|
119
|
+
if self.mcp_manager is None:
|
|
120
|
+
return None
|
|
121
|
+
return await self.mcp_manager.run_tool(name, arguments)
|
|
122
|
+
|
|
123
|
+
async def stream(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
approval_transcript: Sequence[ApprovalTranscriptEntry] = (),
|
|
127
|
+
connection: ProviderConnection,
|
|
128
|
+
conversation_recorder: Callable[[Sequence[Mapping[str, object]]], None]
|
|
129
|
+
| None = None,
|
|
130
|
+
context_compactor: Callable[
|
|
131
|
+
[Sequence[Mapping[str, object]]], Awaitable[AgentContextUpdate | None]
|
|
132
|
+
]
|
|
133
|
+
| None = None,
|
|
134
|
+
include_workflow_tools: bool = True,
|
|
135
|
+
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
136
|
+
user_request: str,
|
|
137
|
+
workflow_depth: int = 0,
|
|
138
|
+
) -> AsyncIterator[AgentStreamEvent]:
|
|
139
|
+
transcript = list(approval_transcript)
|
|
140
|
+
request_messages = [
|
|
141
|
+
message.model_dump() if isinstance(message, ChatMessage) else dict(message)
|
|
142
|
+
for message in messages
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
async def review_tool_approval(request: ApprovalReviewRequest):
|
|
146
|
+
return await review_approval_request(
|
|
147
|
+
connection,
|
|
148
|
+
request.model_copy(
|
|
149
|
+
update={
|
|
150
|
+
"transcript": transcript,
|
|
151
|
+
"user_request": user_request,
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
completion=self.chat_completion,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def tool_runner(
|
|
158
|
+
name: str,
|
|
159
|
+
arguments: dict[str, object],
|
|
160
|
+
context: ToolContext,
|
|
161
|
+
) -> ToolResult:
|
|
162
|
+
return await run_tool_with_path_permissions(
|
|
163
|
+
name,
|
|
164
|
+
arguments,
|
|
165
|
+
context,
|
|
166
|
+
review_approval=review_tool_approval,
|
|
167
|
+
writable_paths=[
|
|
168
|
+
Path(path.path) for path in self.store.read_writable_paths()
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
async def extra_tool_runner(
|
|
173
|
+
name: str, arguments: dict[str, object]
|
|
174
|
+
) -> ToolResult | None:
|
|
175
|
+
return await self.run_extra_tool(
|
|
176
|
+
name,
|
|
177
|
+
arguments,
|
|
178
|
+
include_workflow_tools=include_workflow_tools,
|
|
179
|
+
workflow_depth=workflow_depth,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async for event in run_agent_stream(
|
|
183
|
+
completion=self.chat_completion,
|
|
184
|
+
connection=connection,
|
|
185
|
+
conversation_recorder=conversation_recorder,
|
|
186
|
+
context_compactor=context_compactor,
|
|
187
|
+
cwd=self.cwd,
|
|
188
|
+
extra_tool_runner=extra_tool_runner,
|
|
189
|
+
extra_tool_specs=self.extra_tool_specs(
|
|
190
|
+
include_workflow_tools=include_workflow_tools
|
|
191
|
+
),
|
|
192
|
+
extra_tool_title=self.extra_tool_title,
|
|
193
|
+
messages=request_messages,
|
|
194
|
+
tool_runner=tool_runner,
|
|
195
|
+
):
|
|
196
|
+
yield event
|
|
197
|
+
|
|
198
|
+
async def complete(
|
|
199
|
+
self,
|
|
200
|
+
*,
|
|
201
|
+
approval_transcript: Sequence[ApprovalTranscriptEntry] = (),
|
|
202
|
+
connection: ProviderConnection,
|
|
203
|
+
include_workflow_tools: bool = True,
|
|
204
|
+
history_start_index: int = 0,
|
|
205
|
+
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
206
|
+
user_request: str,
|
|
207
|
+
workflow_depth: int = 0,
|
|
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
|
+
|
|
216
|
+
async for event in self.stream(
|
|
217
|
+
approval_transcript=approval_transcript,
|
|
218
|
+
connection=connection,
|
|
219
|
+
conversation_recorder=record_conversation,
|
|
220
|
+
include_workflow_tools=include_workflow_tools,
|
|
221
|
+
messages=messages,
|
|
222
|
+
user_request=user_request,
|
|
223
|
+
workflow_depth=workflow_depth,
|
|
224
|
+
):
|
|
225
|
+
if event.event != "done":
|
|
226
|
+
continue
|
|
227
|
+
message = event.data.get("message")
|
|
228
|
+
if isinstance(message, Mapping):
|
|
229
|
+
return AgentRunResult(
|
|
230
|
+
content=str(message.get("content") or ""),
|
|
231
|
+
history=recorded_conversation[history_start_index:],
|
|
232
|
+
thinking=str(message.get("thinking") or ""),
|
|
233
|
+
)
|
|
234
|
+
raise RuntimeError("Agent did not return a final response.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel, ConfigDict
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
4
|
|
|
5
5
|
from flowent.llm import ProviderFormat
|
|
6
6
|
from flowent.storage import StoredMessage, StoredWritablePath
|
|
@@ -65,6 +65,14 @@ class WorkspaceClearResponse(BaseModel):
|
|
|
65
65
|
usage_info: TokenUsageInfo | None = None
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
class WorkflowRunRequest(BaseModel):
|
|
69
|
+
model_config = ConfigDict(extra="forbid")
|
|
70
|
+
|
|
71
|
+
input: str = ""
|
|
72
|
+
inputs: dict[str, str] = Field(default_factory=dict)
|
|
73
|
+
timer_id: str = ""
|
|
74
|
+
|
|
75
|
+
|
|
68
76
|
class AboutResponse(BaseModel):
|
|
69
77
|
model_config = ConfigDict(extra="forbid")
|
|
70
78
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
from fastapi import FastAPI, HTTPException
|
|
1
|
+
from fastapi import Body, FastAPI, HTTPException
|
|
2
2
|
|
|
3
|
+
from flowent.api_models import WorkflowRunRequest
|
|
3
4
|
from flowent.storage import StoredWorkflow
|
|
4
5
|
from flowent.workflow_service import WorkflowService
|
|
5
6
|
from flowent.workflows import WorkflowRunResponse
|
|
6
7
|
|
|
8
|
+
OPTIONAL_WORKFLOW_RUN_BODY = Body(default=None)
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
def register_workflow_routes(
|
|
9
12
|
app: FastAPI,
|
|
@@ -23,9 +26,18 @@ def register_workflow_routes(
|
|
|
23
26
|
return {"ok": True}
|
|
24
27
|
|
|
25
28
|
@app.post("/api/workflows/{workflow_id}/run")
|
|
26
|
-
async def run_workflow(
|
|
29
|
+
async def run_workflow(
|
|
30
|
+
workflow_id: str,
|
|
31
|
+
request: WorkflowRunRequest | None = OPTIONAL_WORKFLOW_RUN_BODY,
|
|
32
|
+
) -> WorkflowRunResponse:
|
|
33
|
+
run_request = request or WorkflowRunRequest()
|
|
27
34
|
try:
|
|
28
|
-
return await workflow_service.run_workflow(
|
|
35
|
+
return await workflow_service.run_workflow(
|
|
36
|
+
workflow_id,
|
|
37
|
+
default_input=run_request.input,
|
|
38
|
+
input_values=run_request.inputs,
|
|
39
|
+
timer_node_id=run_request.timer_id,
|
|
40
|
+
)
|
|
29
41
|
except ValueError as error:
|
|
30
42
|
status_code = 404 if str(error) == "Workflow not found." else 400
|
|
31
43
|
raise HTTPException(status_code=status_code, detail=str(error)) from error
|
|
@@ -90,7 +90,7 @@ class StoredWorkflowNode(BaseModel):
|
|
|
90
90
|
position: StoredWorkflowNodePosition = Field(
|
|
91
91
|
default_factory=StoredWorkflowNodePosition
|
|
92
92
|
)
|
|
93
|
-
type: Literal["input", "agent", "merge", "code", "output"]
|
|
93
|
+
type: Literal["input", "agent", "merge", "code", "timer", "output"]
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
class StoredWorkflowEdge(BaseModel):
|
|
@@ -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
|