flowent 0.2.4 → 0.3.1
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 -3
- package/backend/README.md +3 -3
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +1 -1
- package/backend/src/flowent/api_models.py +108 -0
- package/backend/src/flowent/app.py +151 -0
- package/backend/src/flowent/cli.py +13 -4
- package/backend/src/flowent/compact.py +34 -13
- package/backend/src/flowent/llm.py +52 -6
- package/backend/src/flowent/main.py +18 -1994
- package/backend/src/flowent/mcp.py +100 -2
- package/backend/src/flowent/network.py +5 -0
- package/backend/src/flowent/provider_connections.py +42 -0
- package/backend/src/flowent/routes/__init__.py +0 -0
- package/backend/src/flowent/routes/integrations.py +105 -0
- package/backend/src/flowent/routes/permissions.py +36 -0
- package/backend/src/flowent/routes/providers.py +53 -0
- package/backend/src/flowent/routes/system.py +48 -0
- package/backend/src/flowent/routes/workflow_routes.py +63 -0
- package/backend/src/flowent/routes/workspace.py +115 -0
- package/backend/src/flowent/state/__init__.py +53 -0
- package/backend/src/flowent/state/models.py +258 -0
- package/backend/src/flowent/state/schema.py +191 -0
- package/backend/src/flowent/state/store.py +1019 -0
- package/backend/src/flowent/static/assets/index-BaZmIi2Y.js +98 -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/storage.py +52 -1318
- package/backend/src/flowent/system_tools.py +25 -0
- package/backend/src/flowent/tools.py +4 -2
- package/backend/src/flowent/usage.py +9 -4
- package/backend/src/flowent/workflows.py +282 -0
- package/backend/src/flowent/workspace/__init__.py +0 -0
- package/backend/src/flowent/workspace/context.py +335 -0
- package/backend/src/flowent/workspace/events.py +178 -0
- package/backend/src/flowent/workspace/output.py +396 -0
- package/backend/src/flowent/workspace/runtime.py +1160 -0
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-BaZmIi2Y.js +98 -0
- package/dist/frontend/assets/index-EC37agAH.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-BH30iLzb.css +0 -2
- package/backend/src/flowent/static/assets/index-sBFt3ORj.js +0 -84
- package/dist/frontend/assets/index-BH30iLzb.css +0 -2
- package/dist/frontend/assets/index-sBFt3ORj.js +0 -84
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from flowent.llm import ChatMessage, ReasoningEffort
|
|
6
|
+
from flowent.paths import data_directory
|
|
7
|
+
from flowent.state.models import (
|
|
8
|
+
StoredAssistantOutputGroup,
|
|
9
|
+
StoredCompactionCheckpoint,
|
|
10
|
+
StoredMcpServer,
|
|
11
|
+
StoredMcpTool,
|
|
12
|
+
StoredMessage,
|
|
13
|
+
StoredProvider,
|
|
14
|
+
StoredSettings,
|
|
15
|
+
StoredState,
|
|
16
|
+
StoredTelegramBot,
|
|
17
|
+
StoredTelegramSession,
|
|
18
|
+
StoredToolItem,
|
|
19
|
+
StoredWorkflow,
|
|
20
|
+
StoredWorkflowDefinition,
|
|
21
|
+
StoredWritablePath,
|
|
22
|
+
)
|
|
23
|
+
from flowent.state.schema import migrate
|
|
24
|
+
from flowent.usage import TokenUsageInfo
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StateStore:
|
|
28
|
+
def __init__(self, directory: Path | None = None) -> None:
|
|
29
|
+
self.directory = directory or data_directory()
|
|
30
|
+
self.database_path = self.directory / "flowent.db"
|
|
31
|
+
|
|
32
|
+
def connect(self) -> sqlite3.Connection:
|
|
33
|
+
self.directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
34
|
+
connection = sqlite3.connect(self.database_path)
|
|
35
|
+
connection.row_factory = sqlite3.Row
|
|
36
|
+
connection.execute("PRAGMA foreign_keys = ON")
|
|
37
|
+
connection.execute("PRAGMA journal_mode = WAL")
|
|
38
|
+
connection.execute("PRAGMA busy_timeout = 5000")
|
|
39
|
+
migrate(connection)
|
|
40
|
+
return connection
|
|
41
|
+
|
|
42
|
+
def read_state(self) -> StoredState:
|
|
43
|
+
with self.connect() as connection:
|
|
44
|
+
mcp_servers = self._read_mcp_servers(connection)
|
|
45
|
+
telegram_bot = self._read_telegram_bot(connection)
|
|
46
|
+
writable_paths = self._read_writable_paths(connection)
|
|
47
|
+
workflows = self._read_workflows(connection)
|
|
48
|
+
providers = [
|
|
49
|
+
StoredProvider(
|
|
50
|
+
api_key=row["api_key"],
|
|
51
|
+
base_url=row["base_url"],
|
|
52
|
+
id=row["id"],
|
|
53
|
+
models=self._provider_models(connection, row["id"]),
|
|
54
|
+
name=row["name"],
|
|
55
|
+
type=row["type"],
|
|
56
|
+
)
|
|
57
|
+
for row in connection.execute(
|
|
58
|
+
"""
|
|
59
|
+
SELECT id, name, type, base_url, api_key
|
|
60
|
+
FROM providers
|
|
61
|
+
ORDER BY created_at, id
|
|
62
|
+
"""
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
settings_row = connection.execute(
|
|
66
|
+
"""
|
|
67
|
+
SELECT selected_provider_id, selected_model, reasoning_effort, agent_prompt, context_window_limit
|
|
68
|
+
FROM settings
|
|
69
|
+
WHERE id = 1
|
|
70
|
+
"""
|
|
71
|
+
).fetchone()
|
|
72
|
+
messages = [
|
|
73
|
+
StoredMessage(
|
|
74
|
+
author=row["author"],
|
|
75
|
+
content=row["content"],
|
|
76
|
+
groups=[
|
|
77
|
+
StoredAssistantOutputGroup.model_validate(group)
|
|
78
|
+
for group in json.loads(row["groups"] or "[]")
|
|
79
|
+
],
|
|
80
|
+
id=row["id"],
|
|
81
|
+
status=row["status"],
|
|
82
|
+
summary=row["summary"],
|
|
83
|
+
thinking=row["thinking"],
|
|
84
|
+
tools=[
|
|
85
|
+
StoredToolItem.model_validate(tool)
|
|
86
|
+
for tool in json.loads(row["tools"] or "[]")
|
|
87
|
+
],
|
|
88
|
+
usage_info=TokenUsageInfo.model_validate_json(row["usage_info"])
|
|
89
|
+
if row["usage_info"]
|
|
90
|
+
else None,
|
|
91
|
+
)
|
|
92
|
+
for row in connection.execute(
|
|
93
|
+
"""
|
|
94
|
+
SELECT id, author, content, summary, tools, thinking, groups, status, usage_info
|
|
95
|
+
FROM messages
|
|
96
|
+
ORDER BY position, id
|
|
97
|
+
"""
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
usage_row = connection.execute(
|
|
101
|
+
"""
|
|
102
|
+
SELECT is_compacting, usage_info
|
|
103
|
+
FROM workspace_context
|
|
104
|
+
WHERE id = 1
|
|
105
|
+
"""
|
|
106
|
+
).fetchone()
|
|
107
|
+
usage_info = (
|
|
108
|
+
TokenUsageInfo.model_validate_json(usage_row["usage_info"])
|
|
109
|
+
if usage_row and usage_row["usage_info"]
|
|
110
|
+
else None
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return StoredState(
|
|
114
|
+
mcp_servers=mcp_servers,
|
|
115
|
+
is_compacting=bool(usage_row["is_compacting"]) if usage_row else False,
|
|
116
|
+
messages=messages,
|
|
117
|
+
providers=providers,
|
|
118
|
+
settings=StoredSettings(
|
|
119
|
+
agent_prompt=settings_row["agent_prompt"] if settings_row else "",
|
|
120
|
+
context_window_limit=settings_row["context_window_limit"]
|
|
121
|
+
if settings_row
|
|
122
|
+
else None,
|
|
123
|
+
reasoning_effort=settings_row["reasoning_effort"]
|
|
124
|
+
if settings_row
|
|
125
|
+
else ReasoningEffort.DEFAULT,
|
|
126
|
+
selected_model=settings_row["selected_model"] if settings_row else "",
|
|
127
|
+
selected_provider_id=settings_row["selected_provider_id"]
|
|
128
|
+
if settings_row
|
|
129
|
+
else "",
|
|
130
|
+
),
|
|
131
|
+
skills=[],
|
|
132
|
+
telegram_bot=telegram_bot,
|
|
133
|
+
usage_info=usage_info,
|
|
134
|
+
writable_paths=writable_paths,
|
|
135
|
+
workflows=workflows,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def read_writable_paths(self) -> list[StoredWritablePath]:
|
|
139
|
+
with self.connect() as connection:
|
|
140
|
+
return self._read_writable_paths(connection)
|
|
141
|
+
|
|
142
|
+
def save_writable_path(self, path: Path) -> StoredWritablePath:
|
|
143
|
+
normalized_path = str(path.expanduser().resolve(strict=False))
|
|
144
|
+
with self.connect() as connection:
|
|
145
|
+
connection.execute(
|
|
146
|
+
"""
|
|
147
|
+
INSERT INTO writable_paths (path)
|
|
148
|
+
VALUES (?)
|
|
149
|
+
ON CONFLICT(path) DO NOTHING
|
|
150
|
+
""",
|
|
151
|
+
(normalized_path,),
|
|
152
|
+
)
|
|
153
|
+
row = connection.execute(
|
|
154
|
+
"""
|
|
155
|
+
SELECT path, created_at
|
|
156
|
+
FROM writable_paths
|
|
157
|
+
WHERE path = ?
|
|
158
|
+
""",
|
|
159
|
+
(normalized_path,),
|
|
160
|
+
).fetchone()
|
|
161
|
+
return StoredWritablePath(path=row["path"], created_at=row["created_at"])
|
|
162
|
+
|
|
163
|
+
def delete_writable_path(self, path: Path) -> list[StoredWritablePath]:
|
|
164
|
+
normalized_path = str(path.expanduser().resolve(strict=False))
|
|
165
|
+
with self.connect() as connection:
|
|
166
|
+
connection.execute(
|
|
167
|
+
"DELETE FROM writable_paths WHERE path = ?", (normalized_path,)
|
|
168
|
+
)
|
|
169
|
+
return self._read_writable_paths(connection)
|
|
170
|
+
|
|
171
|
+
def read_workflows(self) -> list[StoredWorkflow]:
|
|
172
|
+
with self.connect() as connection:
|
|
173
|
+
return self._read_workflows(connection)
|
|
174
|
+
|
|
175
|
+
def save_workflow(self, workflow: StoredWorkflow) -> StoredWorkflow:
|
|
176
|
+
with self.connect() as connection:
|
|
177
|
+
connection.execute(
|
|
178
|
+
"""
|
|
179
|
+
INSERT INTO workflows (
|
|
180
|
+
id,
|
|
181
|
+
name,
|
|
182
|
+
definition
|
|
183
|
+
)
|
|
184
|
+
VALUES (?, ?, ?)
|
|
185
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
186
|
+
name = excluded.name,
|
|
187
|
+
definition = excluded.definition,
|
|
188
|
+
updated_at = unixepoch()
|
|
189
|
+
""",
|
|
190
|
+
(
|
|
191
|
+
workflow.id,
|
|
192
|
+
workflow.name,
|
|
193
|
+
workflow.definition.model_dump_json(),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
row = connection.execute(
|
|
197
|
+
"""
|
|
198
|
+
SELECT id, name, definition, created_at, updated_at
|
|
199
|
+
FROM workflows
|
|
200
|
+
WHERE id = ?
|
|
201
|
+
""",
|
|
202
|
+
(workflow.id,),
|
|
203
|
+
).fetchone()
|
|
204
|
+
return self._workflow_from_row(row)
|
|
205
|
+
|
|
206
|
+
def delete_workflow(self, workflow_id: str) -> None:
|
|
207
|
+
with self.connect() as connection:
|
|
208
|
+
connection.execute("DELETE FROM workflows WHERE id = ?", (workflow_id,))
|
|
209
|
+
|
|
210
|
+
def read_skill_enabled(self) -> dict[str, bool]:
|
|
211
|
+
with self.connect() as connection:
|
|
212
|
+
return {
|
|
213
|
+
row["id"]: bool(row["enabled"])
|
|
214
|
+
for row in connection.execute(
|
|
215
|
+
"""
|
|
216
|
+
SELECT id, enabled
|
|
217
|
+
FROM skill_settings
|
|
218
|
+
ORDER BY id
|
|
219
|
+
"""
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
def save_skill_enabled(self, skill_id: str, enabled: bool) -> None:
|
|
224
|
+
with self.connect() as connection:
|
|
225
|
+
connection.execute(
|
|
226
|
+
"""
|
|
227
|
+
INSERT INTO skill_settings (id, enabled)
|
|
228
|
+
VALUES (?, ?)
|
|
229
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
230
|
+
enabled = excluded.enabled,
|
|
231
|
+
updated_at = unixepoch()
|
|
232
|
+
""",
|
|
233
|
+
(skill_id, int(enabled)),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def read_mcp_servers(self) -> list[StoredMcpServer]:
|
|
237
|
+
with self.connect() as connection:
|
|
238
|
+
return self._read_mcp_servers(connection)
|
|
239
|
+
|
|
240
|
+
def save_mcp_server(self, server: StoredMcpServer) -> StoredMcpServer:
|
|
241
|
+
with self.connect() as connection:
|
|
242
|
+
connection.execute(
|
|
243
|
+
"""
|
|
244
|
+
INSERT INTO mcp_servers (
|
|
245
|
+
id,
|
|
246
|
+
name,
|
|
247
|
+
type,
|
|
248
|
+
command,
|
|
249
|
+
args,
|
|
250
|
+
config,
|
|
251
|
+
url,
|
|
252
|
+
enabled
|
|
253
|
+
)
|
|
254
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
255
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
256
|
+
name = excluded.name,
|
|
257
|
+
type = excluded.type,
|
|
258
|
+
command = excluded.command,
|
|
259
|
+
args = excluded.args,
|
|
260
|
+
config = excluded.config,
|
|
261
|
+
url = excluded.url,
|
|
262
|
+
enabled = excluded.enabled,
|
|
263
|
+
updated_at = unixepoch()
|
|
264
|
+
""",
|
|
265
|
+
(
|
|
266
|
+
server.id,
|
|
267
|
+
server.name,
|
|
268
|
+
server.type,
|
|
269
|
+
server.command,
|
|
270
|
+
json.dumps(server.args),
|
|
271
|
+
json.dumps(server.config, ensure_ascii=False),
|
|
272
|
+
server.url,
|
|
273
|
+
int(server.enabled),
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
existing = [
|
|
277
|
+
current_server
|
|
278
|
+
for current_server in self._read_mcp_servers(connection)
|
|
279
|
+
if current_server.id == server.id
|
|
280
|
+
]
|
|
281
|
+
return existing[0] if existing else server
|
|
282
|
+
|
|
283
|
+
def save_mcp_tools(
|
|
284
|
+
self, server_id: str, tools: list[StoredMcpTool]
|
|
285
|
+
) -> list[StoredMcpTool]:
|
|
286
|
+
with self.connect() as connection:
|
|
287
|
+
connection.execute(
|
|
288
|
+
"DELETE FROM mcp_tools WHERE server_id = ?", (server_id,)
|
|
289
|
+
)
|
|
290
|
+
connection.executemany(
|
|
291
|
+
"""
|
|
292
|
+
INSERT INTO mcp_tools (
|
|
293
|
+
server_id,
|
|
294
|
+
name,
|
|
295
|
+
description,
|
|
296
|
+
input_schema,
|
|
297
|
+
output_schema,
|
|
298
|
+
position
|
|
299
|
+
)
|
|
300
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
301
|
+
""",
|
|
302
|
+
[
|
|
303
|
+
(
|
|
304
|
+
server_id,
|
|
305
|
+
tool.name,
|
|
306
|
+
tool.description,
|
|
307
|
+
json.dumps(tool.input_schema),
|
|
308
|
+
json.dumps(tool.output_schema)
|
|
309
|
+
if tool.output_schema is not None
|
|
310
|
+
else None,
|
|
311
|
+
position,
|
|
312
|
+
)
|
|
313
|
+
for position, tool in enumerate(tools)
|
|
314
|
+
],
|
|
315
|
+
)
|
|
316
|
+
return tools
|
|
317
|
+
|
|
318
|
+
def delete_mcp_server(self, server_id: str) -> None:
|
|
319
|
+
with self.connect() as connection:
|
|
320
|
+
connection.execute("DELETE FROM mcp_servers WHERE id = ?", (server_id,))
|
|
321
|
+
|
|
322
|
+
def read_telegram_bot(self) -> StoredTelegramBot:
|
|
323
|
+
with self.connect() as connection:
|
|
324
|
+
return self._read_telegram_bot(connection)
|
|
325
|
+
|
|
326
|
+
def save_telegram_bot(self, telegram_bot: StoredTelegramBot) -> StoredTelegramBot:
|
|
327
|
+
with self.connect() as connection:
|
|
328
|
+
connection.execute(
|
|
329
|
+
"""
|
|
330
|
+
INSERT INTO telegram_bot (
|
|
331
|
+
id,
|
|
332
|
+
enabled,
|
|
333
|
+
bot_token
|
|
334
|
+
)
|
|
335
|
+
VALUES (1, ?, ?)
|
|
336
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
337
|
+
enabled = excluded.enabled,
|
|
338
|
+
bot_token = excluded.bot_token,
|
|
339
|
+
updated_at = unixepoch()
|
|
340
|
+
""",
|
|
341
|
+
(
|
|
342
|
+
int(telegram_bot.enabled),
|
|
343
|
+
telegram_bot.bot_token,
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
return self._read_telegram_bot(connection)
|
|
347
|
+
|
|
348
|
+
def save_telegram_session(
|
|
349
|
+
self, session: StoredTelegramSession
|
|
350
|
+
) -> StoredTelegramSession:
|
|
351
|
+
with self.connect() as connection:
|
|
352
|
+
connection.execute(
|
|
353
|
+
"""
|
|
354
|
+
INSERT INTO telegram_sessions (
|
|
355
|
+
chat_id,
|
|
356
|
+
user_id,
|
|
357
|
+
username,
|
|
358
|
+
display_name,
|
|
359
|
+
recent_message,
|
|
360
|
+
status
|
|
361
|
+
)
|
|
362
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
363
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
364
|
+
user_id = excluded.user_id,
|
|
365
|
+
username = excluded.username,
|
|
366
|
+
display_name = excluded.display_name,
|
|
367
|
+
recent_message = excluded.recent_message,
|
|
368
|
+
status = excluded.status,
|
|
369
|
+
updated_at = unixepoch()
|
|
370
|
+
""",
|
|
371
|
+
(
|
|
372
|
+
session.chat_id,
|
|
373
|
+
session.user_id,
|
|
374
|
+
session.username,
|
|
375
|
+
session.display_name,
|
|
376
|
+
session.recent_message,
|
|
377
|
+
session.status,
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
return session
|
|
381
|
+
|
|
382
|
+
def approve_telegram_session(self, chat_id: str) -> StoredTelegramSession:
|
|
383
|
+
with self.connect() as connection:
|
|
384
|
+
connection.execute(
|
|
385
|
+
"""
|
|
386
|
+
UPDATE telegram_sessions
|
|
387
|
+
SET status = 'approved',
|
|
388
|
+
updated_at = unixepoch()
|
|
389
|
+
WHERE chat_id = ?
|
|
390
|
+
""",
|
|
391
|
+
(chat_id,),
|
|
392
|
+
)
|
|
393
|
+
row = connection.execute(
|
|
394
|
+
"""
|
|
395
|
+
SELECT
|
|
396
|
+
chat_id,
|
|
397
|
+
user_id,
|
|
398
|
+
username,
|
|
399
|
+
display_name,
|
|
400
|
+
recent_message,
|
|
401
|
+
status,
|
|
402
|
+
updated_at
|
|
403
|
+
FROM telegram_sessions
|
|
404
|
+
WHERE chat_id = ?
|
|
405
|
+
""",
|
|
406
|
+
(chat_id,),
|
|
407
|
+
).fetchone()
|
|
408
|
+
if row is None:
|
|
409
|
+
raise KeyError(chat_id)
|
|
410
|
+
return StoredTelegramSession(
|
|
411
|
+
chat_id=row["chat_id"],
|
|
412
|
+
display_name=row["display_name"],
|
|
413
|
+
recent_message=row["recent_message"],
|
|
414
|
+
status=row["status"],
|
|
415
|
+
updated_at=row["updated_at"],
|
|
416
|
+
user_id=row["user_id"],
|
|
417
|
+
username=row["username"],
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def save_provider(self, provider: StoredProvider) -> StoredProvider:
|
|
421
|
+
with self.connect() as connection:
|
|
422
|
+
connection.execute(
|
|
423
|
+
"""
|
|
424
|
+
INSERT INTO providers (id, name, type, base_url, api_key)
|
|
425
|
+
VALUES (?, ?, ?, ?, ?)
|
|
426
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
427
|
+
name = excluded.name,
|
|
428
|
+
type = excluded.type,
|
|
429
|
+
base_url = excluded.base_url,
|
|
430
|
+
api_key = excluded.api_key,
|
|
431
|
+
updated_at = unixepoch()
|
|
432
|
+
""",
|
|
433
|
+
(
|
|
434
|
+
provider.id,
|
|
435
|
+
provider.name,
|
|
436
|
+
provider.type.value,
|
|
437
|
+
provider.base_url,
|
|
438
|
+
provider.api_key,
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
connection.execute(
|
|
442
|
+
"DELETE FROM provider_models WHERE provider_id = ?", (provider.id,)
|
|
443
|
+
)
|
|
444
|
+
connection.executemany(
|
|
445
|
+
"""
|
|
446
|
+
INSERT INTO provider_models (provider_id, model, position)
|
|
447
|
+
VALUES (?, ?, ?)
|
|
448
|
+
""",
|
|
449
|
+
[
|
|
450
|
+
(provider.id, model, position)
|
|
451
|
+
for position, model in enumerate(provider.models)
|
|
452
|
+
],
|
|
453
|
+
)
|
|
454
|
+
return provider
|
|
455
|
+
|
|
456
|
+
def delete_provider(self, provider_id: str) -> None:
|
|
457
|
+
with self.connect() as connection:
|
|
458
|
+
settings_row = connection.execute(
|
|
459
|
+
"""
|
|
460
|
+
SELECT selected_provider_id
|
|
461
|
+
FROM settings
|
|
462
|
+
WHERE id = 1
|
|
463
|
+
"""
|
|
464
|
+
).fetchone()
|
|
465
|
+
provider_rows = connection.execute(
|
|
466
|
+
"""
|
|
467
|
+
SELECT id
|
|
468
|
+
FROM providers
|
|
469
|
+
ORDER BY created_at, id
|
|
470
|
+
"""
|
|
471
|
+
).fetchall()
|
|
472
|
+
removed_index = next(
|
|
473
|
+
(
|
|
474
|
+
index
|
|
475
|
+
for index, provider_row in enumerate(provider_rows)
|
|
476
|
+
if provider_row["id"] == provider_id
|
|
477
|
+
),
|
|
478
|
+
-1,
|
|
479
|
+
)
|
|
480
|
+
remaining_provider_ids = [
|
|
481
|
+
provider_row["id"]
|
|
482
|
+
for provider_row in provider_rows
|
|
483
|
+
if provider_row["id"] != provider_id
|
|
484
|
+
]
|
|
485
|
+
connection.execute("DELETE FROM providers WHERE id = ?", (provider_id,))
|
|
486
|
+
if settings_row and settings_row["selected_provider_id"] == provider_id:
|
|
487
|
+
next_provider_id = ""
|
|
488
|
+
if removed_index >= 0:
|
|
489
|
+
next_provider_id = (
|
|
490
|
+
remaining_provider_ids[removed_index]
|
|
491
|
+
if removed_index < len(remaining_provider_ids)
|
|
492
|
+
else remaining_provider_ids[removed_index - 1]
|
|
493
|
+
if remaining_provider_ids
|
|
494
|
+
else ""
|
|
495
|
+
)
|
|
496
|
+
next_model = ""
|
|
497
|
+
if next_provider_id:
|
|
498
|
+
model_row = connection.execute(
|
|
499
|
+
"""
|
|
500
|
+
SELECT model
|
|
501
|
+
FROM provider_models
|
|
502
|
+
WHERE provider_id = ?
|
|
503
|
+
ORDER BY position, model
|
|
504
|
+
LIMIT 1
|
|
505
|
+
""",
|
|
506
|
+
(next_provider_id,),
|
|
507
|
+
).fetchone()
|
|
508
|
+
next_model = model_row["model"] if model_row else ""
|
|
509
|
+
connection.execute(
|
|
510
|
+
"""
|
|
511
|
+
UPDATE settings
|
|
512
|
+
SET selected_provider_id = ?,
|
|
513
|
+
selected_model = ?,
|
|
514
|
+
updated_at = unixepoch()
|
|
515
|
+
WHERE id = 1
|
|
516
|
+
""",
|
|
517
|
+
(next_provider_id, next_model),
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def save_settings(self, settings: StoredSettings) -> StoredSettings:
|
|
521
|
+
with self.connect() as connection:
|
|
522
|
+
connection.execute(
|
|
523
|
+
"""
|
|
524
|
+
INSERT INTO settings (
|
|
525
|
+
id,
|
|
526
|
+
selected_provider_id,
|
|
527
|
+
selected_model,
|
|
528
|
+
reasoning_effort,
|
|
529
|
+
agent_prompt,
|
|
530
|
+
context_window_limit
|
|
531
|
+
)
|
|
532
|
+
VALUES (1, ?, ?, ?, ?, ?)
|
|
533
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
534
|
+
selected_provider_id = excluded.selected_provider_id,
|
|
535
|
+
selected_model = excluded.selected_model,
|
|
536
|
+
reasoning_effort = excluded.reasoning_effort,
|
|
537
|
+
agent_prompt = excluded.agent_prompt,
|
|
538
|
+
context_window_limit = excluded.context_window_limit,
|
|
539
|
+
updated_at = unixepoch()
|
|
540
|
+
""",
|
|
541
|
+
(
|
|
542
|
+
settings.selected_provider_id,
|
|
543
|
+
settings.selected_model,
|
|
544
|
+
settings.reasoning_effort.value,
|
|
545
|
+
settings.agent_prompt,
|
|
546
|
+
settings.context_window_limit,
|
|
547
|
+
),
|
|
548
|
+
)
|
|
549
|
+
return settings
|
|
550
|
+
|
|
551
|
+
def save_messages(self, messages: list[StoredMessage]) -> list[StoredMessage]:
|
|
552
|
+
with self.connect() as connection:
|
|
553
|
+
connection.execute("DELETE FROM messages")
|
|
554
|
+
if messages:
|
|
555
|
+
latest_usage_info = next(
|
|
556
|
+
(
|
|
557
|
+
message.usage_info
|
|
558
|
+
for message in reversed(messages)
|
|
559
|
+
if message.usage_info is not None
|
|
560
|
+
),
|
|
561
|
+
None,
|
|
562
|
+
)
|
|
563
|
+
if latest_usage_info is not None:
|
|
564
|
+
connection.execute(
|
|
565
|
+
"""
|
|
566
|
+
INSERT INTO workspace_context (id, usage_info)
|
|
567
|
+
VALUES (1, ?)
|
|
568
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
569
|
+
usage_info = excluded.usage_info,
|
|
570
|
+
updated_at = unixepoch()
|
|
571
|
+
""",
|
|
572
|
+
(latest_usage_info.model_dump_json(),),
|
|
573
|
+
)
|
|
574
|
+
connection.executemany(
|
|
575
|
+
"""
|
|
576
|
+
INSERT INTO messages (
|
|
577
|
+
id,
|
|
578
|
+
author,
|
|
579
|
+
content,
|
|
580
|
+
summary,
|
|
581
|
+
tools,
|
|
582
|
+
thinking,
|
|
583
|
+
groups,
|
|
584
|
+
status,
|
|
585
|
+
usage_info,
|
|
586
|
+
position
|
|
587
|
+
)
|
|
588
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
589
|
+
""",
|
|
590
|
+
[
|
|
591
|
+
(
|
|
592
|
+
message.id,
|
|
593
|
+
message.author,
|
|
594
|
+
message.content,
|
|
595
|
+
message.summary,
|
|
596
|
+
json.dumps(
|
|
597
|
+
[
|
|
598
|
+
tool.model_dump(exclude_none=True)
|
|
599
|
+
for tool in message.tools
|
|
600
|
+
]
|
|
601
|
+
),
|
|
602
|
+
message.thinking,
|
|
603
|
+
json.dumps(
|
|
604
|
+
[
|
|
605
|
+
group.model_dump(exclude_none=True)
|
|
606
|
+
for group in message.groups
|
|
607
|
+
],
|
|
608
|
+
ensure_ascii=False,
|
|
609
|
+
),
|
|
610
|
+
message.status,
|
|
611
|
+
message.usage_info.model_dump_json()
|
|
612
|
+
if message.usage_info
|
|
613
|
+
else None,
|
|
614
|
+
position,
|
|
615
|
+
)
|
|
616
|
+
for position, message in enumerate(messages)
|
|
617
|
+
],
|
|
618
|
+
)
|
|
619
|
+
if not messages:
|
|
620
|
+
connection.execute("DELETE FROM workspace_context WHERE id = 1")
|
|
621
|
+
return messages
|
|
622
|
+
|
|
623
|
+
def upsert_message(self, message: StoredMessage) -> StoredMessage:
|
|
624
|
+
with self.connect() as connection:
|
|
625
|
+
row = connection.execute(
|
|
626
|
+
"SELECT position FROM messages WHERE id = ?", (message.id,)
|
|
627
|
+
).fetchone()
|
|
628
|
+
if row:
|
|
629
|
+
position = row["position"]
|
|
630
|
+
else:
|
|
631
|
+
position_row = connection.execute(
|
|
632
|
+
"SELECT COALESCE(MAX(position) + 1, 0) AS position FROM messages"
|
|
633
|
+
).fetchone()
|
|
634
|
+
position = position_row["position"]
|
|
635
|
+
connection.execute(
|
|
636
|
+
"""
|
|
637
|
+
INSERT INTO messages (
|
|
638
|
+
id,
|
|
639
|
+
author,
|
|
640
|
+
content,
|
|
641
|
+
summary,
|
|
642
|
+
tools,
|
|
643
|
+
thinking,
|
|
644
|
+
groups,
|
|
645
|
+
status,
|
|
646
|
+
usage_info,
|
|
647
|
+
position
|
|
648
|
+
)
|
|
649
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
650
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
651
|
+
author = excluded.author,
|
|
652
|
+
content = excluded.content,
|
|
653
|
+
summary = excluded.summary,
|
|
654
|
+
tools = excluded.tools,
|
|
655
|
+
thinking = excluded.thinking,
|
|
656
|
+
groups = excluded.groups,
|
|
657
|
+
status = excluded.status,
|
|
658
|
+
usage_info = excluded.usage_info,
|
|
659
|
+
position = excluded.position
|
|
660
|
+
""",
|
|
661
|
+
(
|
|
662
|
+
message.id,
|
|
663
|
+
message.author,
|
|
664
|
+
message.content,
|
|
665
|
+
message.summary,
|
|
666
|
+
json.dumps(
|
|
667
|
+
[tool.model_dump(exclude_none=True) for tool in message.tools]
|
|
668
|
+
),
|
|
669
|
+
message.thinking,
|
|
670
|
+
json.dumps(
|
|
671
|
+
[
|
|
672
|
+
group.model_dump(exclude_none=True)
|
|
673
|
+
for group in message.groups
|
|
674
|
+
],
|
|
675
|
+
ensure_ascii=False,
|
|
676
|
+
),
|
|
677
|
+
message.status,
|
|
678
|
+
message.usage_info.model_dump_json()
|
|
679
|
+
if message.usage_info
|
|
680
|
+
else None,
|
|
681
|
+
position,
|
|
682
|
+
),
|
|
683
|
+
)
|
|
684
|
+
if message.usage_info is not None:
|
|
685
|
+
connection.execute(
|
|
686
|
+
"""
|
|
687
|
+
INSERT INTO workspace_context (id, usage_info)
|
|
688
|
+
VALUES (1, ?)
|
|
689
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
690
|
+
usage_info = excluded.usage_info,
|
|
691
|
+
updated_at = unixepoch()
|
|
692
|
+
""",
|
|
693
|
+
(message.usage_info.model_dump_json(),),
|
|
694
|
+
)
|
|
695
|
+
return message
|
|
696
|
+
|
|
697
|
+
def read_usage_info(self) -> TokenUsageInfo | None:
|
|
698
|
+
with self.connect() as connection:
|
|
699
|
+
row = connection.execute(
|
|
700
|
+
"""
|
|
701
|
+
SELECT usage_info
|
|
702
|
+
FROM workspace_context
|
|
703
|
+
WHERE id = 1
|
|
704
|
+
"""
|
|
705
|
+
).fetchone()
|
|
706
|
+
if row is None or not row["usage_info"]:
|
|
707
|
+
return None
|
|
708
|
+
return TokenUsageInfo.model_validate_json(row["usage_info"])
|
|
709
|
+
|
|
710
|
+
def save_usage_info(self, usage_info: TokenUsageInfo) -> TokenUsageInfo:
|
|
711
|
+
with self.connect() as connection:
|
|
712
|
+
connection.execute(
|
|
713
|
+
"""
|
|
714
|
+
INSERT INTO workspace_context (id, usage_info)
|
|
715
|
+
VALUES (1, ?)
|
|
716
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
717
|
+
usage_info = excluded.usage_info,
|
|
718
|
+
updated_at = unixepoch()
|
|
719
|
+
""",
|
|
720
|
+
(usage_info.model_dump_json(),),
|
|
721
|
+
)
|
|
722
|
+
return usage_info
|
|
723
|
+
|
|
724
|
+
def read_compacted_context(self) -> str:
|
|
725
|
+
with self.connect() as connection:
|
|
726
|
+
row = connection.execute(
|
|
727
|
+
"""
|
|
728
|
+
SELECT compacted_summary
|
|
729
|
+
FROM workspace_context
|
|
730
|
+
WHERE id = 1
|
|
731
|
+
"""
|
|
732
|
+
).fetchone()
|
|
733
|
+
return row["compacted_summary"] if row else ""
|
|
734
|
+
|
|
735
|
+
def save_compacted_context(self, summary: str) -> str:
|
|
736
|
+
with self.connect() as connection:
|
|
737
|
+
connection.execute(
|
|
738
|
+
"""
|
|
739
|
+
INSERT INTO workspace_context (id, compacted_summary)
|
|
740
|
+
VALUES (1, ?)
|
|
741
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
742
|
+
compacted_summary = excluded.compacted_summary,
|
|
743
|
+
active_compaction_id = NULL,
|
|
744
|
+
usage_info = NULL,
|
|
745
|
+
updated_at = unixepoch()
|
|
746
|
+
""",
|
|
747
|
+
(summary,),
|
|
748
|
+
)
|
|
749
|
+
return summary
|
|
750
|
+
|
|
751
|
+
def read_is_compacting(self) -> bool:
|
|
752
|
+
with self.connect() as connection:
|
|
753
|
+
row = connection.execute(
|
|
754
|
+
"""
|
|
755
|
+
SELECT is_compacting
|
|
756
|
+
FROM workspace_context
|
|
757
|
+
WHERE id = 1
|
|
758
|
+
"""
|
|
759
|
+
).fetchone()
|
|
760
|
+
return bool(row["is_compacting"]) if row else False
|
|
761
|
+
|
|
762
|
+
def save_is_compacting(self, is_compacting: bool) -> bool:
|
|
763
|
+
with self.connect() as connection:
|
|
764
|
+
connection.execute(
|
|
765
|
+
"""
|
|
766
|
+
INSERT INTO workspace_context (id, is_compacting)
|
|
767
|
+
VALUES (1, ?)
|
|
768
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
769
|
+
is_compacting = excluded.is_compacting,
|
|
770
|
+
updated_at = unixepoch()
|
|
771
|
+
""",
|
|
772
|
+
(int(is_compacting),),
|
|
773
|
+
)
|
|
774
|
+
return is_compacting
|
|
775
|
+
|
|
776
|
+
def read_active_compaction_checkpoint(
|
|
777
|
+
self,
|
|
778
|
+
) -> StoredCompactionCheckpoint | None:
|
|
779
|
+
with self.connect() as connection:
|
|
780
|
+
row = connection.execute(
|
|
781
|
+
"""
|
|
782
|
+
SELECT
|
|
783
|
+
checkpoint.id,
|
|
784
|
+
checkpoint.trigger,
|
|
785
|
+
checkpoint.method,
|
|
786
|
+
checkpoint.summary,
|
|
787
|
+
checkpoint.replacement_history,
|
|
788
|
+
checkpoint.source_message_id,
|
|
789
|
+
checkpoint.token_before,
|
|
790
|
+
checkpoint.token_after,
|
|
791
|
+
checkpoint.created_at
|
|
792
|
+
FROM workspace_context context
|
|
793
|
+
JOIN compaction_checkpoints checkpoint
|
|
794
|
+
ON checkpoint.id = context.active_compaction_id
|
|
795
|
+
WHERE context.id = 1
|
|
796
|
+
"""
|
|
797
|
+
).fetchone()
|
|
798
|
+
if row is None:
|
|
799
|
+
return None
|
|
800
|
+
return StoredCompactionCheckpoint(
|
|
801
|
+
created_at=row["created_at"],
|
|
802
|
+
id=row["id"],
|
|
803
|
+
method=row["method"],
|
|
804
|
+
replacement_history=[
|
|
805
|
+
ChatMessage.model_validate(message)
|
|
806
|
+
for message in json.loads(row["replacement_history"] or "[]")
|
|
807
|
+
],
|
|
808
|
+
source_message_id=row["source_message_id"],
|
|
809
|
+
summary=row["summary"],
|
|
810
|
+
token_after=row["token_after"],
|
|
811
|
+
token_before=row["token_before"],
|
|
812
|
+
trigger=row["trigger"],
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
def save_compaction_checkpoint(
|
|
816
|
+
self, checkpoint: StoredCompactionCheckpoint
|
|
817
|
+
) -> StoredCompactionCheckpoint:
|
|
818
|
+
with self.connect() as connection:
|
|
819
|
+
connection.execute(
|
|
820
|
+
"""
|
|
821
|
+
INSERT INTO compaction_checkpoints (
|
|
822
|
+
id,
|
|
823
|
+
trigger,
|
|
824
|
+
method,
|
|
825
|
+
summary,
|
|
826
|
+
replacement_history,
|
|
827
|
+
source_message_id,
|
|
828
|
+
token_before,
|
|
829
|
+
token_after
|
|
830
|
+
)
|
|
831
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
832
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
833
|
+
trigger = excluded.trigger,
|
|
834
|
+
method = excluded.method,
|
|
835
|
+
summary = excluded.summary,
|
|
836
|
+
replacement_history = excluded.replacement_history,
|
|
837
|
+
source_message_id = excluded.source_message_id,
|
|
838
|
+
token_before = excluded.token_before,
|
|
839
|
+
token_after = excluded.token_after
|
|
840
|
+
""",
|
|
841
|
+
(
|
|
842
|
+
checkpoint.id,
|
|
843
|
+
checkpoint.trigger,
|
|
844
|
+
checkpoint.method,
|
|
845
|
+
checkpoint.summary,
|
|
846
|
+
json.dumps(
|
|
847
|
+
[
|
|
848
|
+
message.model_dump()
|
|
849
|
+
for message in checkpoint.replacement_history
|
|
850
|
+
],
|
|
851
|
+
ensure_ascii=False,
|
|
852
|
+
),
|
|
853
|
+
checkpoint.source_message_id,
|
|
854
|
+
checkpoint.token_before,
|
|
855
|
+
checkpoint.token_after,
|
|
856
|
+
),
|
|
857
|
+
)
|
|
858
|
+
connection.execute(
|
|
859
|
+
"""
|
|
860
|
+
INSERT INTO workspace_context (
|
|
861
|
+
id,
|
|
862
|
+
compacted_summary,
|
|
863
|
+
active_compaction_id
|
|
864
|
+
)
|
|
865
|
+
VALUES (1, ?, ?)
|
|
866
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
867
|
+
compacted_summary = excluded.compacted_summary,
|
|
868
|
+
active_compaction_id = excluded.active_compaction_id,
|
|
869
|
+
updated_at = unixepoch()
|
|
870
|
+
""",
|
|
871
|
+
(checkpoint.summary, checkpoint.id),
|
|
872
|
+
)
|
|
873
|
+
row = connection.execute(
|
|
874
|
+
"""
|
|
875
|
+
SELECT created_at
|
|
876
|
+
FROM compaction_checkpoints
|
|
877
|
+
WHERE id = ?
|
|
878
|
+
""",
|
|
879
|
+
(checkpoint.id,),
|
|
880
|
+
).fetchone()
|
|
881
|
+
return checkpoint.model_copy(update={"created_at": row["created_at"]})
|
|
882
|
+
|
|
883
|
+
def _provider_models(
|
|
884
|
+
self, connection: sqlite3.Connection, provider_id: str
|
|
885
|
+
) -> list[str]:
|
|
886
|
+
return [
|
|
887
|
+
row["model"]
|
|
888
|
+
for row in connection.execute(
|
|
889
|
+
"""
|
|
890
|
+
SELECT model
|
|
891
|
+
FROM provider_models
|
|
892
|
+
WHERE provider_id = ?
|
|
893
|
+
ORDER BY position, model
|
|
894
|
+
""",
|
|
895
|
+
(provider_id,),
|
|
896
|
+
)
|
|
897
|
+
]
|
|
898
|
+
|
|
899
|
+
def _read_telegram_bot(self, connection: sqlite3.Connection) -> StoredTelegramBot:
|
|
900
|
+
bot_row = connection.execute(
|
|
901
|
+
"""
|
|
902
|
+
SELECT enabled, bot_token
|
|
903
|
+
FROM telegram_bot
|
|
904
|
+
WHERE id = 1
|
|
905
|
+
"""
|
|
906
|
+
).fetchone()
|
|
907
|
+
sessions = [
|
|
908
|
+
StoredTelegramSession(
|
|
909
|
+
chat_id=row["chat_id"],
|
|
910
|
+
display_name=row["display_name"],
|
|
911
|
+
recent_message=row["recent_message"],
|
|
912
|
+
status=row["status"],
|
|
913
|
+
updated_at=row["updated_at"],
|
|
914
|
+
user_id=row["user_id"],
|
|
915
|
+
username=row["username"],
|
|
916
|
+
)
|
|
917
|
+
for row in connection.execute(
|
|
918
|
+
"""
|
|
919
|
+
SELECT
|
|
920
|
+
chat_id,
|
|
921
|
+
user_id,
|
|
922
|
+
username,
|
|
923
|
+
display_name,
|
|
924
|
+
recent_message,
|
|
925
|
+
status,
|
|
926
|
+
updated_at
|
|
927
|
+
FROM telegram_sessions
|
|
928
|
+
ORDER BY status DESC, updated_at DESC, chat_id
|
|
929
|
+
"""
|
|
930
|
+
)
|
|
931
|
+
]
|
|
932
|
+
return StoredTelegramBot(
|
|
933
|
+
bot_token=bot_row["bot_token"] if bot_row else "",
|
|
934
|
+
enabled=bool(bot_row["enabled"]) if bot_row else False,
|
|
935
|
+
sessions=sessions,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
def _read_mcp_servers(
|
|
939
|
+
self, connection: sqlite3.Connection
|
|
940
|
+
) -> list[StoredMcpServer]:
|
|
941
|
+
servers: list[StoredMcpServer] = []
|
|
942
|
+
for row in connection.execute(
|
|
943
|
+
"""
|
|
944
|
+
SELECT id, name, type, command, args, config, url, enabled
|
|
945
|
+
FROM mcp_servers
|
|
946
|
+
ORDER BY created_at, id
|
|
947
|
+
"""
|
|
948
|
+
):
|
|
949
|
+
tools = [
|
|
950
|
+
StoredMcpTool(
|
|
951
|
+
description=tool_row["description"],
|
|
952
|
+
input_schema=json.loads(tool_row["input_schema"] or "{}"),
|
|
953
|
+
name=tool_row["name"],
|
|
954
|
+
output_schema=json.loads(tool_row["output_schema"])
|
|
955
|
+
if tool_row["output_schema"]
|
|
956
|
+
else None,
|
|
957
|
+
)
|
|
958
|
+
for tool_row in connection.execute(
|
|
959
|
+
"""
|
|
960
|
+
SELECT name, description, input_schema, output_schema
|
|
961
|
+
FROM mcp_tools
|
|
962
|
+
WHERE server_id = ?
|
|
963
|
+
ORDER BY position, name
|
|
964
|
+
""",
|
|
965
|
+
(row["id"],),
|
|
966
|
+
)
|
|
967
|
+
]
|
|
968
|
+
servers.append(
|
|
969
|
+
StoredMcpServer(
|
|
970
|
+
args=json.loads(row["args"] or "[]"),
|
|
971
|
+
command=row["command"],
|
|
972
|
+
config=json.loads(row["config"] or "{}"),
|
|
973
|
+
enabled=bool(row["enabled"]),
|
|
974
|
+
id=row["id"],
|
|
975
|
+
name=row["name"],
|
|
976
|
+
status="disabled",
|
|
977
|
+
tools=tools,
|
|
978
|
+
type=row["type"],
|
|
979
|
+
url=row["url"],
|
|
980
|
+
)
|
|
981
|
+
)
|
|
982
|
+
return servers
|
|
983
|
+
|
|
984
|
+
def _read_writable_paths(
|
|
985
|
+
self, connection: sqlite3.Connection
|
|
986
|
+
) -> list[StoredWritablePath]:
|
|
987
|
+
return [
|
|
988
|
+
StoredWritablePath(created_at=row["created_at"], path=row["path"])
|
|
989
|
+
for row in connection.execute(
|
|
990
|
+
"""
|
|
991
|
+
SELECT path, created_at
|
|
992
|
+
FROM writable_paths
|
|
993
|
+
ORDER BY path
|
|
994
|
+
"""
|
|
995
|
+
)
|
|
996
|
+
]
|
|
997
|
+
|
|
998
|
+
def _workflow_from_row(self, row: sqlite3.Row) -> StoredWorkflow:
|
|
999
|
+
return StoredWorkflow(
|
|
1000
|
+
created_at=row["created_at"],
|
|
1001
|
+
definition=StoredWorkflowDefinition.model_validate(
|
|
1002
|
+
json.loads(row["definition"] or "{}")
|
|
1003
|
+
),
|
|
1004
|
+
id=row["id"],
|
|
1005
|
+
name=row["name"],
|
|
1006
|
+
updated_at=row["updated_at"],
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
def _read_workflows(self, connection: sqlite3.Connection) -> list[StoredWorkflow]:
|
|
1010
|
+
return [
|
|
1011
|
+
self._workflow_from_row(row)
|
|
1012
|
+
for row in connection.execute(
|
|
1013
|
+
"""
|
|
1014
|
+
SELECT id, name, definition, created_at, updated_at
|
|
1015
|
+
FROM workflows
|
|
1016
|
+
ORDER BY updated_at DESC, name, id
|
|
1017
|
+
"""
|
|
1018
|
+
)
|
|
1019
|
+
]
|