flowent 0.0.13 → 0.1.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/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 +118 -9
- package/backend/src/flowent/mcp.py +484 -0
- package/backend/src/flowent/mcp_import.py +217 -0
- package/backend/src/flowent/skills.py +157 -0
- package/backend/src/flowent/static/assets/index-BhHdc2d_.js +81 -0
- package/backend/src/flowent/static/assets/index-C89n9qe2.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 +722 -0
- package/backend/tests/test_skills.py +462 -0
- package/backend/uv.lock +160 -1
- package/dist/frontend/assets/index-BhHdc2d_.js +81 -0
- package/dist/frontend/assets/index-C89n9qe2.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.
|
|
3
|
+
version = "0.1.1"
|
|
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,25 @@ 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
|
+
server_id: str
|
|
109
|
+
source: Literal["claude_code", "codex"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class McpImportPreviewRequest(BaseModel):
|
|
113
|
+
model_config = ConfigDict(extra="forbid")
|
|
114
|
+
|
|
115
|
+
source: Literal["claude_code", "codex"]
|
|
116
|
+
|
|
117
|
+
|
|
90
118
|
def stream_event(event: str, data: dict[str, object]) -> str:
|
|
91
119
|
return f"event: {event}\ndata: {json.dumps(data)}\n\n"
|
|
92
120
|
|
|
@@ -202,12 +230,14 @@ def create_app(
|
|
|
202
230
|
*,
|
|
203
231
|
serve_frontend: bool = True,
|
|
204
232
|
chat_completion: CompletionCallable | None = None,
|
|
233
|
+
mcp_transport: McpTransport | None = None,
|
|
205
234
|
telegram_transport: TelegramTransport | None = None,
|
|
206
235
|
) -> FastAPI:
|
|
207
236
|
ensure_logging_configured()
|
|
208
237
|
ensure_sandbox_available()
|
|
209
238
|
|
|
210
239
|
store = StateStore()
|
|
240
|
+
mcp_manager = McpManager(store=store, transport=mcp_transport)
|
|
211
241
|
telegram_bot_manager: TelegramBotManager | None = None
|
|
212
242
|
|
|
213
243
|
static_dir = frontend_static_directory().resolve(strict=False)
|
|
@@ -229,9 +259,14 @@ def create_app(
|
|
|
229
259
|
next_messages,
|
|
230
260
|
store.read_compacted_context(),
|
|
231
261
|
)
|
|
262
|
+
skill_messages = explicit_skill_messages(cwd, store, content)
|
|
232
263
|
request_messages = [
|
|
233
264
|
message.model_dump()
|
|
234
|
-
for message in [
|
|
265
|
+
for message in [
|
|
266
|
+
*runtime_context_messages(cwd),
|
|
267
|
+
*skill_messages,
|
|
268
|
+
*chat_messages,
|
|
269
|
+
]
|
|
235
270
|
]
|
|
236
271
|
assistant_content = ""
|
|
237
272
|
assistant_thinking = ""
|
|
@@ -242,6 +277,9 @@ def create_app(
|
|
|
242
277
|
completion=chat_completion,
|
|
243
278
|
connection=connection,
|
|
244
279
|
cwd=cwd,
|
|
280
|
+
extra_tool_runner=mcp_manager.run_tool,
|
|
281
|
+
extra_tool_specs=mcp_manager.tool_specs(),
|
|
282
|
+
extra_tool_title=mcp_manager.tool_title,
|
|
245
283
|
messages=request_messages,
|
|
246
284
|
):
|
|
247
285
|
if event.event == "delta":
|
|
@@ -291,7 +329,9 @@ def create_app(
|
|
|
291
329
|
|
|
292
330
|
@asynccontextmanager
|
|
293
331
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
332
|
+
app.state.mcp_manager = mcp_manager
|
|
294
333
|
app.state.telegram_bot_manager = telegram_bot_manager
|
|
334
|
+
await mcp_manager.start_enabled()
|
|
295
335
|
if telegram_bot_manager is not None:
|
|
296
336
|
await telegram_bot_manager.start_enabled()
|
|
297
337
|
try:
|
|
@@ -299,8 +339,11 @@ def create_app(
|
|
|
299
339
|
finally:
|
|
300
340
|
if telegram_bot_manager is not None:
|
|
301
341
|
await telegram_bot_manager.stop_all()
|
|
342
|
+
await mcp_manager.stop_all()
|
|
302
343
|
|
|
303
344
|
app = FastAPI(title="Flowent", lifespan=lifespan)
|
|
345
|
+
app.state.mcp_manager = mcp_manager
|
|
346
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
304
347
|
|
|
305
348
|
@app.get("/api/health")
|
|
306
349
|
async def health() -> dict[str, str]:
|
|
@@ -309,13 +352,15 @@ def create_app(
|
|
|
309
352
|
@app.get("/api/state")
|
|
310
353
|
async def app_state() -> StoredState:
|
|
311
354
|
state = store.read_state()
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
update: dict[str, object] = {
|
|
356
|
+
"mcp_servers": mcp_manager.servers_with_status(state.mcp_servers),
|
|
357
|
+
"skills": discover_skills(Path.cwd(), store),
|
|
358
|
+
}
|
|
359
|
+
if telegram_bot_manager is not None:
|
|
360
|
+
update["telegram_bot"] = telegram_bot_manager.bot_with_status(
|
|
361
|
+
state.telegram_bot
|
|
362
|
+
)
|
|
363
|
+
return state.model_copy(update=update)
|
|
319
364
|
|
|
320
365
|
@app.get("/api/about")
|
|
321
366
|
async def about() -> AboutResponse:
|
|
@@ -325,6 +370,63 @@ def create_app(
|
|
|
325
370
|
async def save_provider(provider: StoredProvider) -> StoredProvider:
|
|
326
371
|
return store.save_provider(provider)
|
|
327
372
|
|
|
373
|
+
@app.put("/api/mcp/servers")
|
|
374
|
+
async def save_mcp_server(server: StoredMcpServer) -> StoredMcpServer:
|
|
375
|
+
saved_server = store.save_mcp_server(server)
|
|
376
|
+
return await mcp_manager.sync_server(saved_server)
|
|
377
|
+
|
|
378
|
+
@app.post("/api/mcp/import/preview")
|
|
379
|
+
async def preview_mcp_import(
|
|
380
|
+
request: McpImportPreviewRequest,
|
|
381
|
+
) -> McpImportDiscovery:
|
|
382
|
+
return discover_imported_mcp_servers(Path.cwd(), source=request.source)
|
|
383
|
+
|
|
384
|
+
@app.post("/api/mcp/import")
|
|
385
|
+
async def import_mcp_servers(request: McpImportRequest) -> list[StoredMcpServer]:
|
|
386
|
+
imported_servers = discover_imported_mcp_servers(
|
|
387
|
+
Path.cwd(),
|
|
388
|
+
source=request.source,
|
|
389
|
+
).servers
|
|
390
|
+
existing_servers = {server.id for server in store.read_mcp_servers()}
|
|
391
|
+
for server in imported_servers:
|
|
392
|
+
if server.id != request.server_id:
|
|
393
|
+
continue
|
|
394
|
+
if server.id in existing_servers:
|
|
395
|
+
continue
|
|
396
|
+
store.save_mcp_server(server)
|
|
397
|
+
existing_servers.add(server.id)
|
|
398
|
+
return mcp_manager.servers_with_status(store.read_mcp_servers())
|
|
399
|
+
|
|
400
|
+
@app.delete("/api/mcp/servers/{server_id}")
|
|
401
|
+
async def delete_mcp_server(server_id: str) -> dict[str, bool]:
|
|
402
|
+
await mcp_manager.delete_server(server_id)
|
|
403
|
+
return {"ok": True}
|
|
404
|
+
|
|
405
|
+
@app.post("/api/mcp/servers/{server_id}/reconnect")
|
|
406
|
+
async def reconnect_mcp_server(server_id: str) -> StoredMcpServer:
|
|
407
|
+
try:
|
|
408
|
+
return await mcp_manager.reconnect_server(server_id)
|
|
409
|
+
except KeyError as error:
|
|
410
|
+
raise HTTPException(status_code=404, detail="Server not found.") from error
|
|
411
|
+
|
|
412
|
+
@app.post("/api/mcp/reload")
|
|
413
|
+
async def reload_mcp_servers() -> list[StoredMcpServer]:
|
|
414
|
+
return await mcp_manager.reload()
|
|
415
|
+
|
|
416
|
+
@app.post("/api/skills/reload")
|
|
417
|
+
async def reload_skills() -> list[StoredSkill]:
|
|
418
|
+
return discover_skills(Path.cwd(), store)
|
|
419
|
+
|
|
420
|
+
@app.put("/api/skills/{skill_id:path}")
|
|
421
|
+
async def save_skill_settings(
|
|
422
|
+
skill_id: str,
|
|
423
|
+
request: SkillSettingsRequest,
|
|
424
|
+
) -> StoredSkill:
|
|
425
|
+
try:
|
|
426
|
+
return update_skill_enabled(Path.cwd(), store, skill_id, request.enabled)
|
|
427
|
+
except KeyError as error:
|
|
428
|
+
raise HTTPException(status_code=404, detail="Skill not found.") from error
|
|
429
|
+
|
|
328
430
|
@app.put("/api/telegram-bot")
|
|
329
431
|
async def save_telegram_bot(telegram_bot: StoredTelegramBot) -> StoredTelegramBot:
|
|
330
432
|
saved_bot = store.save_telegram_bot(telegram_bot)
|
|
@@ -430,7 +532,11 @@ def create_app(
|
|
|
430
532
|
)
|
|
431
533
|
request_messages = [
|
|
432
534
|
message.model_dump()
|
|
433
|
-
for message in [
|
|
535
|
+
for message in [
|
|
536
|
+
*runtime_context_messages(cwd),
|
|
537
|
+
*explicit_skill_messages(cwd, store, request.content),
|
|
538
|
+
*chat_messages,
|
|
539
|
+
]
|
|
434
540
|
]
|
|
435
541
|
|
|
436
542
|
async def response_stream() -> AsyncIterator[str]:
|
|
@@ -440,6 +546,9 @@ def create_app(
|
|
|
440
546
|
completion=chat_completion,
|
|
441
547
|
connection=connection,
|
|
442
548
|
cwd=cwd,
|
|
549
|
+
extra_tool_runner=mcp_manager.run_tool,
|
|
550
|
+
extra_tool_specs=mcp_manager.tool_specs(),
|
|
551
|
+
extra_tool_title=mcp_manager.tool_title,
|
|
443
552
|
messages=request_messages,
|
|
444
553
|
):
|
|
445
554
|
if event.event == "tool_start":
|