flowent 0.0.0 → 0.0.4

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 (494) hide show
  1. package/README.md +70 -10
  2. package/assets/flowent-banner.png +0 -0
  3. package/backend/.python-version +1 -0
  4. package/backend/pyproject.toml +57 -0
  5. package/backend/src/flowent/__init__.py +3 -0
  6. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
  23. package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
  24. package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
  25. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  26. package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
  27. package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
  28. package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
  29. package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
  30. package/backend/src/flowent/__pycache__/stats_service.cpython-313.pyc +0 -0
  31. package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
  32. package/backend/src/flowent/_version.py +7 -0
  33. package/backend/src/flowent/access.py +247 -0
  34. package/backend/src/flowent/agent.py +2808 -0
  35. package/backend/src/flowent/assistant_commands.py +106 -0
  36. package/backend/src/flowent/channels/__init__.py +3 -0
  37. package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
  39. package/backend/src/flowent/channels/telegram.py +615 -0
  40. package/backend/src/flowent/cli.py +85 -0
  41. package/backend/src/flowent/config.py +14 -0
  42. package/backend/src/flowent/dev.py +3 -0
  43. package/backend/src/flowent/events.py +157 -0
  44. package/backend/src/flowent/graph_runtime.py +60 -0
  45. package/backend/src/flowent/graph_service.py +1346 -0
  46. package/backend/src/flowent/image_assets.py +356 -0
  47. package/backend/src/flowent/logging.py +155 -0
  48. package/backend/src/flowent/main.py +124 -0
  49. package/backend/src/flowent/mcp_service.py +1904 -0
  50. package/backend/src/flowent/model_metadata.py +98 -0
  51. package/backend/src/flowent/models/__init__.py +121 -0
  52. package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
  54. package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
  55. package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
  56. package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
  57. package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
  58. package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
  59. package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
  60. package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
  61. package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
  62. package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
  63. package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
  64. package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
  65. package/backend/src/flowent/models/agent.py +33 -0
  66. package/backend/src/flowent/models/base.py +24 -0
  67. package/backend/src/flowent/models/blueprint.py +176 -0
  68. package/backend/src/flowent/models/content.py +164 -0
  69. package/backend/src/flowent/models/delta.py +44 -0
  70. package/backend/src/flowent/models/event.py +51 -0
  71. package/backend/src/flowent/models/graph.py +437 -0
  72. package/backend/src/flowent/models/history.py +214 -0
  73. package/backend/src/flowent/models/llm.py +61 -0
  74. package/backend/src/flowent/models/message.py +27 -0
  75. package/backend/src/flowent/models/tab.py +48 -0
  76. package/backend/src/flowent/models/todo.py +10 -0
  77. package/backend/src/flowent/network.py +146 -0
  78. package/backend/src/flowent/prompts/__init__.py +67 -0
  79. package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  80. package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
  81. package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
  82. package/backend/src/flowent/prompts/common.py +250 -0
  83. package/backend/src/flowent/prompts/steward.py +64 -0
  84. package/backend/src/flowent/providers/__init__.py +23 -0
  85. package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  86. package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
  87. package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
  88. package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
  89. package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
  90. package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
  91. package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
  92. package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
  93. package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
  94. package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
  95. package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
  96. package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
  97. package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
  98. package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
  99. package/backend/src/flowent/providers/anthropic.py +468 -0
  100. package/backend/src/flowent/providers/base_url.py +60 -0
  101. package/backend/src/flowent/providers/configuration.py +182 -0
  102. package/backend/src/flowent/providers/content.py +122 -0
  103. package/backend/src/flowent/providers/errors.py +223 -0
  104. package/backend/src/flowent/providers/gateway.py +169 -0
  105. package/backend/src/flowent/providers/gemini.py +447 -0
  106. package/backend/src/flowent/providers/headers.py +20 -0
  107. package/backend/src/flowent/providers/management.py +96 -0
  108. package/backend/src/flowent/providers/ollama.py +293 -0
  109. package/backend/src/flowent/providers/openai.py +422 -0
  110. package/backend/src/flowent/providers/openai_responses.py +655 -0
  111. package/backend/src/flowent/providers/registry.py +144 -0
  112. package/backend/src/flowent/providers/sse.py +31 -0
  113. package/backend/src/flowent/providers/thinking.py +79 -0
  114. package/backend/src/flowent/registry.py +73 -0
  115. package/backend/src/flowent/role_management.py +255 -0
  116. package/backend/src/flowent/routes/__init__.py +30 -0
  117. package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  118. package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
  119. package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
  120. package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
  121. package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
  122. package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
  123. package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
  124. package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
  125. package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
  126. package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
  127. package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
  128. package/backend/src/flowent/routes/__pycache__/stats.cpython-313.pyc +0 -0
  129. package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
  130. package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
  131. package/backend/src/flowent/routes/access.py +48 -0
  132. package/backend/src/flowent/routes/assistant.py +155 -0
  133. package/backend/src/flowent/routes/image_assets.py +33 -0
  134. package/backend/src/flowent/routes/mcp.py +125 -0
  135. package/backend/src/flowent/routes/meta.py +28 -0
  136. package/backend/src/flowent/routes/nodes.py +365 -0
  137. package/backend/src/flowent/routes/prompts.py +46 -0
  138. package/backend/src/flowent/routes/providers_route.py +364 -0
  139. package/backend/src/flowent/routes/roles.py +207 -0
  140. package/backend/src/flowent/routes/settings.py +324 -0
  141. package/backend/src/flowent/routes/stats.py +229 -0
  142. package/backend/src/flowent/routes/tabs.py +292 -0
  143. package/backend/src/flowent/routes/ws.py +33 -0
  144. package/backend/src/flowent/runtime.py +188 -0
  145. package/backend/src/flowent/sandbox.py +45 -0
  146. package/backend/src/flowent/security.py +42 -0
  147. package/backend/src/flowent/settings.py +2467 -0
  148. package/backend/src/flowent/settings_management.py +286 -0
  149. package/backend/src/flowent/state_db.py +120 -0
  150. package/backend/src/flowent/static/assets/AssistantPage-B3Xc08AS.js +1 -0
  151. package/backend/src/flowent/static/assets/ChannelsPage-ByLd28xk.js +1 -0
  152. package/backend/src/flowent/static/assets/HomePage-C0hAx9_l.js +3 -0
  153. package/backend/src/flowent/static/assets/McpPage-DkrYLvBv.js +7 -0
  154. package/backend/src/flowent/static/assets/PageScaffold-D4jO9ooX.js +1 -0
  155. package/backend/src/flowent/static/assets/PromptsPage-DWA7rRJd.js +1 -0
  156. package/backend/src/flowent/static/assets/ProvidersPage-PUWT8seJ.js +3 -0
  157. package/backend/src/flowent/static/assets/RolesPage-CqcclGRw.js +1 -0
  158. package/backend/src/flowent/static/assets/SettingsPage-8tS2cJgX.js +3 -0
  159. package/backend/src/flowent/static/assets/StatsPage-BX9khYzu.js +1 -0
  160. package/backend/src/flowent/static/assets/ToolsPage-9Tl9FdeD.js +1 -0
  161. package/backend/src/flowent/static/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
  162. package/backend/src/flowent/static/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
  163. package/backend/src/flowent/static/assets/alert-dialog-kFYVQ7oX.js +1 -0
  164. package/backend/src/flowent/static/assets/badge-74-3jsCg.js +1 -0
  165. package/backend/src/flowent/static/assets/constants-XUzFf6i1.js +1 -0
  166. package/backend/src/flowent/static/assets/datetime-m6_O_Ci9.js +1 -0
  167. package/backend/src/flowent/static/assets/dialog-BeGSweF6.js +1 -0
  168. package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  169. package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +1 -0
  170. package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +7 -0
  171. package/backend/src/flowent/static/assets/index-BHC1Vhy8.css +1 -0
  172. package/backend/src/flowent/static/assets/index-CL1ALZ3r.js +10 -0
  173. package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +24 -0
  174. package/backend/src/flowent/static/assets/markdown-vendor-DVdy_w12.js +29 -0
  175. package/backend/src/flowent/static/assets/modelParams-CaHd0903.js +1 -0
  176. package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +9 -0
  177. package/backend/src/flowent/static/assets/roles-2OLDeTc5.js +1 -0
  178. package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  179. package/backend/src/flowent/static/assets/select-DL_LPeDj.js +1 -0
  180. package/backend/src/flowent/static/assets/shared-CMxbpLeQ.js +1 -0
  181. package/backend/src/flowent/static/assets/triState-DEr3NkXV.js +1 -0
  182. package/backend/src/flowent/static/assets/ui-vendor-Dg9NNnWX.js +51 -0
  183. package/backend/src/flowent/static/index.html +36 -0
  184. package/backend/src/flowent/stats_service.py +218 -0
  185. package/backend/src/flowent/tools/__init__.py +201 -0
  186. package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  187. package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
  188. package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
  189. package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
  190. package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
  191. package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
  192. package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
  193. package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
  194. package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
  195. package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
  196. package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
  197. package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
  198. package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
  199. package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
  200. package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
  201. package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
  202. package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
  203. package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
  204. package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
  205. package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
  206. package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
  207. package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
  208. package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
  209. package/backend/src/flowent/tools/connect.py +156 -0
  210. package/backend/src/flowent/tools/contacts.py +22 -0
  211. package/backend/src/flowent/tools/create_agent.py +270 -0
  212. package/backend/src/flowent/tools/create_tab.py +59 -0
  213. package/backend/src/flowent/tools/delete_tab.py +39 -0
  214. package/backend/src/flowent/tools/edit.py +142 -0
  215. package/backend/src/flowent/tools/exec.py +117 -0
  216. package/backend/src/flowent/tools/fetch.py +85 -0
  217. package/backend/src/flowent/tools/idle.py +27 -0
  218. package/backend/src/flowent/tools/list_roles.py +50 -0
  219. package/backend/src/flowent/tools/list_tabs.py +96 -0
  220. package/backend/src/flowent/tools/list_tools.py +24 -0
  221. package/backend/src/flowent/tools/manage_prompts.py +102 -0
  222. package/backend/src/flowent/tools/manage_providers.py +220 -0
  223. package/backend/src/flowent/tools/manage_roles.py +275 -0
  224. package/backend/src/flowent/tools/manage_settings.py +346 -0
  225. package/backend/src/flowent/tools/mcp.py +199 -0
  226. package/backend/src/flowent/tools/read.py +152 -0
  227. package/backend/src/flowent/tools/send.py +50 -0
  228. package/backend/src/flowent/tools/set_permissions.py +84 -0
  229. package/backend/src/flowent/tools/sleep.py +41 -0
  230. package/backend/src/flowent/tools/todo.py +51 -0
  231. package/backend/src/flowent/workspace_store.py +479 -0
  232. package/backend/tests/__init__.py +0 -0
  233. package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  234. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  235. package/backend/tests/conftest.py +6 -0
  236. package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  237. package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
  238. package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
  239. package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
  240. package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
  241. package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
  242. package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
  243. package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
  244. package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
  245. package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
  246. package/backend/tests/integration/api/conftest.py +29 -0
  247. package/backend/tests/integration/api/test_access_api.py +182 -0
  248. package/backend/tests/integration/api/test_assistant_api.py +354 -0
  249. package/backend/tests/integration/api/test_frontend_mounting.py +61 -0
  250. package/backend/tests/integration/api/test_mcp_api.py +116 -0
  251. package/backend/tests/integration/api/test_meta_api.py +33 -0
  252. package/backend/tests/integration/api/test_nodes_api.py +486 -0
  253. package/backend/tests/integration/api/test_prompts_api.py +47 -0
  254. package/backend/tests/integration/api/test_roles_api.py +227 -0
  255. package/backend/tests/integration/api/test_tabs_api.py +501 -0
  256. package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
  257. package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  258. package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  259. package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
  260. package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
  261. package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
  262. package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
  263. package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  264. package/backend/tests/unit/agent/test_agent_public_api.py +746 -0
  265. package/backend/tests/unit/agent/test_agent_runtime.py +2726 -0
  266. package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
  267. package/backend/tests/unit/channels/test_telegram_channel.py +552 -0
  268. package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  269. package/backend/tests/unit/logging/test_logging.py +132 -0
  270. package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
  271. package/backend/tests/unit/prompts/test_prompts.py +569 -0
  272. package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  273. package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
  274. package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
  275. package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  276. package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
  277. package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
  278. package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
  279. package/backend/tests/unit/providers/test_anthropic_provider.py +185 -0
  280. package/backend/tests/unit/providers/test_errors.py +68 -0
  281. package/backend/tests/unit/providers/test_extract_delta_parts.py +22 -0
  282. package/backend/tests/unit/providers/test_openai_provider.py +139 -0
  283. package/backend/tests/unit/providers/test_openai_responses.py +402 -0
  284. package/backend/tests/unit/providers/test_provider_gateway.py +359 -0
  285. package/backend/tests/unit/providers/test_think_tag_parser.py +36 -0
  286. package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  287. package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
  288. package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  289. package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  290. package/backend/tests/unit/routes/__pycache__/test_stats_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  291. package/backend/tests/unit/routes/test_prompts_routes.py +104 -0
  292. package/backend/tests/unit/routes/test_providers_route.py +368 -0
  293. package/backend/tests/unit/routes/test_roles_routes.py +426 -0
  294. package/backend/tests/unit/routes/test_settings_routes.py +1138 -0
  295. package/backend/tests/unit/routes/test_stats_routes.py +149 -0
  296. package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  297. package/backend/tests/unit/runtime/test_bootstrap_runtime.py +1012 -0
  298. package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  299. package/backend/tests/unit/sandbox/test_sandbox_tools.py +78 -0
  300. package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
  301. package/backend/tests/unit/security/test_security.py +110 -0
  302. package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
  303. package/backend/tests/unit/settings/test_settings_roles.py +711 -0
  304. package/backend/tests/unit/test_access.py +45 -0
  305. package/backend/tests/unit/test_cli.py +124 -0
  306. package/backend/tests/unit/test_graph_runtime.py +72 -0
  307. package/backend/tests/unit/test_network.py +51 -0
  308. package/backend/tests/unit/test_state_sqlite_storage.py +93 -0
  309. package/backend/tests/unit/test_workspace_store.py +231 -0
  310. package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  311. package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  312. package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  313. package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  314. package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  315. package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  316. package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  317. package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  318. package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  319. package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  320. package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  321. package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  322. package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  323. package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
  324. package/backend/tests/unit/tools/test_connect_tool.py +229 -0
  325. package/backend/tests/unit/tools/test_create_agent_tool.py +524 -0
  326. package/backend/tests/unit/tools/test_delete_tab_tool.py +83 -0
  327. package/backend/tests/unit/tools/test_edit_tool.py +115 -0
  328. package/backend/tests/unit/tools/test_exec_tool.py +81 -0
  329. package/backend/tests/unit/tools/test_fetch_tool.py +65 -0
  330. package/backend/tests/unit/tools/test_manage_prompts_tool.py +117 -0
  331. package/backend/tests/unit/tools/test_manage_providers_tool.py +458 -0
  332. package/backend/tests/unit/tools/test_manage_roles_tool.py +411 -0
  333. package/backend/tests/unit/tools/test_manage_settings_tool.py +608 -0
  334. package/backend/tests/unit/tools/test_read_tool.py +33 -0
  335. package/backend/tests/unit/tools/test_set_permissions_tool.py +391 -0
  336. package/backend/tests/unit/tools/test_todo_tool.py +37 -0
  337. package/backend/tests/unit/tools/test_tool_registry.py +91 -0
  338. package/backend/uv.lock +1144 -0
  339. package/bin/flowent.mjs +62 -35
  340. package/dist/frontend/assets/AssistantPage-B3Xc08AS.js +1 -0
  341. package/dist/frontend/assets/ChannelsPage-ByLd28xk.js +1 -0
  342. package/dist/frontend/assets/HomePage-C0hAx9_l.js +3 -0
  343. package/dist/frontend/assets/McpPage-DkrYLvBv.js +7 -0
  344. package/dist/frontend/assets/PageScaffold-D4jO9ooX.js +1 -0
  345. package/dist/frontend/assets/PromptsPage-DWA7rRJd.js +1 -0
  346. package/dist/frontend/assets/ProvidersPage-PUWT8seJ.js +3 -0
  347. package/dist/frontend/assets/RolesPage-CqcclGRw.js +1 -0
  348. package/dist/frontend/assets/SettingsPage-8tS2cJgX.js +3 -0
  349. package/dist/frontend/assets/StatsPage-BX9khYzu.js +1 -0
  350. package/dist/frontend/assets/ToolsPage-9Tl9FdeD.js +1 -0
  351. package/dist/frontend/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
  352. package/dist/frontend/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
  353. package/dist/frontend/assets/alert-dialog-kFYVQ7oX.js +1 -0
  354. package/dist/frontend/assets/badge-74-3jsCg.js +1 -0
  355. package/dist/frontend/assets/constants-XUzFf6i1.js +1 -0
  356. package/dist/frontend/assets/datetime-m6_O_Ci9.js +1 -0
  357. package/dist/frontend/assets/dialog-BeGSweF6.js +1 -0
  358. package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  359. package/dist/frontend/assets/graph-vendor-CHpVij2M.css +1 -0
  360. package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +7 -0
  361. package/dist/frontend/assets/index-BHC1Vhy8.css +1 -0
  362. package/dist/frontend/assets/index-CL1ALZ3r.js +10 -0
  363. package/dist/frontend/assets/layout.worker-jMHqAFbP.js +24 -0
  364. package/dist/frontend/assets/markdown-vendor-DVdy_w12.js +29 -0
  365. package/dist/frontend/assets/modelParams-CaHd0903.js +1 -0
  366. package/dist/frontend/assets/react-vendor-mEs_JJxa.js +9 -0
  367. package/dist/frontend/assets/roles-2OLDeTc5.js +1 -0
  368. package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  369. package/dist/frontend/assets/select-DL_LPeDj.js +1 -0
  370. package/dist/frontend/assets/shared-CMxbpLeQ.js +1 -0
  371. package/dist/frontend/assets/triState-DEr3NkXV.js +1 -0
  372. package/dist/frontend/assets/ui-vendor-Dg9NNnWX.js +51 -0
  373. package/dist/frontend/index.html +36 -0
  374. package/package.json +28 -41
  375. package/dist/.next/BUILD_ID +0 -1
  376. package/dist/.next/app-path-routes-manifest.json +0 -6
  377. package/dist/.next/build-manifest.json +0 -20
  378. package/dist/.next/package.json +0 -1
  379. package/dist/.next/prerender-manifest.json +0 -114
  380. package/dist/.next/required-server-files.json +0 -333
  381. package/dist/.next/routes-manifest.json +0 -69
  382. package/dist/.next/server/app/_global-error/page/app-paths-manifest.json +0 -3
  383. package/dist/.next/server/app/_global-error/page/build-manifest.json +0 -16
  384. package/dist/.next/server/app/_global-error/page/next-font-manifest.json +0 -6
  385. package/dist/.next/server/app/_global-error/page/react-loadable-manifest.json +0 -1
  386. package/dist/.next/server/app/_global-error/page/server-reference-manifest.json +0 -4
  387. package/dist/.next/server/app/_global-error/page.js +0 -9
  388. package/dist/.next/server/app/_global-error/page.js.map +0 -5
  389. package/dist/.next/server/app/_global-error/page.js.nft.json +0 -1
  390. package/dist/.next/server/app/_global-error/page_client-reference-manifest.js +0 -3
  391. package/dist/.next/server/app/_global-error.html +0 -1
  392. package/dist/.next/server/app/_global-error.meta +0 -15
  393. package/dist/.next/server/app/_global-error.rsc +0 -14
  394. package/dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +0 -5
  395. package/dist/.next/server/app/_global-error.segments/_full.segment.rsc +0 -14
  396. package/dist/.next/server/app/_global-error.segments/_head.segment.rsc +0 -5
  397. package/dist/.next/server/app/_global-error.segments/_index.segment.rsc +0 -5
  398. package/dist/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
  399. package/dist/.next/server/app/_not-found/page/app-paths-manifest.json +0 -3
  400. package/dist/.next/server/app/_not-found/page/build-manifest.json +0 -16
  401. package/dist/.next/server/app/_not-found/page/next-font-manifest.json +0 -10
  402. package/dist/.next/server/app/_not-found/page/react-loadable-manifest.json +0 -1
  403. package/dist/.next/server/app/_not-found/page/server-reference-manifest.json +0 -4
  404. package/dist/.next/server/app/_not-found/page.js +0 -13
  405. package/dist/.next/server/app/_not-found/page.js.map +0 -5
  406. package/dist/.next/server/app/_not-found/page.js.nft.json +0 -1
  407. package/dist/.next/server/app/_not-found/page_client-reference-manifest.js +0 -3
  408. package/dist/.next/server/app/_not-found.html +0 -1
  409. package/dist/.next/server/app/_not-found.meta +0 -16
  410. package/dist/.next/server/app/_not-found.rsc +0 -16
  411. package/dist/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
  412. package/dist/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
  413. package/dist/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
  414. package/dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
  415. package/dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
  416. package/dist/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
  417. package/dist/.next/server/app/icon.svg/route/app-paths-manifest.json +0 -3
  418. package/dist/.next/server/app/icon.svg/route/build-manifest.json +0 -9
  419. package/dist/.next/server/app/icon.svg/route.js +0 -6
  420. package/dist/.next/server/app/icon.svg/route.js.map +0 -5
  421. package/dist/.next/server/app/icon.svg/route.js.nft.json +0 -1
  422. package/dist/.next/server/app/icon.svg.meta +0 -1
  423. package/dist/.next/server/app/index.html +0 -1
  424. package/dist/.next/server/app/index.meta +0 -14
  425. package/dist/.next/server/app/index.rsc +0 -15
  426. package/dist/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -5
  427. package/dist/.next/server/app/index.segments/_full.segment.rsc +0 -15
  428. package/dist/.next/server/app/index.segments/_head.segment.rsc +0 -6
  429. package/dist/.next/server/app/index.segments/_index.segment.rsc +0 -5
  430. package/dist/.next/server/app/index.segments/_tree.segment.rsc +0 -3
  431. package/dist/.next/server/app/page/app-paths-manifest.json +0 -3
  432. package/dist/.next/server/app/page/build-manifest.json +0 -16
  433. package/dist/.next/server/app/page/next-font-manifest.json +0 -10
  434. package/dist/.next/server/app/page/react-loadable-manifest.json +0 -1
  435. package/dist/.next/server/app/page/server-reference-manifest.json +0 -4
  436. package/dist/.next/server/app/page.js +0 -14
  437. package/dist/.next/server/app/page.js.map +0 -5
  438. package/dist/.next/server/app/page.js.nft.json +0 -1
  439. package/dist/.next/server/app/page_client-reference-manifest.js +0 -3
  440. package/dist/.next/server/app-paths-manifest.json +0 -6
  441. package/dist/.next/server/chunks/[externals]_next_dist_0arv.vj._.js +0 -3
  442. package/dist/.next/server/chunks/[root-of-the-server]__0vcj1q1._.js +0 -13
  443. package/dist/.next/server/chunks/[turbopack]_runtime.js +0 -903
  444. package/dist/.next/server/chunks/_next-internal_server_app_icon_svg_route_actions_0-0ehc~.js +0 -3
  445. package/dist/.next/server/chunks/ssr/05w9_next_dist_0ihu0u9._.js +0 -6
  446. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_12u3mib._.js +0 -3
  447. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_forbidden_04fbe_..js +0 -3
  448. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_global-error_0brpl_..js +0 -3
  449. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_unauthorized_0~2g66g.js +0 -3
  450. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_0~cyr1_.js +0 -4
  451. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_1105emf.js +0 -4
  452. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_11uhyqv.js +0 -4
  453. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0.t9_75._.js +0 -33
  454. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0c0ud_z._.js +0 -3
  455. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0f9_8d4._.js +0 -3
  456. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0l5ko41._.js +0 -19
  457. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0mn6z7i._.js +0 -3
  458. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0npxxst._.js +0 -33
  459. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0qjhaca._.js +0 -3
  460. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0rwgw3s._.js +0 -3
  461. package/dist/.next/server/chunks/ssr/[turbopack]_runtime.js +0 -903
  462. package/dist/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
  463. package/dist/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
  464. package/dist/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
  465. package/dist/.next/server/chunks/ssr/node_modules__pnpm_056~6.6._.js +0 -3
  466. package/dist/.next/server/chunks/ssr/node_modules__pnpm_0~j0k.e._.js +0 -33
  467. package/dist/.next/server/functions-config-manifest.json +0 -4
  468. package/dist/.next/server/middleware-build-manifest.js +0 -20
  469. package/dist/.next/server/middleware-manifest.json +0 -6
  470. package/dist/.next/server/next-font-manifest.js +0 -1
  471. package/dist/.next/server/next-font-manifest.json +0 -13
  472. package/dist/.next/server/pages/404.html +0 -1
  473. package/dist/.next/server/pages/500.html +0 -1
  474. package/dist/.next/server/pages-manifest.json +0 -4
  475. package/dist/.next/server/prefetch-hints.json +0 -1
  476. package/dist/.next/server/server-reference-manifest.js +0 -1
  477. package/dist/.next/server/server-reference-manifest.json +0 -5
  478. package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_buildManifest.js +0 -11
  479. package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_clientMiddlewareManifest.js +0 -1
  480. package/dist/.next/static/7FFlzRe2eS-D0Lw5oEpmC/_ssgManifest.js +0 -1
  481. package/dist/.next/static/chunks/01qk2~bgf76vu.js +0 -1
  482. package/dist/.next/static/chunks/03~yq9q893hmn.js +0 -1
  483. package/dist/.next/static/chunks/080queev.r2uy.js +0 -31
  484. package/dist/.next/static/chunks/0v3lyuj75aq50.js +0 -1
  485. package/dist/.next/static/chunks/10b~xdx5c-i7s.js +0 -5
  486. package/dist/.next/static/chunks/15~9l5n.~r-.4.css +0 -2
  487. package/dist/.next/static/chunks/turbopack-0m-970~qvs7sc.js +0 -1
  488. package/dist/.next/static/media/7178b3e590c64307-s.11.cyxs5p-0z~.woff2 +0 -0
  489. package/dist/.next/static/media/8a480f0b521d4e75-s.06d3mdzz5bre_.woff2 +0 -0
  490. package/dist/.next/static/media/caa3a2e1cccd8315-s.p.16t1db8_9y2o~.woff2 +0 -0
  491. package/dist/package.json +0 -87
  492. package/dist/server.js +0 -38
  493. /package/{dist/.next/server/app/icon.svg.body → backend/src/flowent/static/favicon.svg} +0 -0
  494. /package/dist/{.next/static/media/icon.0.r~afrtrocz9.svg → frontend/favicon.svg} +0 -0
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from typing import Literal
5
+
6
+ from fastapi import APIRouter, HTTPException
7
+ from pydantic import BaseModel
8
+
9
+ from flowent.assistant_commands import (
10
+ AssistantCommandError,
11
+ execute_assistant_command_input,
12
+ )
13
+ from flowent.image_assets import require_image_asset
14
+ from flowent.models import (
15
+ Message,
16
+ content_parts_to_text,
17
+ has_image_parts,
18
+ parse_content_parts_payload,
19
+ )
20
+ from flowent.models import TextPart as ModelTextPart
21
+ from flowent.providers.errors import LLMProviderError
22
+ from flowent.registry import registry
23
+
24
+ router = APIRouter()
25
+
26
+
27
+ def _get_assistant():
28
+ return registry.get_assistant()
29
+
30
+
31
+ @router.get("/api/assistant")
32
+ async def get_assistant() -> dict:
33
+ assistant = _get_assistant()
34
+ if assistant is None:
35
+ raise HTTPException(status_code=404, detail="Assistant not found")
36
+ return {
37
+ "id": assistant.uuid,
38
+ "name": assistant.config.name,
39
+ "role_name": assistant.config.role_name,
40
+ "state": assistant.state.value,
41
+ "connections": assistant.get_connections_snapshot(),
42
+ }
43
+
44
+
45
+ class AssistantMessageRequest(BaseModel):
46
+ content: str | None = None
47
+ parts: list[AssistantMessagePart] | None = None
48
+
49
+
50
+ class AssistantMessagePart(BaseModel):
51
+ type: Literal["text", "image"]
52
+ text: str | None = None
53
+ asset_id: str | None = None
54
+ mime_type: str | None = None
55
+ width: int | None = None
56
+ height: int | None = None
57
+ alt: str | None = None
58
+
59
+
60
+ AssistantMessageRequest.model_rebuild()
61
+
62
+
63
+ class AssistantRetryResponse(BaseModel):
64
+ status: Literal["retried"]
65
+ message_id: str
66
+
67
+
68
+ def _parse_request_parts(req: AssistantMessageRequest):
69
+ if req.parts:
70
+ parts = parse_content_parts_payload(
71
+ [part.model_dump(exclude_none=True) for part in req.parts]
72
+ )
73
+ elif isinstance(req.content, str):
74
+ parts = [ModelTextPart(text=req.content)]
75
+ else:
76
+ parts = []
77
+ if not parts:
78
+ raise HTTPException(status_code=400, detail="Assistant message cannot be empty")
79
+ if not has_image_parts(parts) and not content_parts_to_text(parts).strip():
80
+ raise HTTPException(status_code=400, detail="Assistant message cannot be empty")
81
+ for part in parts:
82
+ asset_id = getattr(part, "asset_id", None)
83
+ if isinstance(asset_id, str):
84
+ require_image_asset(asset_id)
85
+ return parts
86
+
87
+
88
+ @router.post("/api/assistant/message")
89
+ async def send_assistant_message(req: AssistantMessageRequest) -> dict:
90
+ assistant = _get_assistant()
91
+ if assistant is None:
92
+ raise HTTPException(status_code=404, detail="Assistant not found")
93
+
94
+ try:
95
+ parts = _parse_request_parts(req)
96
+ except ValueError as exc:
97
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
98
+
99
+ if has_image_parts(parts) and not assistant.supports_input_image():
100
+ raise HTTPException(
101
+ status_code=409,
102
+ detail="Assistant current model does not support `input_image`.",
103
+ )
104
+
105
+ command_input = (
106
+ parts[0].text
107
+ if len(parts) == 1 and isinstance(parts[0], ModelTextPart)
108
+ else None
109
+ )
110
+
111
+ try:
112
+ executed_command = (
113
+ execute_assistant_command_input(assistant, command_input)
114
+ if isinstance(command_input, str)
115
+ else None
116
+ )
117
+ except AssistantCommandError as exc:
118
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
119
+ except (RuntimeError, TimeoutError, LLMProviderError) as exc:
120
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
121
+
122
+ if executed_command is not None:
123
+ return {
124
+ "status": "command_executed",
125
+ "command_name": executed_command.command_name,
126
+ }
127
+
128
+ message_id = str(uuid.uuid4())
129
+ msg = Message(
130
+ from_id="human",
131
+ to_id=assistant.uuid,
132
+ parts=parts,
133
+ message_id=message_id,
134
+ )
135
+ assistant.enqueue_message(msg)
136
+ return {"status": "sent", "message_id": message_id}
137
+
138
+
139
+ @router.post(
140
+ "/api/assistant/messages/{message_id}/retry",
141
+ response_model=AssistantRetryResponse,
142
+ )
143
+ async def retry_assistant_message(message_id: str) -> AssistantRetryResponse:
144
+ assistant = _get_assistant()
145
+ if assistant is None:
146
+ raise HTTPException(status_code=404, detail="Assistant not found")
147
+
148
+ try:
149
+ retried_message_id = assistant.retry_human_message(message_id=message_id)
150
+ except LookupError as exc:
151
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
152
+ except (RuntimeError, TimeoutError, ValueError) as exc:
153
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
154
+
155
+ return AssistantRetryResponse(status="retried", message_id=retried_message_id)
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, File, HTTPException, UploadFile
4
+ from fastapi.responses import FileResponse
5
+
6
+ from flowent.image_assets import create_image_asset, get_image_asset
7
+
8
+ router = APIRouter()
9
+ image_upload_file = File(...)
10
+
11
+
12
+ @router.post("/api/image-assets")
13
+ async def upload_image_asset(
14
+ file: UploadFile = image_upload_file,
15
+ ) -> dict[str, object]:
16
+ try:
17
+ data = await file.read()
18
+ asset = create_image_asset(
19
+ data,
20
+ mime_type=file.content_type,
21
+ original_name=file.filename,
22
+ )
23
+ except ValueError as exc:
24
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
25
+ return asset.serialize()
26
+
27
+
28
+ @router.get("/api/image-assets/{asset_id}")
29
+ async def get_uploaded_image_asset(asset_id: str) -> FileResponse:
30
+ asset = get_image_asset(asset_id)
31
+ if asset is None:
32
+ raise HTTPException(status_code=404, detail="Image asset not found")
33
+ return FileResponse(asset.file_path, media_type=asset.mime_type)
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+ from flowent.mcp_service import MCPError, mcp_service
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ class MCPServerMutationRequest(BaseModel):
12
+ name: str
13
+ transport: str
14
+ enabled: bool = True
15
+ required: bool = False
16
+ startup_timeout_sec: int = 10
17
+ tool_timeout_sec: int = 30
18
+ enabled_tools: list[str] = []
19
+ disabled_tools: list[str] = []
20
+ scopes: list[str] = []
21
+ oauth_resource: str = ""
22
+ launcher: str = ""
23
+ command: str = ""
24
+ args: list[str] = []
25
+ env: dict[str, str] = {}
26
+ env_vars: list[str] = []
27
+ cwd: str = ""
28
+ url: str = ""
29
+ bearer_token_env_var: str = ""
30
+ http_headers: dict[str, str] = {}
31
+ env_http_headers: list[str] = []
32
+
33
+
34
+ class MCPPromptPreviewRequest(BaseModel):
35
+ name: str
36
+ arguments: dict[str, object] = {}
37
+
38
+
39
+ @router.get("/api/mcp")
40
+ async def get_mcp_state() -> dict[str, object]:
41
+ return {"servers": mcp_service.list_server_payloads()}
42
+
43
+
44
+ @router.post("/api/mcp/refresh")
45
+ async def refresh_all_mcp_servers() -> dict[str, object]:
46
+ return {"servers": mcp_service.refresh_all()}
47
+
48
+
49
+ @router.post("/api/mcp/servers")
50
+ async def create_mcp_server(req: MCPServerMutationRequest) -> dict[str, object]:
51
+ try:
52
+ snapshot = mcp_service.create_or_update_server(
53
+ current_name=None,
54
+ config_data=req.model_dump(),
55
+ )
56
+ except MCPError as exc:
57
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
58
+ return {"snapshot": snapshot}
59
+
60
+
61
+ @router.patch("/api/mcp/servers/{server_name}")
62
+ async def update_mcp_server(
63
+ server_name: str,
64
+ req: MCPServerMutationRequest,
65
+ ) -> dict[str, object]:
66
+ try:
67
+ snapshot = mcp_service.create_or_update_server(
68
+ current_name=server_name,
69
+ config_data=req.model_dump(),
70
+ )
71
+ except MCPError as exc:
72
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
73
+ return {"snapshot": snapshot}
74
+
75
+
76
+ @router.delete("/api/mcp/servers/{server_name}")
77
+ async def delete_mcp_server(server_name: str) -> dict[str, object]:
78
+ try:
79
+ mcp_service.delete_server(server_name)
80
+ except MCPError as exc:
81
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
82
+ return {"status": "deleted", "server_name": server_name}
83
+
84
+
85
+ @router.post("/api/mcp/servers/{server_name}/refresh")
86
+ async def refresh_mcp_server(server_name: str) -> dict[str, object]:
87
+ try:
88
+ snapshot = mcp_service.refresh_server(server_name)
89
+ except MCPError as exc:
90
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
91
+ return {"snapshot": snapshot}
92
+
93
+
94
+ @router.post("/api/mcp/servers/{server_name}/login")
95
+ async def login_mcp_server(server_name: str) -> dict[str, object]:
96
+ try:
97
+ snapshot = mcp_service.login_server(server_name)
98
+ except MCPError as exc:
99
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
100
+ return {"snapshot": snapshot}
101
+
102
+
103
+ @router.post("/api/mcp/servers/{server_name}/logout")
104
+ async def logout_mcp_server(server_name: str) -> dict[str, object]:
105
+ try:
106
+ snapshot = mcp_service.logout_server(server_name)
107
+ except MCPError as exc:
108
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
109
+ return {"snapshot": snapshot}
110
+
111
+
112
+ @router.post("/api/mcp/servers/{server_name}/prompt-preview")
113
+ async def preview_mcp_prompt(
114
+ server_name: str,
115
+ req: MCPPromptPreviewRequest,
116
+ ) -> dict[str, object]:
117
+ try:
118
+ preview = mcp_service.preview_server_prompt(
119
+ server_name=server_name,
120
+ name=req.name,
121
+ arguments=req.arguments,
122
+ )
123
+ except MCPError as exc:
124
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
125
+ return {"preview": preview}
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/health")
9
+ async def health_check() -> dict:
10
+ return {"status": "healthy"}
11
+
12
+
13
+ @router.get("/api/meta")
14
+ async def get_meta() -> dict:
15
+ from flowent._version import __version__
16
+ from flowent.providers.registry import ProviderType
17
+
18
+ return {
19
+ "provider_types": [pt.value for pt in ProviderType],
20
+ "version": __version__,
21
+ }
22
+
23
+
24
+ @router.get("/api/tools")
25
+ async def list_tools() -> dict:
26
+ from flowent.tools import list_agent_visible_tool_descriptors
27
+
28
+ return {"tools": list_agent_visible_tool_descriptors()}
@@ -0,0 +1,365 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from pydantic import BaseModel
7
+
8
+ from flowent.graph_service import is_tab_leader, list_node_connection_ids
9
+ from flowent.registry import registry
10
+ from flowent.settings import (
11
+ find_provider,
12
+ find_role,
13
+ get_settings,
14
+ resolve_model_info,
15
+ )
16
+ from flowent.tools import MINIMUM_TOOLS
17
+ from flowent.workspace_store import workspace_store
18
+
19
+ router = APIRouter()
20
+
21
+
22
+ class DispatchNodeMessagePart(BaseModel):
23
+ type: Literal["text", "image"]
24
+ text: str | None = None
25
+ asset_id: str | None = None
26
+ mime_type: str | None = None
27
+ width: int | None = None
28
+ height: int | None = None
29
+ alt: str | None = None
30
+
31
+
32
+ class DispatchNodeMessageRequest(BaseModel):
33
+ content: str | None = None
34
+ parts: list[DispatchNodeMessagePart] | None = None
35
+ from_id: str = "human"
36
+
37
+
38
+ DispatchNodeMessageRequest.model_rebuild()
39
+
40
+
41
+ def _parse_dispatch_parts(req: DispatchNodeMessageRequest):
42
+ from flowent.image_assets import require_image_asset
43
+ from flowent.models import TextPart as ModelTextPart
44
+ from flowent.models import (
45
+ content_parts_to_text,
46
+ has_image_parts,
47
+ parse_content_parts_payload,
48
+ )
49
+
50
+ if req.parts:
51
+ parts = parse_content_parts_payload(
52
+ [part.model_dump(exclude_none=True) for part in req.parts]
53
+ )
54
+ elif isinstance(req.content, str):
55
+ parts = [ModelTextPart(text=req.content)]
56
+ else:
57
+ parts = []
58
+
59
+ if not parts:
60
+ raise HTTPException(status_code=400, detail="Node message cannot be empty")
61
+ if not has_image_parts(parts) and not content_parts_to_text(parts).strip():
62
+ raise HTTPException(status_code=400, detail="Node message cannot be empty")
63
+
64
+ for part in parts:
65
+ asset_id = getattr(part, "asset_id", None)
66
+ if isinstance(asset_id, str):
67
+ require_image_asset(asset_id)
68
+ return parts
69
+
70
+
71
+ def _serialize_model_capabilities(role_name: str | None) -> dict[str, bool] | None:
72
+ settings = get_settings()
73
+ provider_id = settings.model.active_provider_id
74
+ model_id = settings.model.active_model
75
+ use_system_model_overrides = True
76
+ role_cfg = find_role(settings, role_name) if role_name else None
77
+ if (
78
+ role_cfg is not None
79
+ and role_cfg.model is not None
80
+ and role_cfg.model.provider_id
81
+ and role_cfg.model.model
82
+ ):
83
+ provider_id = role_cfg.model.provider_id
84
+ model_id = role_cfg.model.model
85
+ use_system_model_overrides = False
86
+ if not provider_id or not model_id:
87
+ return None
88
+ provider = find_provider(settings, provider_id)
89
+ if provider is None:
90
+ return None
91
+ model_info = resolve_model_info(
92
+ provider=provider,
93
+ model_id=model_id,
94
+ input_image=settings.model.input_image if use_system_model_overrides else None,
95
+ output_image=(
96
+ settings.model.output_image if use_system_model_overrides else None
97
+ ),
98
+ context_window_tokens=(
99
+ settings.model.context_window_tokens if use_system_model_overrides else None
100
+ ),
101
+ )
102
+ return {
103
+ "input_image": model_info.capabilities.input_image,
104
+ "output_image": model_info.capabilities.output_image,
105
+ }
106
+
107
+
108
+ @router.get("/api/nodes")
109
+ async def list_nodes() -> dict:
110
+ nodes_by_id: dict[str, dict[str, object]] = {}
111
+
112
+ assistant = registry.get_assistant()
113
+ if assistant is not None:
114
+ nodes_by_id[assistant.uuid] = {
115
+ "id": assistant.uuid,
116
+ "node_type": assistant.config.node_type.value,
117
+ "workflow_id": assistant.config.tab_id,
118
+ "role_name": assistant.config.role_name,
119
+ "state": assistant.state.value,
120
+ "connections": assistant.get_connections_snapshot(),
121
+ "name": assistant.config.name,
122
+ "is_leader": False,
123
+ "todos": [t.serialize() for t in assistant.todos],
124
+ "capabilities": _serialize_model_capabilities(assistant.config.role_name),
125
+ "position": None,
126
+ }
127
+
128
+ for record in workspace_store.list_node_records():
129
+ live = registry.get(record.id)
130
+ nodes_by_id[record.id] = {
131
+ "id": record.id,
132
+ "node_type": record.config.node_type.value,
133
+ "workflow_id": record.config.tab_id,
134
+ "role_name": record.config.role_name,
135
+ "is_leader": is_tab_leader(node_id=record.id, tab_id=record.config.tab_id),
136
+ "state": (live.state if live is not None else record.state).value,
137
+ "connections": (
138
+ list_node_connection_ids(
139
+ tab_id=record.config.tab_id,
140
+ node_id=record.id,
141
+ )
142
+ if record.config.tab_id
143
+ else []
144
+ ),
145
+ "name": record.config.name,
146
+ "todos": [
147
+ todo.serialize()
148
+ for todo in (
149
+ live.get_todos_snapshot() if live is not None else record.todos
150
+ )
151
+ ],
152
+ "capabilities": _serialize_model_capabilities(record.config.role_name),
153
+ "position": record.position.serialize()
154
+ if record.position is not None
155
+ else None,
156
+ }
157
+
158
+ for node in registry.get_all():
159
+ if node.uuid in nodes_by_id:
160
+ continue
161
+ nodes_by_id[node.uuid] = {
162
+ "id": node.uuid,
163
+ "node_type": node.config.node_type.value,
164
+ "workflow_id": node.config.tab_id,
165
+ "role_name": node.config.role_name,
166
+ "is_leader": is_tab_leader(node_id=node.uuid, tab_id=node.config.tab_id),
167
+ "state": node.state.value,
168
+ "connections": (
169
+ list_node_connection_ids(
170
+ tab_id=node.config.tab_id,
171
+ node_id=node.uuid,
172
+ )
173
+ if node.config.tab_id
174
+ else node.get_connections_snapshot()
175
+ ),
176
+ "name": node.config.name,
177
+ "todos": [t.serialize() for t in node.todos],
178
+ "capabilities": _serialize_model_capabilities(node.config.role_name),
179
+ "position": None,
180
+ }
181
+
182
+ return {
183
+ "nodes": list(nodes_by_id.values()),
184
+ }
185
+
186
+
187
+ @router.get("/api/nodes/{node_id}")
188
+ async def get_node(node_id: str) -> dict:
189
+ node = registry.get(node_id)
190
+ record = workspace_store.get_node_record(node_id)
191
+
192
+ if node is None and record is None:
193
+ raise HTTPException(status_code=404, detail="Node not found")
194
+
195
+ if node is not None:
196
+ record_id = node.uuid
197
+ record_state = node.state
198
+ target_config = node.config
199
+ else:
200
+ assert record is not None
201
+ record_id = record.id
202
+ record_state = record.state
203
+ target_config = record.config
204
+ history = (
205
+ node.get_history_snapshot()
206
+ if node is not None
207
+ else (record.history if record is not None else [])
208
+ )
209
+ todos = (
210
+ node.get_todos_snapshot()
211
+ if node is not None
212
+ else (record.todos if record is not None else [])
213
+ )
214
+
215
+ return {
216
+ "id": record_id,
217
+ "node_type": target_config.node_type.value,
218
+ "workflow_id": target_config.tab_id,
219
+ "role_name": target_config.role_name,
220
+ "is_leader": is_tab_leader(node_id=record_id, tab_id=target_config.tab_id),
221
+ "state": record_state.value,
222
+ "contacts": node.get_contact_ids_snapshot() if node is not None else [],
223
+ "connections": (
224
+ list_node_connection_ids(
225
+ tab_id=target_config.tab_id,
226
+ node_id=record_id,
227
+ )
228
+ if target_config.tab_id
229
+ else (node.get_connections_snapshot() if node is not None else [])
230
+ ),
231
+ "name": target_config.name,
232
+ "todos": [t.serialize() for t in todos],
233
+ "capabilities": _serialize_model_capabilities(target_config.role_name),
234
+ "tools": sorted(set(target_config.tools) | set(MINIMUM_TOOLS)),
235
+ "write_dirs": list(target_config.write_dirs),
236
+ "allow_network": target_config.allow_network,
237
+ "position": record.position.serialize()
238
+ if record is not None and record.position is not None
239
+ else None,
240
+ "history": [entry.serialize() for entry in history],
241
+ }
242
+
243
+
244
+ @router.post("/api/nodes/{node_id}/terminate")
245
+ async def terminate_node(node_id: str) -> dict:
246
+ node = registry.get(node_id)
247
+ if node is None:
248
+ raise HTTPException(status_code=404, detail="Node not found")
249
+
250
+ from flowent.models import NodeType
251
+
252
+ if node.config.node_type == NodeType.ASSISTANT:
253
+ raise HTTPException(status_code=400, detail="Cannot terminate assistant")
254
+ if is_tab_leader(node_id=node.uuid, tab_id=node.config.tab_id):
255
+ raise HTTPException(
256
+ status_code=400,
257
+ detail="Cannot terminate a workflow Leader directly",
258
+ )
259
+
260
+ node.request_termination("user_requested")
261
+ return {"status": "terminating"}
262
+
263
+
264
+ @router.post("/api/nodes/{node_id}/interrupt")
265
+ async def interrupt_node(node_id: str) -> dict:
266
+ node = registry.get(node_id)
267
+ if node is None:
268
+ raise HTTPException(status_code=404, detail="Node not found")
269
+ if not node.request_interrupt():
270
+ return {"status": "ignored"}
271
+ return {"status": "interrupting"}
272
+
273
+
274
+ @router.post("/api/nodes/{node_id}/messages/{message_id}/retry")
275
+ async def retry_node_message(node_id: str, message_id: str) -> dict:
276
+ node = registry.get(node_id)
277
+ if node is None:
278
+ raise HTTPException(status_code=404, detail="Node not found")
279
+ from flowent.models import NodeType
280
+
281
+ if node.config.node_type == NodeType.ASSISTANT:
282
+ raise HTTPException(
283
+ status_code=400,
284
+ detail="Use /api/assistant/messages/{message_id}/retry for Assistant retry",
285
+ )
286
+ if not is_tab_leader(node_id=node.uuid, tab_id=node.config.tab_id):
287
+ raise HTTPException(
288
+ status_code=400,
289
+ detail="Only a Workflow Leader can retry chat history",
290
+ )
291
+ try:
292
+ retried_message_id = node.retry_received_message(message_id=message_id)
293
+ except LookupError as exc:
294
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
295
+ except (RuntimeError, TimeoutError, ValueError) as exc:
296
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
297
+ return {"status": "retried", "message_id": retried_message_id}
298
+
299
+
300
+ @router.post("/api/nodes/{node_id}/clear-chat")
301
+ async def clear_node_chat(node_id: str) -> dict:
302
+ from flowent.models import NodeType
303
+
304
+ node = registry.get(node_id)
305
+ if node is None:
306
+ raise HTTPException(status_code=404, detail="Node not found")
307
+ if node.config.node_type != NodeType.ASSISTANT:
308
+ raise HTTPException(status_code=400, detail="Can only clear assistant chat")
309
+
310
+ try:
311
+ node.clear_chat_history()
312
+ except RuntimeError as exc:
313
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
314
+ except TimeoutError as exc:
315
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
316
+
317
+ return {"status": "cleared"}
318
+
319
+
320
+ @router.post("/api/nodes/{node_id}/messages")
321
+ async def dispatch_node_message(node_id: str, req: DispatchNodeMessageRequest) -> dict:
322
+ from flowent.graph_service import dispatch_node_message
323
+ from flowent.models import NodeType, content_parts_to_text, has_image_parts
324
+
325
+ node = registry.get(node_id)
326
+ if node is None:
327
+ raise HTTPException(status_code=404, detail="Node not found")
328
+
329
+ if req.from_id != "human":
330
+ raise HTTPException(
331
+ status_code=400,
332
+ detail="Web UI node messages must originate from `human`",
333
+ )
334
+
335
+ if node.config.node_type == NodeType.ASSISTANT:
336
+ raise HTTPException(
337
+ status_code=400,
338
+ detail="Use /api/assistant/message for Assistant input",
339
+ )
340
+ if not is_tab_leader(node_id=node.uuid, tab_id=node.config.tab_id):
341
+ raise HTTPException(
342
+ status_code=400,
343
+ detail="Human input can only target Assistant or a Workflow Leader",
344
+ )
345
+
346
+ try:
347
+ parts = _parse_dispatch_parts(req)
348
+ except ValueError as exc:
349
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
350
+
351
+ if has_image_parts(parts) and not node.supports_input_image():
352
+ raise HTTPException(
353
+ status_code=409,
354
+ detail="Current model does not support `input_image`.",
355
+ )
356
+
357
+ error, message_id = dispatch_node_message(
358
+ node_id=node_id,
359
+ content=content_parts_to_text(parts),
360
+ parts=parts,
361
+ from_id="human",
362
+ )
363
+ if error is not None:
364
+ raise HTTPException(status_code=400, detail=error)
365
+ return {"status": "sent", "message_id": message_id}