flowent 0.0.12 → 0.0.13
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/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/channels.py +296 -0
- package/backend/src/flowent/main.py +128 -2
- package/backend/src/flowent/static/assets/index-CEZrWoDG.css +2 -0
- package/backend/src/flowent/static/assets/index-S5a0Rkj1.js +81 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +179 -0
- package/backend/src/flowent/tools.py +28 -5
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/test_agent_tools.py +54 -0
- package/backend/tests/test_channels.py +360 -0
- package/backend/tests/test_persistence.py +30 -0
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-CEZrWoDG.css +2 -0
- package/dist/frontend/assets/index-S5a0Rkj1.js +81 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-BwQOML_0.css +0 -2
- package/backend/src/flowent/static/assets/index-DXQ_smj0.js +0 -81
- package/dist/frontend/assets/index-BwQOML_0.css +0 -2
- package/dist/frontend/assets/index-DXQ_smj0.js +0 -81
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Flowent</title>
|
|
8
8
|
<meta name="description" content="Flowent application" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-S5a0Rkj1.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CEZrWoDG.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -8,6 +8,28 @@ from flowent.llm import ProviderFormat, ReasoningEffort
|
|
|
8
8
|
from flowent.paths import data_directory
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class StoredTelegramSession(BaseModel):
|
|
12
|
+
model_config = ConfigDict(extra="forbid")
|
|
13
|
+
|
|
14
|
+
chat_id: str
|
|
15
|
+
display_name: str = ""
|
|
16
|
+
recent_message: str = ""
|
|
17
|
+
status: str
|
|
18
|
+
updated_at: int = 0
|
|
19
|
+
user_id: str = ""
|
|
20
|
+
username: str = ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StoredTelegramBot(BaseModel):
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
bot_token: str
|
|
27
|
+
enabled: bool
|
|
28
|
+
error: str = ""
|
|
29
|
+
sessions: list[StoredTelegramSession] = Field(default_factory=list)
|
|
30
|
+
status: str = "disabled"
|
|
31
|
+
|
|
32
|
+
|
|
11
33
|
class StoredProvider(BaseModel):
|
|
12
34
|
model_config = ConfigDict(extra="forbid")
|
|
13
35
|
|
|
@@ -55,6 +77,7 @@ class StoredState(BaseModel):
|
|
|
55
77
|
messages: list[StoredMessage]
|
|
56
78
|
providers: list[StoredProvider]
|
|
57
79
|
settings: StoredSettings
|
|
80
|
+
telegram_bot: StoredTelegramBot
|
|
58
81
|
|
|
59
82
|
|
|
60
83
|
class StateStore:
|
|
@@ -74,6 +97,7 @@ class StateStore:
|
|
|
74
97
|
|
|
75
98
|
def read_state(self) -> StoredState:
|
|
76
99
|
with self.connect() as connection:
|
|
100
|
+
telegram_bot = self._read_telegram_bot(connection)
|
|
77
101
|
providers = [
|
|
78
102
|
StoredProvider(
|
|
79
103
|
api_key=row["api_key"],
|
|
@@ -130,6 +154,105 @@ class StateStore:
|
|
|
130
154
|
if settings_row
|
|
131
155
|
else "",
|
|
132
156
|
),
|
|
157
|
+
telegram_bot=telegram_bot,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def read_telegram_bot(self) -> StoredTelegramBot:
|
|
161
|
+
with self.connect() as connection:
|
|
162
|
+
return self._read_telegram_bot(connection)
|
|
163
|
+
|
|
164
|
+
def save_telegram_bot(self, telegram_bot: StoredTelegramBot) -> StoredTelegramBot:
|
|
165
|
+
with self.connect() as connection:
|
|
166
|
+
connection.execute(
|
|
167
|
+
"""
|
|
168
|
+
INSERT INTO telegram_bot (
|
|
169
|
+
id,
|
|
170
|
+
enabled,
|
|
171
|
+
bot_token
|
|
172
|
+
)
|
|
173
|
+
VALUES (1, ?, ?)
|
|
174
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
175
|
+
enabled = excluded.enabled,
|
|
176
|
+
bot_token = excluded.bot_token,
|
|
177
|
+
updated_at = unixepoch()
|
|
178
|
+
""",
|
|
179
|
+
(
|
|
180
|
+
int(telegram_bot.enabled),
|
|
181
|
+
telegram_bot.bot_token,
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
return self._read_telegram_bot(connection)
|
|
185
|
+
|
|
186
|
+
def save_telegram_session(
|
|
187
|
+
self, session: StoredTelegramSession
|
|
188
|
+
) -> StoredTelegramSession:
|
|
189
|
+
with self.connect() as connection:
|
|
190
|
+
connection.execute(
|
|
191
|
+
"""
|
|
192
|
+
INSERT INTO telegram_sessions (
|
|
193
|
+
chat_id,
|
|
194
|
+
user_id,
|
|
195
|
+
username,
|
|
196
|
+
display_name,
|
|
197
|
+
recent_message,
|
|
198
|
+
status
|
|
199
|
+
)
|
|
200
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
201
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
202
|
+
user_id = excluded.user_id,
|
|
203
|
+
username = excluded.username,
|
|
204
|
+
display_name = excluded.display_name,
|
|
205
|
+
recent_message = excluded.recent_message,
|
|
206
|
+
status = excluded.status,
|
|
207
|
+
updated_at = unixepoch()
|
|
208
|
+
""",
|
|
209
|
+
(
|
|
210
|
+
session.chat_id,
|
|
211
|
+
session.user_id,
|
|
212
|
+
session.username,
|
|
213
|
+
session.display_name,
|
|
214
|
+
session.recent_message,
|
|
215
|
+
session.status,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
return session
|
|
219
|
+
|
|
220
|
+
def approve_telegram_session(self, chat_id: str) -> StoredTelegramSession:
|
|
221
|
+
with self.connect() as connection:
|
|
222
|
+
connection.execute(
|
|
223
|
+
"""
|
|
224
|
+
UPDATE telegram_sessions
|
|
225
|
+
SET status = 'approved',
|
|
226
|
+
updated_at = unixepoch()
|
|
227
|
+
WHERE chat_id = ?
|
|
228
|
+
""",
|
|
229
|
+
(chat_id,),
|
|
230
|
+
)
|
|
231
|
+
row = connection.execute(
|
|
232
|
+
"""
|
|
233
|
+
SELECT
|
|
234
|
+
chat_id,
|
|
235
|
+
user_id,
|
|
236
|
+
username,
|
|
237
|
+
display_name,
|
|
238
|
+
recent_message,
|
|
239
|
+
status,
|
|
240
|
+
updated_at
|
|
241
|
+
FROM telegram_sessions
|
|
242
|
+
WHERE chat_id = ?
|
|
243
|
+
""",
|
|
244
|
+
(chat_id,),
|
|
245
|
+
).fetchone()
|
|
246
|
+
if row is None:
|
|
247
|
+
raise KeyError(chat_id)
|
|
248
|
+
return StoredTelegramSession(
|
|
249
|
+
chat_id=row["chat_id"],
|
|
250
|
+
display_name=row["display_name"],
|
|
251
|
+
recent_message=row["recent_message"],
|
|
252
|
+
status=row["status"],
|
|
253
|
+
updated_at=row["updated_at"],
|
|
254
|
+
user_id=row["user_id"],
|
|
255
|
+
username=row["username"],
|
|
133
256
|
)
|
|
134
257
|
|
|
135
258
|
def save_provider(self, provider: StoredProvider) -> StoredProvider:
|
|
@@ -263,9 +386,65 @@ class StateStore:
|
|
|
263
386
|
)
|
|
264
387
|
]
|
|
265
388
|
|
|
389
|
+
def _read_telegram_bot(self, connection: sqlite3.Connection) -> StoredTelegramBot:
|
|
390
|
+
bot_row = connection.execute(
|
|
391
|
+
"""
|
|
392
|
+
SELECT enabled, bot_token
|
|
393
|
+
FROM telegram_bot
|
|
394
|
+
WHERE id = 1
|
|
395
|
+
"""
|
|
396
|
+
).fetchone()
|
|
397
|
+
sessions = [
|
|
398
|
+
StoredTelegramSession(
|
|
399
|
+
chat_id=row["chat_id"],
|
|
400
|
+
display_name=row["display_name"],
|
|
401
|
+
recent_message=row["recent_message"],
|
|
402
|
+
status=row["status"],
|
|
403
|
+
updated_at=row["updated_at"],
|
|
404
|
+
user_id=row["user_id"],
|
|
405
|
+
username=row["username"],
|
|
406
|
+
)
|
|
407
|
+
for row in connection.execute(
|
|
408
|
+
"""
|
|
409
|
+
SELECT
|
|
410
|
+
chat_id,
|
|
411
|
+
user_id,
|
|
412
|
+
username,
|
|
413
|
+
display_name,
|
|
414
|
+
recent_message,
|
|
415
|
+
status,
|
|
416
|
+
updated_at
|
|
417
|
+
FROM telegram_sessions
|
|
418
|
+
ORDER BY status DESC, updated_at DESC, chat_id
|
|
419
|
+
"""
|
|
420
|
+
)
|
|
421
|
+
]
|
|
422
|
+
return StoredTelegramBot(
|
|
423
|
+
bot_token=bot_row["bot_token"] if bot_row else "",
|
|
424
|
+
enabled=bool(bot_row["enabled"]) if bot_row else False,
|
|
425
|
+
sessions=sessions,
|
|
426
|
+
)
|
|
427
|
+
|
|
266
428
|
def _migrate(self, connection: sqlite3.Connection) -> None:
|
|
267
429
|
connection.executescript(
|
|
268
430
|
"""
|
|
431
|
+
CREATE TABLE IF NOT EXISTS telegram_bot (
|
|
432
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
433
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
434
|
+
bot_token TEXT NOT NULL,
|
|
435
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
CREATE TABLE IF NOT EXISTS telegram_sessions (
|
|
439
|
+
chat_id TEXT PRIMARY KEY,
|
|
440
|
+
user_id TEXT NOT NULL DEFAULT '',
|
|
441
|
+
username TEXT NOT NULL DEFAULT '',
|
|
442
|
+
display_name TEXT NOT NULL DEFAULT '',
|
|
443
|
+
recent_message TEXT NOT NULL DEFAULT '',
|
|
444
|
+
status TEXT NOT NULL,
|
|
445
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
446
|
+
);
|
|
447
|
+
|
|
269
448
|
CREATE TABLE IF NOT EXISTS providers (
|
|
270
449
|
id TEXT PRIMARY KEY,
|
|
271
450
|
name TEXT NOT NULL,
|
|
@@ -162,7 +162,7 @@ def tool_call_title(name: str, arguments: dict[str, object]) -> str:
|
|
|
162
162
|
if name == "grep_files":
|
|
163
163
|
return f"Searching {arguments.get('pattern', 'files')}"
|
|
164
164
|
if name == "apply_patch":
|
|
165
|
-
return "
|
|
165
|
+
return "Editing files"
|
|
166
166
|
if name == "shell_command":
|
|
167
167
|
return f"Running {arguments.get('command', 'command')}"
|
|
168
168
|
if name == "update_plan":
|
|
@@ -192,9 +192,10 @@ def run_tool(
|
|
|
192
192
|
return web_search(arguments, context)
|
|
193
193
|
raise ValueError("Tool is not available.")
|
|
194
194
|
except Exception as error:
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
title = (
|
|
196
|
+
"Edit failed" if name == "apply_patch" else tool_call_title(name, arguments)
|
|
197
197
|
)
|
|
198
|
+
return ToolResult(content=str(error), ok=False, title=title)
|
|
198
199
|
|
|
199
200
|
|
|
200
201
|
def integer_argument(arguments: dict[str, object], name: str, default: int) -> int:
|
|
@@ -262,13 +263,35 @@ def apply_patch_tool(arguments: dict[str, object], context: ToolContext) -> Tool
|
|
|
262
263
|
)
|
|
263
264
|
if result.exit_code != 0:
|
|
264
265
|
raise SandboxError(tool_failure_content(result))
|
|
266
|
+
data = json.loads(result.stdout or "{}")
|
|
265
267
|
return ToolResult(
|
|
266
268
|
content=result.stdout,
|
|
267
|
-
data=
|
|
268
|
-
title=
|
|
269
|
+
data=data if isinstance(data, dict) else {},
|
|
270
|
+
title=patch_title_from_result(data),
|
|
269
271
|
)
|
|
270
272
|
|
|
271
273
|
|
|
274
|
+
def patch_title_from_result(data: object) -> str:
|
|
275
|
+
if not isinstance(data, dict):
|
|
276
|
+
return "Edited files"
|
|
277
|
+
files = data.get("files")
|
|
278
|
+
if not isinstance(files, list) or not files:
|
|
279
|
+
return "Edited files"
|
|
280
|
+
if len(files) > 1:
|
|
281
|
+
return f"Edited {len(files)} files"
|
|
282
|
+
file_info = files[0]
|
|
283
|
+
if not isinstance(file_info, dict):
|
|
284
|
+
return "Edited files"
|
|
285
|
+
raw_path = file_info.get("path")
|
|
286
|
+
name = Path(str(raw_path)).name if raw_path else "file"
|
|
287
|
+
status = file_info.get("status")
|
|
288
|
+
if status == "added":
|
|
289
|
+
return f"Added {name}"
|
|
290
|
+
if status == "deleted":
|
|
291
|
+
return f"Deleted {name}"
|
|
292
|
+
return f"Edited {name}"
|
|
293
|
+
|
|
294
|
+
|
|
272
295
|
def tool_failure_content(result: object) -> str:
|
|
273
296
|
stdout = str(getattr(result, "stdout", "") or "").strip()
|
|
274
297
|
stderr = str(getattr(result, "stderr", "") or "").strip()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -253,9 +253,60 @@ def test_apply_patch_modifies_workdir_file(tmp_path) -> None:
|
|
|
253
253
|
result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
|
|
254
254
|
|
|
255
255
|
assert result.ok
|
|
256
|
+
assert result.title == "Edited notes.txt"
|
|
256
257
|
assert target.read_text() == "alpha\nready\n"
|
|
257
258
|
|
|
258
259
|
|
|
260
|
+
def test_apply_patch_added_file_title(tmp_path) -> None:
|
|
261
|
+
patch = """*** Begin Patch
|
|
262
|
+
*** Add File: created.txt
|
|
263
|
+
+hello
|
|
264
|
+
*** End Patch
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
|
|
268
|
+
|
|
269
|
+
assert result.ok
|
|
270
|
+
assert result.title == "Added created.txt"
|
|
271
|
+
assert (tmp_path / "created.txt").read_text() == "hello\n"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_apply_patch_deleted_file_title(tmp_path) -> None:
|
|
275
|
+
target = tmp_path / "old.txt"
|
|
276
|
+
target.write_text("remove me\n")
|
|
277
|
+
patch = """*** Begin Patch
|
|
278
|
+
*** Delete File: old.txt
|
|
279
|
+
*** End Patch
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
|
|
283
|
+
|
|
284
|
+
assert result.ok
|
|
285
|
+
assert result.title == "Deleted old.txt"
|
|
286
|
+
assert not target.exists()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_apply_patch_multiple_files_title(tmp_path) -> None:
|
|
290
|
+
target = tmp_path / "notes.txt"
|
|
291
|
+
target.write_text("alpha\nbeta\n")
|
|
292
|
+
patch = """*** Begin Patch
|
|
293
|
+
*** Update File: notes.txt
|
|
294
|
+
@@
|
|
295
|
+
-beta
|
|
296
|
+
+ready
|
|
297
|
+
*** Add File: created.txt
|
|
298
|
+
+hello
|
|
299
|
+
*** End Patch
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
|
|
303
|
+
|
|
304
|
+
assert result.ok
|
|
305
|
+
assert result.title == "Edited 2 files"
|
|
306
|
+
assert target.read_text() == "alpha\nready\n"
|
|
307
|
+
assert (tmp_path / "created.txt").read_text() == "hello\n"
|
|
308
|
+
|
|
309
|
+
|
|
259
310
|
def test_apply_patch_rejects_outside_workdir_file(tmp_path) -> None:
|
|
260
311
|
outside = Path(__file__).resolve().parent / "outside-patch.txt"
|
|
261
312
|
outside.write_text("alpha\n")
|
|
@@ -271,6 +322,7 @@ def test_apply_patch_rejects_outside_workdir_file(tmp_path) -> None:
|
|
|
271
322
|
result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
|
|
272
323
|
|
|
273
324
|
assert not result.ok
|
|
325
|
+
assert result.title == "Edit failed"
|
|
274
326
|
assert outside.read_text() == "alpha\n"
|
|
275
327
|
finally:
|
|
276
328
|
outside.unlink(missing_ok=True)
|
|
@@ -297,6 +349,7 @@ def test_apply_patch_uses_internal_subcommand(tmp_path, monkeypatch) -> None:
|
|
|
297
349
|
result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
|
|
298
350
|
|
|
299
351
|
assert result.ok
|
|
352
|
+
assert result.title == "Edited files"
|
|
300
353
|
assert calls
|
|
301
354
|
assert calls[0][1:4] == ["-m", "flowent.cli", "apply-patch"]
|
|
302
355
|
|
|
@@ -326,6 +379,7 @@ def test_apply_patch_reports_patch_error_when_stderr_has_warning(
|
|
|
326
379
|
result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
|
|
327
380
|
|
|
328
381
|
assert not result.ok
|
|
382
|
+
assert result.title == "Edit failed"
|
|
329
383
|
assert result.content == "Patch context was not found."
|
|
330
384
|
|
|
331
385
|
|