flowent 0.0.12 → 0.1.0
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 +2 -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__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_import.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__/skills.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/agent.py +28 -7
- package/backend/src/flowent/channels.py +296 -0
- package/backend/src/flowent/main.py +226 -3
- package/backend/src/flowent/mcp.py +484 -0
- package/backend/src/flowent/mcp_import.py +202 -0
- package/backend/src/flowent/skills.py +157 -0
- package/backend/src/flowent/static/assets/index-DqTHSMBo.js +81 -0
- package/backend/src/flowent/static/assets/index-d3FBbOXX.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +419 -0
- package/backend/src/flowent/tools.py +34 -7
- 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_mcp.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_skills.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_mcp.py +710 -0
- package/backend/tests/test_persistence.py +30 -0
- package/backend/tests/test_skills.py +462 -0
- package/backend/uv.lock +160 -1
- package/dist/frontend/assets/index-DqTHSMBo.js +81 -0
- package/dist/frontend/assets/index-d3FBbOXX.css +2 -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
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
from collections.abc import AsyncIterator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Literal
|
|
7
8
|
from uuid import uuid4
|
|
@@ -13,6 +14,7 @@ from pydantic import BaseModel, ConfigDict
|
|
|
13
14
|
|
|
14
15
|
from flowent._version import __version__
|
|
15
16
|
from flowent.agent import run_agent_stream
|
|
17
|
+
from flowent.channels import TelegramBotManager, TelegramTransport
|
|
16
18
|
from flowent.context import runtime_context_messages
|
|
17
19
|
from flowent.llm import (
|
|
18
20
|
ChatMessage,
|
|
@@ -23,13 +25,24 @@ from flowent.llm import (
|
|
|
23
25
|
list_provider_models,
|
|
24
26
|
)
|
|
25
27
|
from flowent.logging import TRACE_LEVEL, ensure_logging_configured
|
|
28
|
+
from flowent.mcp import McpManager, McpTransport
|
|
29
|
+
from flowent.mcp_import import McpImportDiscovery, discover_imported_mcp_servers
|
|
26
30
|
from flowent.sandbox import ensure_sandbox_available
|
|
31
|
+
from flowent.skills import (
|
|
32
|
+
discover_skills,
|
|
33
|
+
explicit_skill_messages,
|
|
34
|
+
update_skill_enabled,
|
|
35
|
+
)
|
|
27
36
|
from flowent.storage import (
|
|
28
37
|
StateStore,
|
|
38
|
+
StoredMcpServer,
|
|
29
39
|
StoredMessage,
|
|
30
40
|
StoredProvider,
|
|
31
41
|
StoredSettings,
|
|
42
|
+
StoredSkill,
|
|
32
43
|
StoredState,
|
|
44
|
+
StoredTelegramBot,
|
|
45
|
+
StoredTelegramSession,
|
|
33
46
|
StoredToolItem,
|
|
34
47
|
)
|
|
35
48
|
|
|
@@ -77,6 +90,24 @@ class AboutResponse(BaseModel):
|
|
|
77
90
|
version: str
|
|
78
91
|
|
|
79
92
|
|
|
93
|
+
class TelegramSessionApproveRequest(BaseModel):
|
|
94
|
+
model_config = ConfigDict(extra="forbid")
|
|
95
|
+
|
|
96
|
+
chat_id: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SkillSettingsRequest(BaseModel):
|
|
100
|
+
model_config = ConfigDict(extra="forbid")
|
|
101
|
+
|
|
102
|
+
enabled: bool
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class McpImportRequest(BaseModel):
|
|
106
|
+
model_config = ConfigDict(extra="forbid")
|
|
107
|
+
|
|
108
|
+
duplicate_action: Literal["replace", "skip"] = "skip"
|
|
109
|
+
|
|
110
|
+
|
|
80
111
|
def stream_event(event: str, data: dict[str, object]) -> str:
|
|
81
112
|
return f"event: {event}\ndata: {json.dumps(data)}\n\n"
|
|
82
113
|
|
|
@@ -192,24 +223,137 @@ def create_app(
|
|
|
192
223
|
*,
|
|
193
224
|
serve_frontend: bool = True,
|
|
194
225
|
chat_completion: CompletionCallable | None = None,
|
|
226
|
+
mcp_transport: McpTransport | None = None,
|
|
227
|
+
telegram_transport: TelegramTransport | None = None,
|
|
195
228
|
) -> FastAPI:
|
|
196
229
|
ensure_logging_configured()
|
|
197
230
|
ensure_sandbox_available()
|
|
198
231
|
|
|
199
|
-
app = FastAPI(title="Flowent")
|
|
200
232
|
store = StateStore()
|
|
233
|
+
mcp_manager = McpManager(store=store, transport=mcp_transport)
|
|
234
|
+
telegram_bot_manager: TelegramBotManager | None = None
|
|
201
235
|
|
|
202
236
|
static_dir = frontend_static_directory().resolve(strict=False)
|
|
203
237
|
logger.debug("Flowent app created serve_frontend=%s", serve_frontend)
|
|
204
238
|
logger.info("Static directory: %s", static_dir)
|
|
205
239
|
|
|
240
|
+
async def run_workspace_turn(content: str) -> StoredMessage:
|
|
241
|
+
state = store.read_state()
|
|
242
|
+
connection = selected_connection(state)
|
|
243
|
+
cwd = Path.cwd()
|
|
244
|
+
user_message = StoredMessage(
|
|
245
|
+
author="user",
|
|
246
|
+
content=content,
|
|
247
|
+
id=str(uuid4()),
|
|
248
|
+
)
|
|
249
|
+
next_messages = [*state.messages, user_message]
|
|
250
|
+
store.save_messages(next_messages)
|
|
251
|
+
chat_messages = workspace_chat_messages(
|
|
252
|
+
next_messages,
|
|
253
|
+
store.read_compacted_context(),
|
|
254
|
+
)
|
|
255
|
+
skill_messages = explicit_skill_messages(cwd, store, content)
|
|
256
|
+
request_messages = [
|
|
257
|
+
message.model_dump()
|
|
258
|
+
for message in [
|
|
259
|
+
*runtime_context_messages(cwd),
|
|
260
|
+
*skill_messages,
|
|
261
|
+
*chat_messages,
|
|
262
|
+
]
|
|
263
|
+
]
|
|
264
|
+
assistant_content = ""
|
|
265
|
+
assistant_thinking = ""
|
|
266
|
+
assistant_tools: dict[str, StoredToolItem] = {}
|
|
267
|
+
assistant_id = str(uuid4())
|
|
268
|
+
|
|
269
|
+
async for event in run_agent_stream(
|
|
270
|
+
completion=chat_completion,
|
|
271
|
+
connection=connection,
|
|
272
|
+
cwd=cwd,
|
|
273
|
+
extra_tool_runner=mcp_manager.run_tool,
|
|
274
|
+
extra_tool_specs=mcp_manager.tool_specs(),
|
|
275
|
+
extra_tool_title=mcp_manager.tool_title,
|
|
276
|
+
messages=request_messages,
|
|
277
|
+
):
|
|
278
|
+
if event.event == "delta":
|
|
279
|
+
assistant_content += str(event.data.get("content") or "")
|
|
280
|
+
if event.event == "thinking_delta":
|
|
281
|
+
assistant_thinking += str(event.data.get("content") or "")
|
|
282
|
+
if event.event == "tool_start":
|
|
283
|
+
tool = event.data.get("tool")
|
|
284
|
+
if isinstance(tool, dict) and isinstance(tool.get("id"), str):
|
|
285
|
+
assistant_tools[tool["id"]] = StoredToolItem.model_validate(tool)
|
|
286
|
+
if event.event in {"tool_done", "tool_error"}:
|
|
287
|
+
tool_id = event.data.get("id")
|
|
288
|
+
if isinstance(tool_id, str) and tool_id in assistant_tools:
|
|
289
|
+
assistant_tools[tool_id] = StoredToolItem.model_validate(
|
|
290
|
+
{
|
|
291
|
+
**assistant_tools[tool_id].model_dump(exclude_none=True),
|
|
292
|
+
**event.data,
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
if event.event == "done":
|
|
296
|
+
message = event.data.get("message")
|
|
297
|
+
if isinstance(message, dict):
|
|
298
|
+
assistant_id = str(message.get("id") or assistant_id)
|
|
299
|
+
assistant_content = str(message.get("content") or assistant_content)
|
|
300
|
+
assistant_thinking = str(
|
|
301
|
+
message.get("thinking") or assistant_thinking
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
assistant_message = StoredMessage(
|
|
305
|
+
author="assistant",
|
|
306
|
+
content=assistant_content,
|
|
307
|
+
id=assistant_id,
|
|
308
|
+
thinking=assistant_thinking,
|
|
309
|
+
tools=list(assistant_tools.values()),
|
|
310
|
+
)
|
|
311
|
+
store.save_messages([*next_messages, assistant_message])
|
|
312
|
+
return assistant_message
|
|
313
|
+
|
|
314
|
+
async def workspace_reply_text(content: str) -> str:
|
|
315
|
+
return (await run_workspace_turn(content)).content
|
|
316
|
+
|
|
317
|
+
telegram_bot_manager = TelegramBotManager(
|
|
318
|
+
message_handler=workspace_reply_text,
|
|
319
|
+
store=store,
|
|
320
|
+
telegram_transport=telegram_transport,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
@asynccontextmanager
|
|
324
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
325
|
+
app.state.mcp_manager = mcp_manager
|
|
326
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
327
|
+
await mcp_manager.start_enabled()
|
|
328
|
+
if telegram_bot_manager is not None:
|
|
329
|
+
await telegram_bot_manager.start_enabled()
|
|
330
|
+
try:
|
|
331
|
+
yield
|
|
332
|
+
finally:
|
|
333
|
+
if telegram_bot_manager is not None:
|
|
334
|
+
await telegram_bot_manager.stop_all()
|
|
335
|
+
await mcp_manager.stop_all()
|
|
336
|
+
|
|
337
|
+
app = FastAPI(title="Flowent", lifespan=lifespan)
|
|
338
|
+
app.state.mcp_manager = mcp_manager
|
|
339
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
340
|
+
|
|
206
341
|
@app.get("/api/health")
|
|
207
342
|
async def health() -> dict[str, str]:
|
|
208
343
|
return {"status": "ok"}
|
|
209
344
|
|
|
210
345
|
@app.get("/api/state")
|
|
211
346
|
async def app_state() -> StoredState:
|
|
212
|
-
|
|
347
|
+
state = store.read_state()
|
|
348
|
+
update: dict[str, object] = {
|
|
349
|
+
"mcp_servers": mcp_manager.servers_with_status(state.mcp_servers),
|
|
350
|
+
"skills": discover_skills(Path.cwd(), store),
|
|
351
|
+
}
|
|
352
|
+
if telegram_bot_manager is not None:
|
|
353
|
+
update["telegram_bot"] = telegram_bot_manager.bot_with_status(
|
|
354
|
+
state.telegram_bot
|
|
355
|
+
)
|
|
356
|
+
return state.model_copy(update=update)
|
|
213
357
|
|
|
214
358
|
@app.get("/api/about")
|
|
215
359
|
async def about() -> AboutResponse:
|
|
@@ -219,6 +363,78 @@ def create_app(
|
|
|
219
363
|
async def save_provider(provider: StoredProvider) -> StoredProvider:
|
|
220
364
|
return store.save_provider(provider)
|
|
221
365
|
|
|
366
|
+
@app.put("/api/mcp/servers")
|
|
367
|
+
async def save_mcp_server(server: StoredMcpServer) -> StoredMcpServer:
|
|
368
|
+
saved_server = store.save_mcp_server(server)
|
|
369
|
+
return await mcp_manager.sync_server(saved_server)
|
|
370
|
+
|
|
371
|
+
@app.get("/api/mcp/import/preview")
|
|
372
|
+
async def preview_mcp_import() -> McpImportDiscovery:
|
|
373
|
+
return discover_imported_mcp_servers(Path.cwd())
|
|
374
|
+
|
|
375
|
+
@app.post("/api/mcp/import")
|
|
376
|
+
async def import_mcp_servers(request: McpImportRequest) -> list[StoredMcpServer]:
|
|
377
|
+
imported_servers = discover_imported_mcp_servers(Path.cwd()).servers
|
|
378
|
+
existing_servers = {server.id for server in store.read_mcp_servers()}
|
|
379
|
+
for server in imported_servers:
|
|
380
|
+
if request.duplicate_action == "skip" and server.id in existing_servers:
|
|
381
|
+
continue
|
|
382
|
+
if request.duplicate_action == "replace" and server.id in existing_servers:
|
|
383
|
+
await mcp_manager.delete_server(server.id)
|
|
384
|
+
store.save_mcp_server(server)
|
|
385
|
+
existing_servers.add(server.id)
|
|
386
|
+
return mcp_manager.servers_with_status(store.read_mcp_servers())
|
|
387
|
+
|
|
388
|
+
@app.delete("/api/mcp/servers/{server_id}")
|
|
389
|
+
async def delete_mcp_server(server_id: str) -> dict[str, bool]:
|
|
390
|
+
await mcp_manager.delete_server(server_id)
|
|
391
|
+
return {"ok": True}
|
|
392
|
+
|
|
393
|
+
@app.post("/api/mcp/servers/{server_id}/reconnect")
|
|
394
|
+
async def reconnect_mcp_server(server_id: str) -> StoredMcpServer:
|
|
395
|
+
try:
|
|
396
|
+
return await mcp_manager.reconnect_server(server_id)
|
|
397
|
+
except KeyError as error:
|
|
398
|
+
raise HTTPException(status_code=404, detail="Server not found.") from error
|
|
399
|
+
|
|
400
|
+
@app.post("/api/mcp/reload")
|
|
401
|
+
async def reload_mcp_servers() -> list[StoredMcpServer]:
|
|
402
|
+
return await mcp_manager.reload()
|
|
403
|
+
|
|
404
|
+
@app.post("/api/skills/reload")
|
|
405
|
+
async def reload_skills() -> list[StoredSkill]:
|
|
406
|
+
return discover_skills(Path.cwd(), store)
|
|
407
|
+
|
|
408
|
+
@app.put("/api/skills/{skill_id:path}")
|
|
409
|
+
async def save_skill_settings(
|
|
410
|
+
skill_id: str,
|
|
411
|
+
request: SkillSettingsRequest,
|
|
412
|
+
) -> StoredSkill:
|
|
413
|
+
try:
|
|
414
|
+
return update_skill_enabled(Path.cwd(), store, skill_id, request.enabled)
|
|
415
|
+
except KeyError as error:
|
|
416
|
+
raise HTTPException(status_code=404, detail="Skill not found.") from error
|
|
417
|
+
|
|
418
|
+
@app.put("/api/telegram-bot")
|
|
419
|
+
async def save_telegram_bot(telegram_bot: StoredTelegramBot) -> StoredTelegramBot:
|
|
420
|
+
saved_bot = store.save_telegram_bot(telegram_bot)
|
|
421
|
+
if telegram_bot_manager is not None:
|
|
422
|
+
await telegram_bot_manager.sync_bot(saved_bot)
|
|
423
|
+
return telegram_bot_manager.bot_with_status(saved_bot)
|
|
424
|
+
return saved_bot
|
|
425
|
+
|
|
426
|
+
@app.post("/api/telegram-bot/approve")
|
|
427
|
+
async def approve_telegram_session(
|
|
428
|
+
request: TelegramSessionApproveRequest,
|
|
429
|
+
) -> StoredTelegramSession:
|
|
430
|
+
try:
|
|
431
|
+
return store.approve_telegram_session(request.chat_id)
|
|
432
|
+
except KeyError as error:
|
|
433
|
+
raise HTTPException(
|
|
434
|
+
status_code=404,
|
|
435
|
+
detail="Conversation not found.",
|
|
436
|
+
) from error
|
|
437
|
+
|
|
222
438
|
@app.post("/api/providers/models")
|
|
223
439
|
async def provider_models(request: ProviderModelsRequest) -> ProviderModelsResponse:
|
|
224
440
|
return ProviderModelsResponse(
|
|
@@ -304,7 +520,11 @@ def create_app(
|
|
|
304
520
|
)
|
|
305
521
|
request_messages = [
|
|
306
522
|
message.model_dump()
|
|
307
|
-
for message in [
|
|
523
|
+
for message in [
|
|
524
|
+
*runtime_context_messages(cwd),
|
|
525
|
+
*explicit_skill_messages(cwd, store, request.content),
|
|
526
|
+
*chat_messages,
|
|
527
|
+
]
|
|
308
528
|
]
|
|
309
529
|
|
|
310
530
|
async def response_stream() -> AsyncIterator[str]:
|
|
@@ -314,6 +534,9 @@ def create_app(
|
|
|
314
534
|
completion=chat_completion,
|
|
315
535
|
connection=connection,
|
|
316
536
|
cwd=cwd,
|
|
537
|
+
extra_tool_runner=mcp_manager.run_tool,
|
|
538
|
+
extra_tool_specs=mcp_manager.tool_specs(),
|
|
539
|
+
extra_tool_title=mcp_manager.tool_title,
|
|
317
540
|
messages=request_messages,
|
|
318
541
|
):
|
|
319
542
|
if event.event == "tool_start":
|