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.
Files changed (50) hide show
  1. package/backend/pyproject.toml +2 -1
  2. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  3. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  4. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/agent.py +28 -7
  20. package/backend/src/flowent/main.py +106 -9
  21. package/backend/src/flowent/mcp.py +484 -0
  22. package/backend/src/flowent/mcp_import.py +202 -0
  23. package/backend/src/flowent/skills.py +157 -0
  24. package/backend/src/flowent/static/assets/index-DqTHSMBo.js +81 -0
  25. package/backend/src/flowent/static/assets/index-d3FBbOXX.css +2 -0
  26. package/backend/src/flowent/static/index.html +2 -2
  27. package/backend/src/flowent/storage.py +240 -0
  28. package/backend/src/flowent/tools.py +6 -2
  29. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  30. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  31. package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
  32. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  33. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  34. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  35. package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
  36. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  37. package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
  38. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  39. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  40. package/backend/tests/test_mcp.py +710 -0
  41. package/backend/tests/test_skills.py +462 -0
  42. package/backend/uv.lock +160 -1
  43. package/dist/frontend/assets/index-DqTHSMBo.js +81 -0
  44. package/dist/frontend/assets/index-d3FBbOXX.css +2 -0
  45. package/dist/frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/backend/src/flowent/static/assets/index-CEZrWoDG.css +0 -2
  48. package/backend/src/flowent/static/assets/index-S5a0Rkj1.js +0 -81
  49. package/dist/frontend/assets/index-CEZrWoDG.css +0 -2
  50. package/dist/frontend/assets/index-S5a0Rkj1.js +0 -81
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowent"
3
- version = "0.0.13"
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
 
@@ -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, conversation, completion=completion, tools=tool_specs()
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(tool_call.name, arguments)
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
- result = run_tool(
231
- tool_call.name,
232
- arguments,
233
- ToolContext(cwd=cwd, web_searcher=web_searcher),
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 [*runtime_context_messages(cwd), *chat_messages]
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
- if telegram_bot_manager is None:
313
- return state
314
- return state.model_copy(
315
- update={
316
- "telegram_bot": telegram_bot_manager.bot_with_status(state.telegram_bot)
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 [*runtime_context_messages(cwd), *chat_messages]
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":