flowent 0.0.13 → 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/main.py +106 -9
- 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 +240 -0
- package/backend/src/flowent/tools.py +6 -2
- 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_mcp.py +710 -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-CEZrWoDG.css +0 -2
- package/backend/src/flowent/static/assets/index-S5a0Rkj1.js +0 -81
- package/dist/frontend/assets/index-CEZrWoDG.css +0 -2
- package/dist/frontend/assets/index-S5a0Rkj1.js +0 -81
package/backend/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flowent"
|
|
3
|
-
version = "0.0
|
|
3
|
+
version = "0.1.0"
|
|
4
4
|
description = "A workflow orchestration platform for multi-agent collaboration."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -11,6 +11,7 @@ license = "Apache-2.0"
|
|
|
11
11
|
dependencies = [
|
|
12
12
|
"fastapi[standard]>=0.136.1",
|
|
13
13
|
"litellm>=1.84.0",
|
|
14
|
+
"mcp>=1.24.0",
|
|
14
15
|
"uvicorn>=0.46.0",
|
|
15
16
|
]
|
|
16
17
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from uuid import uuid4
|
|
@@ -20,6 +20,7 @@ from flowent.llm import (
|
|
|
20
20
|
from flowent.logging import TRACE_LEVEL
|
|
21
21
|
from flowent.tools import (
|
|
22
22
|
ToolContext,
|
|
23
|
+
ToolResult,
|
|
23
24
|
new_tool_item,
|
|
24
25
|
parse_tool_arguments,
|
|
25
26
|
run_tool,
|
|
@@ -104,6 +105,10 @@ async def run_agent_stream(
|
|
|
104
105
|
connection: ProviderConnection,
|
|
105
106
|
cwd: Path,
|
|
106
107
|
messages: Sequence[Mapping[str, object]],
|
|
108
|
+
extra_tool_runner: Callable[[str, dict[str, object]], Awaitable[ToolResult | None]]
|
|
109
|
+
| None = None,
|
|
110
|
+
extra_tool_specs: Sequence[Mapping[str, object]] | None = None,
|
|
111
|
+
extra_tool_title: Callable[[str], str | None] | None = None,
|
|
107
112
|
web_searcher: Callable[[str], Sequence[dict[str, str]]] | None = None,
|
|
108
113
|
) -> AsyncIterator[AgentStreamEvent]:
|
|
109
114
|
conversation: list[Mapping[str, object]] = [
|
|
@@ -132,7 +137,10 @@ async def run_agent_stream(
|
|
|
132
137
|
pending: dict[int, PendingToolCall] = {}
|
|
133
138
|
|
|
134
139
|
async for chunk in stream_chat_chunks(
|
|
135
|
-
connection,
|
|
140
|
+
connection,
|
|
141
|
+
conversation,
|
|
142
|
+
completion=completion,
|
|
143
|
+
tools=[*tool_specs(), *list(extra_tool_specs or [])],
|
|
136
144
|
):
|
|
137
145
|
reasoning = chunk_delta_reasoning(chunk)
|
|
138
146
|
if reasoning:
|
|
@@ -221,16 +229,29 @@ async def run_agent_stream(
|
|
|
221
229
|
},
|
|
222
230
|
)
|
|
223
231
|
else:
|
|
224
|
-
tool_item = new_tool_item(
|
|
232
|
+
tool_item = new_tool_item(
|
|
233
|
+
tool_call.name,
|
|
234
|
+
arguments,
|
|
235
|
+
extra_tool_title(tool_call.name) if extra_tool_title else None,
|
|
236
|
+
)
|
|
225
237
|
logger.debug(
|
|
226
238
|
"Tool call started name=%s id=%s", tool_call.name, tool_item["id"]
|
|
227
239
|
)
|
|
228
240
|
logger.log(TRACE_LEVEL, "Tool start item=%r", tool_item)
|
|
229
241
|
yield AgentStreamEvent(event="tool_start", data={"tool": tool_item})
|
|
230
|
-
|
|
231
|
-
tool_call.name,
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
extra_result = (
|
|
243
|
+
await extra_tool_runner(tool_call.name, arguments)
|
|
244
|
+
if extra_tool_runner is not None
|
|
245
|
+
else None
|
|
246
|
+
)
|
|
247
|
+
result = (
|
|
248
|
+
extra_result
|
|
249
|
+
if isinstance(extra_result, ToolResult)
|
|
250
|
+
else run_tool(
|
|
251
|
+
tool_call.name,
|
|
252
|
+
arguments,
|
|
253
|
+
ToolContext(cwd=cwd, web_searcher=web_searcher),
|
|
254
|
+
)
|
|
234
255
|
)
|
|
235
256
|
result_content = result.content
|
|
236
257
|
logger.debug(
|
|
@@ -25,12 +25,21 @@ from flowent.llm import (
|
|
|
25
25
|
list_provider_models,
|
|
26
26
|
)
|
|
27
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
|
|
28
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
|
+
)
|
|
29
36
|
from flowent.storage import (
|
|
30
37
|
StateStore,
|
|
38
|
+
StoredMcpServer,
|
|
31
39
|
StoredMessage,
|
|
32
40
|
StoredProvider,
|
|
33
41
|
StoredSettings,
|
|
42
|
+
StoredSkill,
|
|
34
43
|
StoredState,
|
|
35
44
|
StoredTelegramBot,
|
|
36
45
|
StoredTelegramSession,
|
|
@@ -87,6 +96,18 @@ class TelegramSessionApproveRequest(BaseModel):
|
|
|
87
96
|
chat_id: str
|
|
88
97
|
|
|
89
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
|
+
|
|
90
111
|
def stream_event(event: str, data: dict[str, object]) -> str:
|
|
91
112
|
return f"event: {event}\ndata: {json.dumps(data)}\n\n"
|
|
92
113
|
|
|
@@ -202,12 +223,14 @@ def create_app(
|
|
|
202
223
|
*,
|
|
203
224
|
serve_frontend: bool = True,
|
|
204
225
|
chat_completion: CompletionCallable | None = None,
|
|
226
|
+
mcp_transport: McpTransport | None = None,
|
|
205
227
|
telegram_transport: TelegramTransport | None = None,
|
|
206
228
|
) -> FastAPI:
|
|
207
229
|
ensure_logging_configured()
|
|
208
230
|
ensure_sandbox_available()
|
|
209
231
|
|
|
210
232
|
store = StateStore()
|
|
233
|
+
mcp_manager = McpManager(store=store, transport=mcp_transport)
|
|
211
234
|
telegram_bot_manager: TelegramBotManager | None = None
|
|
212
235
|
|
|
213
236
|
static_dir = frontend_static_directory().resolve(strict=False)
|
|
@@ -229,9 +252,14 @@ def create_app(
|
|
|
229
252
|
next_messages,
|
|
230
253
|
store.read_compacted_context(),
|
|
231
254
|
)
|
|
255
|
+
skill_messages = explicit_skill_messages(cwd, store, content)
|
|
232
256
|
request_messages = [
|
|
233
257
|
message.model_dump()
|
|
234
|
-
for message in [
|
|
258
|
+
for message in [
|
|
259
|
+
*runtime_context_messages(cwd),
|
|
260
|
+
*skill_messages,
|
|
261
|
+
*chat_messages,
|
|
262
|
+
]
|
|
235
263
|
]
|
|
236
264
|
assistant_content = ""
|
|
237
265
|
assistant_thinking = ""
|
|
@@ -242,6 +270,9 @@ def create_app(
|
|
|
242
270
|
completion=chat_completion,
|
|
243
271
|
connection=connection,
|
|
244
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,
|
|
245
276
|
messages=request_messages,
|
|
246
277
|
):
|
|
247
278
|
if event.event == "delta":
|
|
@@ -291,7 +322,9 @@ def create_app(
|
|
|
291
322
|
|
|
292
323
|
@asynccontextmanager
|
|
293
324
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
325
|
+
app.state.mcp_manager = mcp_manager
|
|
294
326
|
app.state.telegram_bot_manager = telegram_bot_manager
|
|
327
|
+
await mcp_manager.start_enabled()
|
|
295
328
|
if telegram_bot_manager is not None:
|
|
296
329
|
await telegram_bot_manager.start_enabled()
|
|
297
330
|
try:
|
|
@@ -299,8 +332,11 @@ def create_app(
|
|
|
299
332
|
finally:
|
|
300
333
|
if telegram_bot_manager is not None:
|
|
301
334
|
await telegram_bot_manager.stop_all()
|
|
335
|
+
await mcp_manager.stop_all()
|
|
302
336
|
|
|
303
337
|
app = FastAPI(title="Flowent", lifespan=lifespan)
|
|
338
|
+
app.state.mcp_manager = mcp_manager
|
|
339
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
304
340
|
|
|
305
341
|
@app.get("/api/health")
|
|
306
342
|
async def health() -> dict[str, str]:
|
|
@@ -309,13 +345,15 @@ def create_app(
|
|
|
309
345
|
@app.get("/api/state")
|
|
310
346
|
async def app_state() -> StoredState:
|
|
311
347
|
state = store.read_state()
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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)
|
|
319
357
|
|
|
320
358
|
@app.get("/api/about")
|
|
321
359
|
async def about() -> AboutResponse:
|
|
@@ -325,6 +363,58 @@ def create_app(
|
|
|
325
363
|
async def save_provider(provider: StoredProvider) -> StoredProvider:
|
|
326
364
|
return store.save_provider(provider)
|
|
327
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
|
+
|
|
328
418
|
@app.put("/api/telegram-bot")
|
|
329
419
|
async def save_telegram_bot(telegram_bot: StoredTelegramBot) -> StoredTelegramBot:
|
|
330
420
|
saved_bot = store.save_telegram_bot(telegram_bot)
|
|
@@ -430,7 +520,11 @@ def create_app(
|
|
|
430
520
|
)
|
|
431
521
|
request_messages = [
|
|
432
522
|
message.model_dump()
|
|
433
|
-
for message in [
|
|
523
|
+
for message in [
|
|
524
|
+
*runtime_context_messages(cwd),
|
|
525
|
+
*explicit_skill_messages(cwd, store, request.content),
|
|
526
|
+
*chat_messages,
|
|
527
|
+
]
|
|
434
528
|
]
|
|
435
529
|
|
|
436
530
|
async def response_stream() -> AsyncIterator[str]:
|
|
@@ -440,6 +534,9 @@ def create_app(
|
|
|
440
534
|
completion=chat_completion,
|
|
441
535
|
connection=connection,
|
|
442
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,
|
|
443
540
|
messages=request_messages,
|
|
444
541
|
):
|
|
445
542
|
if event.event == "tool_start":
|