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.
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 +118 -9
  21. package/backend/src/flowent/mcp.py +484 -0
  22. package/backend/src/flowent/mcp_import.py +217 -0
  23. package/backend/src/flowent/skills.py +157 -0
  24. package/backend/src/flowent/static/assets/index-BhHdc2d_.js +81 -0
  25. package/backend/src/flowent/static/assets/index-C89n9qe2.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 +722 -0
  41. package/backend/tests/test_skills.py +462 -0
  42. package/backend/uv.lock +160 -1
  43. package/dist/frontend/assets/index-BhHdc2d_.js +81 -0
  44. package/dist/frontend/assets/index-C89n9qe2.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.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
 
@@ -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,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 [*runtime_context_messages(cwd), *chat_messages]
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
- 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
- )
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 [*runtime_context_messages(cwd), *chat_messages]
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":