flowent 0.0.1 → 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 (493) hide show
  1. package/README.md +19 -8
  2. package/backend/.python-version +1 -0
  3. package/backend/pyproject.toml +57 -0
  4. package/backend/src/flowent/__init__.py +3 -0
  5. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
  23. package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
  24. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  25. package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
  26. package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
  27. package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
  28. package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
  29. package/backend/src/flowent/__pycache__/stats_service.cpython-313.pyc +0 -0
  30. package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
  31. package/backend/src/flowent/_version.py +7 -0
  32. package/backend/src/flowent/access.py +247 -0
  33. package/backend/src/flowent/agent.py +2808 -0
  34. package/backend/src/flowent/assistant_commands.py +106 -0
  35. package/backend/src/flowent/channels/__init__.py +3 -0
  36. package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
  38. package/backend/src/flowent/channels/telegram.py +615 -0
  39. package/backend/src/flowent/cli.py +85 -0
  40. package/backend/src/flowent/config.py +14 -0
  41. package/backend/src/flowent/dev.py +3 -0
  42. package/backend/src/flowent/events.py +157 -0
  43. package/backend/src/flowent/graph_runtime.py +60 -0
  44. package/backend/src/flowent/graph_service.py +1346 -0
  45. package/backend/src/flowent/image_assets.py +356 -0
  46. package/backend/src/flowent/logging.py +155 -0
  47. package/backend/src/flowent/main.py +124 -0
  48. package/backend/src/flowent/mcp_service.py +1904 -0
  49. package/backend/src/flowent/model_metadata.py +98 -0
  50. package/backend/src/flowent/models/__init__.py +121 -0
  51. package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
  52. package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
  53. package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
  54. package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
  55. package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
  56. package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
  57. package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
  58. package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
  59. package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
  60. package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
  61. package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
  62. package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
  63. package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
  64. package/backend/src/flowent/models/agent.py +33 -0
  65. package/backend/src/flowent/models/base.py +24 -0
  66. package/backend/src/flowent/models/blueprint.py +176 -0
  67. package/backend/src/flowent/models/content.py +164 -0
  68. package/backend/src/flowent/models/delta.py +44 -0
  69. package/backend/src/flowent/models/event.py +51 -0
  70. package/backend/src/flowent/models/graph.py +437 -0
  71. package/backend/src/flowent/models/history.py +214 -0
  72. package/backend/src/flowent/models/llm.py +61 -0
  73. package/backend/src/flowent/models/message.py +27 -0
  74. package/backend/src/flowent/models/tab.py +48 -0
  75. package/backend/src/flowent/models/todo.py +10 -0
  76. package/backend/src/flowent/network.py +146 -0
  77. package/backend/src/flowent/prompts/__init__.py +67 -0
  78. package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  79. package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
  80. package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
  81. package/backend/src/flowent/prompts/common.py +250 -0
  82. package/backend/src/flowent/prompts/steward.py +64 -0
  83. package/backend/src/flowent/providers/__init__.py +23 -0
  84. package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
  86. package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
  87. package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
  88. package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
  89. package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
  90. package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
  91. package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
  92. package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
  93. package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
  94. package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
  95. package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
  96. package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
  97. package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
  98. package/backend/src/flowent/providers/anthropic.py +468 -0
  99. package/backend/src/flowent/providers/base_url.py +60 -0
  100. package/backend/src/flowent/providers/configuration.py +182 -0
  101. package/backend/src/flowent/providers/content.py +122 -0
  102. package/backend/src/flowent/providers/errors.py +223 -0
  103. package/backend/src/flowent/providers/gateway.py +169 -0
  104. package/backend/src/flowent/providers/gemini.py +447 -0
  105. package/backend/src/flowent/providers/headers.py +20 -0
  106. package/backend/src/flowent/providers/management.py +96 -0
  107. package/backend/src/flowent/providers/ollama.py +293 -0
  108. package/backend/src/flowent/providers/openai.py +422 -0
  109. package/backend/src/flowent/providers/openai_responses.py +655 -0
  110. package/backend/src/flowent/providers/registry.py +144 -0
  111. package/backend/src/flowent/providers/sse.py +31 -0
  112. package/backend/src/flowent/providers/thinking.py +79 -0
  113. package/backend/src/flowent/registry.py +73 -0
  114. package/backend/src/flowent/role_management.py +255 -0
  115. package/backend/src/flowent/routes/__init__.py +30 -0
  116. package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  117. package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
  118. package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
  119. package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
  120. package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
  121. package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
  122. package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
  123. package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
  124. package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
  125. package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
  126. package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
  127. package/backend/src/flowent/routes/__pycache__/stats.cpython-313.pyc +0 -0
  128. package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
  129. package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
  130. package/backend/src/flowent/routes/access.py +48 -0
  131. package/backend/src/flowent/routes/assistant.py +155 -0
  132. package/backend/src/flowent/routes/image_assets.py +33 -0
  133. package/backend/src/flowent/routes/mcp.py +125 -0
  134. package/backend/src/flowent/routes/meta.py +28 -0
  135. package/backend/src/flowent/routes/nodes.py +365 -0
  136. package/backend/src/flowent/routes/prompts.py +46 -0
  137. package/backend/src/flowent/routes/providers_route.py +364 -0
  138. package/backend/src/flowent/routes/roles.py +207 -0
  139. package/backend/src/flowent/routes/settings.py +324 -0
  140. package/backend/src/flowent/routes/stats.py +229 -0
  141. package/backend/src/flowent/routes/tabs.py +292 -0
  142. package/backend/src/flowent/routes/ws.py +33 -0
  143. package/backend/src/flowent/runtime.py +188 -0
  144. package/backend/src/flowent/sandbox.py +45 -0
  145. package/backend/src/flowent/security.py +42 -0
  146. package/backend/src/flowent/settings.py +2467 -0
  147. package/backend/src/flowent/settings_management.py +286 -0
  148. package/backend/src/flowent/state_db.py +120 -0
  149. package/backend/src/flowent/static/assets/AssistantPage-B3Xc08AS.js +1 -0
  150. package/backend/src/flowent/static/assets/ChannelsPage-ByLd28xk.js +1 -0
  151. package/backend/src/flowent/static/assets/HomePage-C0hAx9_l.js +3 -0
  152. package/backend/src/flowent/static/assets/McpPage-DkrYLvBv.js +7 -0
  153. package/backend/src/flowent/static/assets/PageScaffold-D4jO9ooX.js +1 -0
  154. package/backend/src/flowent/static/assets/PromptsPage-DWA7rRJd.js +1 -0
  155. package/backend/src/flowent/static/assets/ProvidersPage-PUWT8seJ.js +3 -0
  156. package/backend/src/flowent/static/assets/RolesPage-CqcclGRw.js +1 -0
  157. package/backend/src/flowent/static/assets/SettingsPage-8tS2cJgX.js +3 -0
  158. package/backend/src/flowent/static/assets/StatsPage-BX9khYzu.js +1 -0
  159. package/backend/src/flowent/static/assets/ToolsPage-9Tl9FdeD.js +1 -0
  160. package/backend/src/flowent/static/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
  161. package/backend/src/flowent/static/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
  162. package/backend/src/flowent/static/assets/alert-dialog-kFYVQ7oX.js +1 -0
  163. package/backend/src/flowent/static/assets/badge-74-3jsCg.js +1 -0
  164. package/backend/src/flowent/static/assets/constants-XUzFf6i1.js +1 -0
  165. package/backend/src/flowent/static/assets/datetime-m6_O_Ci9.js +1 -0
  166. package/backend/src/flowent/static/assets/dialog-BeGSweF6.js +1 -0
  167. package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  168. package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +1 -0
  169. package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +7 -0
  170. package/backend/src/flowent/static/assets/index-BHC1Vhy8.css +1 -0
  171. package/backend/src/flowent/static/assets/index-CL1ALZ3r.js +10 -0
  172. package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +24 -0
  173. package/backend/src/flowent/static/assets/markdown-vendor-DVdy_w12.js +29 -0
  174. package/backend/src/flowent/static/assets/modelParams-CaHd0903.js +1 -0
  175. package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +9 -0
  176. package/backend/src/flowent/static/assets/roles-2OLDeTc5.js +1 -0
  177. package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  178. package/backend/src/flowent/static/assets/select-DL_LPeDj.js +1 -0
  179. package/backend/src/flowent/static/assets/shared-CMxbpLeQ.js +1 -0
  180. package/backend/src/flowent/static/assets/triState-DEr3NkXV.js +1 -0
  181. package/backend/src/flowent/static/assets/ui-vendor-Dg9NNnWX.js +51 -0
  182. package/backend/src/flowent/static/index.html +36 -0
  183. package/backend/src/flowent/stats_service.py +218 -0
  184. package/backend/src/flowent/tools/__init__.py +201 -0
  185. package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  186. package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
  187. package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
  188. package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
  189. package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
  190. package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
  191. package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
  192. package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
  193. package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
  194. package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
  195. package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
  196. package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
  197. package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
  198. package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
  199. package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
  200. package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
  201. package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
  202. package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
  203. package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
  204. package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
  205. package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
  206. package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
  207. package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
  208. package/backend/src/flowent/tools/connect.py +156 -0
  209. package/backend/src/flowent/tools/contacts.py +22 -0
  210. package/backend/src/flowent/tools/create_agent.py +270 -0
  211. package/backend/src/flowent/tools/create_tab.py +59 -0
  212. package/backend/src/flowent/tools/delete_tab.py +39 -0
  213. package/backend/src/flowent/tools/edit.py +142 -0
  214. package/backend/src/flowent/tools/exec.py +117 -0
  215. package/backend/src/flowent/tools/fetch.py +85 -0
  216. package/backend/src/flowent/tools/idle.py +27 -0
  217. package/backend/src/flowent/tools/list_roles.py +50 -0
  218. package/backend/src/flowent/tools/list_tabs.py +96 -0
  219. package/backend/src/flowent/tools/list_tools.py +24 -0
  220. package/backend/src/flowent/tools/manage_prompts.py +102 -0
  221. package/backend/src/flowent/tools/manage_providers.py +220 -0
  222. package/backend/src/flowent/tools/manage_roles.py +275 -0
  223. package/backend/src/flowent/tools/manage_settings.py +346 -0
  224. package/backend/src/flowent/tools/mcp.py +199 -0
  225. package/backend/src/flowent/tools/read.py +152 -0
  226. package/backend/src/flowent/tools/send.py +50 -0
  227. package/backend/src/flowent/tools/set_permissions.py +84 -0
  228. package/backend/src/flowent/tools/sleep.py +41 -0
  229. package/backend/src/flowent/tools/todo.py +51 -0
  230. package/backend/src/flowent/workspace_store.py +479 -0
  231. package/backend/tests/__init__.py +0 -0
  232. package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  233. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  234. package/backend/tests/conftest.py +6 -0
  235. package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  236. package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
  237. package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
  238. package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
  239. package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
  240. package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
  241. package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
  242. package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
  243. package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
  244. package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
  245. package/backend/tests/integration/api/conftest.py +29 -0
  246. package/backend/tests/integration/api/test_access_api.py +182 -0
  247. package/backend/tests/integration/api/test_assistant_api.py +354 -0
  248. package/backend/tests/integration/api/test_frontend_mounting.py +61 -0
  249. package/backend/tests/integration/api/test_mcp_api.py +116 -0
  250. package/backend/tests/integration/api/test_meta_api.py +33 -0
  251. package/backend/tests/integration/api/test_nodes_api.py +486 -0
  252. package/backend/tests/integration/api/test_prompts_api.py +47 -0
  253. package/backend/tests/integration/api/test_roles_api.py +227 -0
  254. package/backend/tests/integration/api/test_tabs_api.py +501 -0
  255. package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
  256. package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  257. package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  258. package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
  259. package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
  260. package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
  261. package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
  262. package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  263. package/backend/tests/unit/agent/test_agent_public_api.py +746 -0
  264. package/backend/tests/unit/agent/test_agent_runtime.py +2726 -0
  265. package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
  266. package/backend/tests/unit/channels/test_telegram_channel.py +552 -0
  267. package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  268. package/backend/tests/unit/logging/test_logging.py +132 -0
  269. package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
  270. package/backend/tests/unit/prompts/test_prompts.py +569 -0
  271. package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  272. package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
  273. package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
  274. package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  275. package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
  276. package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
  277. package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
  278. package/backend/tests/unit/providers/test_anthropic_provider.py +185 -0
  279. package/backend/tests/unit/providers/test_errors.py +68 -0
  280. package/backend/tests/unit/providers/test_extract_delta_parts.py +22 -0
  281. package/backend/tests/unit/providers/test_openai_provider.py +139 -0
  282. package/backend/tests/unit/providers/test_openai_responses.py +402 -0
  283. package/backend/tests/unit/providers/test_provider_gateway.py +359 -0
  284. package/backend/tests/unit/providers/test_think_tag_parser.py +36 -0
  285. package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  286. package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
  287. package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  288. package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  289. package/backend/tests/unit/routes/__pycache__/test_stats_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  290. package/backend/tests/unit/routes/test_prompts_routes.py +104 -0
  291. package/backend/tests/unit/routes/test_providers_route.py +368 -0
  292. package/backend/tests/unit/routes/test_roles_routes.py +426 -0
  293. package/backend/tests/unit/routes/test_settings_routes.py +1138 -0
  294. package/backend/tests/unit/routes/test_stats_routes.py +149 -0
  295. package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  296. package/backend/tests/unit/runtime/test_bootstrap_runtime.py +1012 -0
  297. package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  298. package/backend/tests/unit/sandbox/test_sandbox_tools.py +78 -0
  299. package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
  300. package/backend/tests/unit/security/test_security.py +110 -0
  301. package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
  302. package/backend/tests/unit/settings/test_settings_roles.py +711 -0
  303. package/backend/tests/unit/test_access.py +45 -0
  304. package/backend/tests/unit/test_cli.py +124 -0
  305. package/backend/tests/unit/test_graph_runtime.py +72 -0
  306. package/backend/tests/unit/test_network.py +51 -0
  307. package/backend/tests/unit/test_state_sqlite_storage.py +93 -0
  308. package/backend/tests/unit/test_workspace_store.py +231 -0
  309. package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  310. package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  311. package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  312. package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  313. package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  314. package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  315. package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  316. package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  317. package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  318. package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  319. package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  320. package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  321. package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  322. package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
  323. package/backend/tests/unit/tools/test_connect_tool.py +229 -0
  324. package/backend/tests/unit/tools/test_create_agent_tool.py +524 -0
  325. package/backend/tests/unit/tools/test_delete_tab_tool.py +83 -0
  326. package/backend/tests/unit/tools/test_edit_tool.py +115 -0
  327. package/backend/tests/unit/tools/test_exec_tool.py +81 -0
  328. package/backend/tests/unit/tools/test_fetch_tool.py +65 -0
  329. package/backend/tests/unit/tools/test_manage_prompts_tool.py +117 -0
  330. package/backend/tests/unit/tools/test_manage_providers_tool.py +458 -0
  331. package/backend/tests/unit/tools/test_manage_roles_tool.py +411 -0
  332. package/backend/tests/unit/tools/test_manage_settings_tool.py +608 -0
  333. package/backend/tests/unit/tools/test_read_tool.py +33 -0
  334. package/backend/tests/unit/tools/test_set_permissions_tool.py +391 -0
  335. package/backend/tests/unit/tools/test_todo_tool.py +37 -0
  336. package/backend/tests/unit/tools/test_tool_registry.py +91 -0
  337. package/backend/uv.lock +1144 -0
  338. package/bin/flowent.mjs +62 -36
  339. package/dist/frontend/assets/AssistantPage-B3Xc08AS.js +1 -0
  340. package/dist/frontend/assets/ChannelsPage-ByLd28xk.js +1 -0
  341. package/dist/frontend/assets/HomePage-C0hAx9_l.js +3 -0
  342. package/dist/frontend/assets/McpPage-DkrYLvBv.js +7 -0
  343. package/dist/frontend/assets/PageScaffold-D4jO9ooX.js +1 -0
  344. package/dist/frontend/assets/PromptsPage-DWA7rRJd.js +1 -0
  345. package/dist/frontend/assets/ProvidersPage-PUWT8seJ.js +3 -0
  346. package/dist/frontend/assets/RolesPage-CqcclGRw.js +1 -0
  347. package/dist/frontend/assets/SettingsPage-8tS2cJgX.js +3 -0
  348. package/dist/frontend/assets/StatsPage-BX9khYzu.js +1 -0
  349. package/dist/frontend/assets/ToolsPage-9Tl9FdeD.js +1 -0
  350. package/dist/frontend/assets/WorkspaceCommandDialog-CCXxjDL8.js +1 -0
  351. package/dist/frontend/assets/WorkspacePanels-aMdJ7ZH7.js +1 -0
  352. package/dist/frontend/assets/alert-dialog-kFYVQ7oX.js +1 -0
  353. package/dist/frontend/assets/badge-74-3jsCg.js +1 -0
  354. package/dist/frontend/assets/constants-XUzFf6i1.js +1 -0
  355. package/dist/frontend/assets/datetime-m6_O_Ci9.js +1 -0
  356. package/dist/frontend/assets/dialog-BeGSweF6.js +1 -0
  357. package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  358. package/dist/frontend/assets/graph-vendor-CHpVij2M.css +1 -0
  359. package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +7 -0
  360. package/dist/frontend/assets/index-BHC1Vhy8.css +1 -0
  361. package/dist/frontend/assets/index-CL1ALZ3r.js +10 -0
  362. package/dist/frontend/assets/layout.worker-jMHqAFbP.js +24 -0
  363. package/dist/frontend/assets/markdown-vendor-DVdy_w12.js +29 -0
  364. package/dist/frontend/assets/modelParams-CaHd0903.js +1 -0
  365. package/dist/frontend/assets/react-vendor-mEs_JJxa.js +9 -0
  366. package/dist/frontend/assets/roles-2OLDeTc5.js +1 -0
  367. package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  368. package/dist/frontend/assets/select-DL_LPeDj.js +1 -0
  369. package/dist/frontend/assets/shared-CMxbpLeQ.js +1 -0
  370. package/dist/frontend/assets/triState-DEr3NkXV.js +1 -0
  371. package/dist/frontend/assets/ui-vendor-Dg9NNnWX.js +51 -0
  372. package/dist/frontend/index.html +36 -0
  373. package/package.json +27 -41
  374. package/dist/.next/BUILD_ID +0 -1
  375. package/dist/.next/app-path-routes-manifest.json +0 -6
  376. package/dist/.next/build-manifest.json +0 -20
  377. package/dist/.next/package.json +0 -1
  378. package/dist/.next/prerender-manifest.json +0 -114
  379. package/dist/.next/required-server-files.json +0 -333
  380. package/dist/.next/routes-manifest.json +0 -69
  381. package/dist/.next/server/app/_global-error/page/app-paths-manifest.json +0 -3
  382. package/dist/.next/server/app/_global-error/page/build-manifest.json +0 -16
  383. package/dist/.next/server/app/_global-error/page/next-font-manifest.json +0 -6
  384. package/dist/.next/server/app/_global-error/page/react-loadable-manifest.json +0 -1
  385. package/dist/.next/server/app/_global-error/page/server-reference-manifest.json +0 -4
  386. package/dist/.next/server/app/_global-error/page.js +0 -9
  387. package/dist/.next/server/app/_global-error/page.js.map +0 -5
  388. package/dist/.next/server/app/_global-error/page.js.nft.json +0 -1
  389. package/dist/.next/server/app/_global-error/page_client-reference-manifest.js +0 -3
  390. package/dist/.next/server/app/_global-error.html +0 -1
  391. package/dist/.next/server/app/_global-error.meta +0 -15
  392. package/dist/.next/server/app/_global-error.rsc +0 -14
  393. package/dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +0 -5
  394. package/dist/.next/server/app/_global-error.segments/_full.segment.rsc +0 -14
  395. package/dist/.next/server/app/_global-error.segments/_head.segment.rsc +0 -5
  396. package/dist/.next/server/app/_global-error.segments/_index.segment.rsc +0 -5
  397. package/dist/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
  398. package/dist/.next/server/app/_not-found/page/app-paths-manifest.json +0 -3
  399. package/dist/.next/server/app/_not-found/page/build-manifest.json +0 -16
  400. package/dist/.next/server/app/_not-found/page/next-font-manifest.json +0 -10
  401. package/dist/.next/server/app/_not-found/page/react-loadable-manifest.json +0 -1
  402. package/dist/.next/server/app/_not-found/page/server-reference-manifest.json +0 -4
  403. package/dist/.next/server/app/_not-found/page.js +0 -13
  404. package/dist/.next/server/app/_not-found/page.js.map +0 -5
  405. package/dist/.next/server/app/_not-found/page.js.nft.json +0 -1
  406. package/dist/.next/server/app/_not-found/page_client-reference-manifest.js +0 -3
  407. package/dist/.next/server/app/_not-found.html +0 -1
  408. package/dist/.next/server/app/_not-found.meta +0 -16
  409. package/dist/.next/server/app/_not-found.rsc +0 -16
  410. package/dist/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
  411. package/dist/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
  412. package/dist/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
  413. package/dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
  414. package/dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
  415. package/dist/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
  416. package/dist/.next/server/app/icon.svg/route/app-paths-manifest.json +0 -3
  417. package/dist/.next/server/app/icon.svg/route/build-manifest.json +0 -9
  418. package/dist/.next/server/app/icon.svg/route.js +0 -6
  419. package/dist/.next/server/app/icon.svg/route.js.map +0 -5
  420. package/dist/.next/server/app/icon.svg/route.js.nft.json +0 -1
  421. package/dist/.next/server/app/icon.svg.meta +0 -1
  422. package/dist/.next/server/app/index.html +0 -1
  423. package/dist/.next/server/app/index.meta +0 -14
  424. package/dist/.next/server/app/index.rsc +0 -15
  425. package/dist/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -5
  426. package/dist/.next/server/app/index.segments/_full.segment.rsc +0 -15
  427. package/dist/.next/server/app/index.segments/_head.segment.rsc +0 -6
  428. package/dist/.next/server/app/index.segments/_index.segment.rsc +0 -5
  429. package/dist/.next/server/app/index.segments/_tree.segment.rsc +0 -3
  430. package/dist/.next/server/app/page/app-paths-manifest.json +0 -3
  431. package/dist/.next/server/app/page/build-manifest.json +0 -16
  432. package/dist/.next/server/app/page/next-font-manifest.json +0 -10
  433. package/dist/.next/server/app/page/react-loadable-manifest.json +0 -1
  434. package/dist/.next/server/app/page/server-reference-manifest.json +0 -4
  435. package/dist/.next/server/app/page.js +0 -14
  436. package/dist/.next/server/app/page.js.map +0 -5
  437. package/dist/.next/server/app/page.js.nft.json +0 -1
  438. package/dist/.next/server/app/page_client-reference-manifest.js +0 -3
  439. package/dist/.next/server/app-paths-manifest.json +0 -6
  440. package/dist/.next/server/chunks/[externals]_next_dist_0arv.vj._.js +0 -3
  441. package/dist/.next/server/chunks/[root-of-the-server]__0vcj1q1._.js +0 -13
  442. package/dist/.next/server/chunks/[turbopack]_runtime.js +0 -903
  443. package/dist/.next/server/chunks/_next-internal_server_app_icon_svg_route_actions_0-0ehc~.js +0 -3
  444. package/dist/.next/server/chunks/ssr/05w9_next_dist_0ihu0u9._.js +0 -6
  445. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_12u3mib._.js +0 -3
  446. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_forbidden_04fbe_..js +0 -3
  447. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_global-error_0brpl_..js +0 -3
  448. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_unauthorized_0~2g66g.js +0 -3
  449. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_0~cyr1_.js +0 -4
  450. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_1105emf.js +0 -4
  451. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_11uhyqv.js +0 -4
  452. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0.t9_75._.js +0 -33
  453. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0c0ud_z._.js +0 -3
  454. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0f9_8d4._.js +0 -3
  455. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0l5ko41._.js +0 -19
  456. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0mn6z7i._.js +0 -3
  457. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0npxxst._.js +0 -33
  458. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0qjhaca._.js +0 -3
  459. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0rwgw3s._.js +0 -3
  460. package/dist/.next/server/chunks/ssr/[turbopack]_runtime.js +0 -903
  461. package/dist/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
  462. package/dist/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
  463. package/dist/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
  464. package/dist/.next/server/chunks/ssr/node_modules__pnpm_056~6.6._.js +0 -3
  465. package/dist/.next/server/chunks/ssr/node_modules__pnpm_0~j0k.e._.js +0 -33
  466. package/dist/.next/server/functions-config-manifest.json +0 -4
  467. package/dist/.next/server/middleware-build-manifest.js +0 -20
  468. package/dist/.next/server/middleware-manifest.json +0 -6
  469. package/dist/.next/server/next-font-manifest.js +0 -1
  470. package/dist/.next/server/next-font-manifest.json +0 -13
  471. package/dist/.next/server/pages/404.html +0 -1
  472. package/dist/.next/server/pages/500.html +0 -1
  473. package/dist/.next/server/pages-manifest.json +0 -4
  474. package/dist/.next/server/prefetch-hints.json +0 -1
  475. package/dist/.next/server/server-reference-manifest.js +0 -1
  476. package/dist/.next/server/server-reference-manifest.json +0 -5
  477. package/dist/.next/static/SMWpxFVvkpYFxY7uuFvGB/_buildManifest.js +0 -11
  478. package/dist/.next/static/SMWpxFVvkpYFxY7uuFvGB/_clientMiddlewareManifest.js +0 -1
  479. package/dist/.next/static/SMWpxFVvkpYFxY7uuFvGB/_ssgManifest.js +0 -1
  480. package/dist/.next/static/chunks/01qk2~bgf76vu.js +0 -1
  481. package/dist/.next/static/chunks/03~yq9q893hmn.js +0 -1
  482. package/dist/.next/static/chunks/080queev.r2uy.js +0 -31
  483. package/dist/.next/static/chunks/0v3lyuj75aq50.js +0 -1
  484. package/dist/.next/static/chunks/10b~xdx5c-i7s.js +0 -5
  485. package/dist/.next/static/chunks/14gla2ascffgv.css +0 -2
  486. package/dist/.next/static/chunks/turbopack-0m-970~qvs7sc.js +0 -1
  487. package/dist/.next/static/media/7178b3e590c64307-s.11.cyxs5p-0z~.woff2 +0 -0
  488. package/dist/.next/static/media/8a480f0b521d4e75-s.06d3mdzz5bre_.woff2 +0 -0
  489. package/dist/.next/static/media/caa3a2e1cccd8315-s.p.16t1db8_9y2o~.woff2 +0 -0
  490. package/dist/package.json +0 -88
  491. package/dist/server.js +0 -38
  492. /package/{dist/.next/server/app/icon.svg.body → backend/src/flowent/static/favicon.svg} +0 -0
  493. /package/dist/{.next/static/media/icon.0.r~afrtrocz9.svg → frontend/favicon.svg} +0 -0
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import Protocol
5
+
6
+ from flowent.models import ModelInfo
7
+ from flowent.providers.base_url import resolve_provider_base_url
8
+ from flowent.settings import (
9
+ PROVIDER_MODEL_SOURCE_OPTIONS,
10
+ ProviderConfig,
11
+ ProviderModelCatalogEntry,
12
+ build_model_context_window_tokens,
13
+ build_model_input_image,
14
+ build_model_output_image,
15
+ build_provider_headers,
16
+ build_provider_retry_429_delay_seconds,
17
+ serialize_provider_model_catalog_entry,
18
+ )
19
+ from flowent.settings import (
20
+ serialize_provider as serialize_full_provider,
21
+ )
22
+
23
+
24
+ class ProviderModelCatalogPayload(Protocol):
25
+ model: str
26
+ source: str
27
+ context_window_tokens: int | None
28
+ input_image: bool | None
29
+ output_image: bool | None
30
+
31
+
32
+ def validate_provider_base_url_input(
33
+ provider_type: str,
34
+ base_url: str,
35
+ *,
36
+ required_message: str = "base_url is required",
37
+ ) -> str:
38
+ raw_base_url = base_url.strip()
39
+ if not raw_base_url:
40
+ raise ValueError(required_message)
41
+ resolve_provider_base_url(provider_type, raw_base_url)
42
+ return raw_base_url
43
+
44
+
45
+ def build_provider_model_catalog_entry(
46
+ payload: ProviderModelCatalogPayload,
47
+ *,
48
+ field_name_prefix: str = "models[]",
49
+ ) -> ProviderModelCatalogEntry:
50
+ model = payload.model.strip()
51
+ if not model:
52
+ raise ValueError(f"{field_name_prefix}.model must not be empty")
53
+
54
+ source = payload.source.strip().lower()
55
+ if source not in PROVIDER_MODEL_SOURCE_OPTIONS:
56
+ raise ValueError(
57
+ f"{field_name_prefix}.source must be one of: discovered, manual"
58
+ )
59
+
60
+ return ProviderModelCatalogEntry(
61
+ model=model,
62
+ source=source,
63
+ context_window_tokens=build_model_context_window_tokens(
64
+ payload.context_window_tokens,
65
+ field_name=f"{field_name_prefix}.context_window_tokens",
66
+ ),
67
+ input_image=build_model_input_image(
68
+ payload.input_image,
69
+ field_name=f"{field_name_prefix}.input_image",
70
+ ),
71
+ output_image=build_model_output_image(
72
+ payload.output_image,
73
+ field_name=f"{field_name_prefix}.output_image",
74
+ ),
75
+ )
76
+
77
+
78
+ def coerce_provider_model_catalog(
79
+ payloads: Iterable[ProviderModelCatalogPayload] | None,
80
+ *,
81
+ field_name_prefix: str = "models[]",
82
+ ) -> list[ProviderModelCatalogEntry]:
83
+ entries: list[ProviderModelCatalogEntry] = []
84
+ seen_models: set[str] = set()
85
+ for payload in payloads or ():
86
+ entry = build_provider_model_catalog_entry(
87
+ payload,
88
+ field_name_prefix=field_name_prefix,
89
+ )
90
+ if entry.model in seen_models:
91
+ raise ValueError(f"{field_name_prefix}.model '{entry.model}' is duplicated")
92
+ seen_models.add(entry.model)
93
+ entries.append(entry)
94
+ return entries
95
+
96
+
97
+ def build_provider_config(
98
+ *,
99
+ provider_id: str,
100
+ name: str,
101
+ provider_type: str,
102
+ base_url: str,
103
+ api_key: str = "",
104
+ raw_headers: object = None,
105
+ raw_retry_429_delay_seconds: object = 0,
106
+ models: list[ProviderModelCatalogEntry] | None = None,
107
+ base_url_required_message: str = "base_url is required",
108
+ ) -> ProviderConfig:
109
+ return ProviderConfig(
110
+ id=provider_id,
111
+ name=name,
112
+ type=provider_type,
113
+ base_url=validate_provider_base_url_input(
114
+ provider_type,
115
+ base_url,
116
+ required_message=base_url_required_message,
117
+ ),
118
+ api_key=api_key,
119
+ headers=build_provider_headers(raw_headers),
120
+ retry_429_delay_seconds=build_provider_retry_429_delay_seconds(
121
+ raw_retry_429_delay_seconds
122
+ ),
123
+ models=list(models or []),
124
+ )
125
+
126
+
127
+ def apply_provider_update(
128
+ provider: ProviderConfig,
129
+ *,
130
+ name: str | None = None,
131
+ provider_type: str | None = None,
132
+ base_url: str | None = None,
133
+ api_key: str | None = None,
134
+ raw_headers: object | None = None,
135
+ raw_retry_429_delay_seconds: object | None = None,
136
+ models: list[ProviderModelCatalogEntry] | None = None,
137
+ ) -> ProviderConfig:
138
+ next_type = provider_type if provider_type is not None else provider.type
139
+ next_base_url = base_url.strip() if base_url is not None else provider.base_url
140
+ raw_base_url = validate_provider_base_url_input(next_type, next_base_url)
141
+
142
+ if name is not None:
143
+ provider.name = name
144
+ if provider_type is not None:
145
+ provider.type = provider_type
146
+ if base_url is not None or provider_type is not None:
147
+ provider.base_url = raw_base_url
148
+ if api_key is not None:
149
+ provider.api_key = api_key
150
+ if raw_headers is not None:
151
+ provider.headers = build_provider_headers(raw_headers)
152
+ if raw_retry_429_delay_seconds is not None:
153
+ provider.retry_429_delay_seconds = build_provider_retry_429_delay_seconds(
154
+ raw_retry_429_delay_seconds
155
+ )
156
+ if models is not None:
157
+ provider.models = list(models)
158
+
159
+ return provider
160
+
161
+
162
+ def serialize_provider(
163
+ provider: ProviderConfig,
164
+ *,
165
+ include_api_key: bool = True,
166
+ ) -> dict[str, object]:
167
+ data = serialize_full_provider(provider)
168
+ if not include_api_key:
169
+ data.pop("api_key", None)
170
+ return data
171
+
172
+
173
+ def serialize_discovered_model_catalog_entry(model: ModelInfo) -> dict[str, object]:
174
+ return serialize_provider_model_catalog_entry(
175
+ ProviderModelCatalogEntry(
176
+ model=model.id,
177
+ source="discovered",
178
+ context_window_tokens=model.context_window_tokens,
179
+ input_image=model.capabilities.input_image,
180
+ output_image=model.capabilities.output_image,
181
+ )
182
+ )
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from flowent.image_assets import (
6
+ encode_image_asset_as_base64,
7
+ encode_image_asset_as_data_url,
8
+ )
9
+ from flowent.models import (
10
+ ContentPart,
11
+ TextPart,
12
+ content_parts_to_text,
13
+ deserialize_content_parts,
14
+ has_image_parts,
15
+ )
16
+
17
+
18
+ def parse_message_content_parts(content: Any) -> list[ContentPart]:
19
+ if isinstance(content, str):
20
+ return [TextPart(text=content)] if content else []
21
+ return deserialize_content_parts(content)
22
+
23
+
24
+ def collapse_parts_to_text(content: Any) -> str:
25
+ if isinstance(content, str):
26
+ return content
27
+ return content_parts_to_text(parse_message_content_parts(content))
28
+
29
+
30
+ def has_image_content(content: Any) -> bool:
31
+ return has_image_parts(parse_message_content_parts(content))
32
+
33
+
34
+ def to_openai_chat_content(
35
+ content: Any, *, allow_images: bool
36
+ ) -> str | list[dict[str, Any]]:
37
+ parts = parse_message_content_parts(content)
38
+ if not parts:
39
+ return ""
40
+ if not allow_images or not has_image_parts(parts):
41
+ return content_parts_to_text(parts)
42
+ payload: list[dict[str, Any]] = []
43
+ for part in parts:
44
+ if isinstance(part, TextPart):
45
+ payload.append({"type": "text", "text": part.text})
46
+ continue
47
+ _, data_url = encode_image_asset_as_data_url(part.asset_id)
48
+ payload.append(
49
+ {
50
+ "type": "image_url",
51
+ "image_url": {"url": data_url},
52
+ }
53
+ )
54
+ return payload
55
+
56
+
57
+ def to_openai_responses_content(
58
+ content: Any, *, allow_images: bool
59
+ ) -> str | list[dict[str, Any]]:
60
+ parts = parse_message_content_parts(content)
61
+ if not parts:
62
+ return ""
63
+ if not allow_images or not has_image_parts(parts):
64
+ return content_parts_to_text(parts)
65
+ payload: list[dict[str, Any]] = []
66
+ for part in parts:
67
+ if isinstance(part, TextPart):
68
+ payload.append({"type": "input_text", "text": part.text})
69
+ continue
70
+ _, data_url = encode_image_asset_as_data_url(part.asset_id)
71
+ payload.append({"type": "input_image", "image_url": data_url})
72
+ return payload
73
+
74
+
75
+ def to_anthropic_content(
76
+ content: Any, *, allow_images: bool
77
+ ) -> str | list[dict[str, Any]]:
78
+ parts = parse_message_content_parts(content)
79
+ if not parts:
80
+ return ""
81
+ if not allow_images or not has_image_parts(parts):
82
+ return content_parts_to_text(parts)
83
+ payload: list[dict[str, Any]] = []
84
+ for part in parts:
85
+ if isinstance(part, TextPart):
86
+ payload.append({"type": "text", "text": part.text})
87
+ continue
88
+ asset, encoded = encode_image_asset_as_base64(part.asset_id)
89
+ payload.append(
90
+ {
91
+ "type": "image",
92
+ "source": {
93
+ "type": "base64",
94
+ "media_type": asset.mime_type,
95
+ "data": encoded,
96
+ },
97
+ }
98
+ )
99
+ return payload
100
+
101
+
102
+ def to_gemini_parts(content: Any, *, allow_images: bool) -> list[dict[str, Any]]:
103
+ parts = parse_message_content_parts(content)
104
+ if not parts:
105
+ return [{"text": ""}]
106
+ if not allow_images or not has_image_parts(parts):
107
+ return [{"text": content_parts_to_text(parts)}]
108
+ payload: list[dict[str, Any]] = []
109
+ for part in parts:
110
+ if isinstance(part, TextPart):
111
+ payload.append({"text": part.text})
112
+ continue
113
+ asset, encoded = encode_image_asset_as_base64(part.asset_id)
114
+ payload.append(
115
+ {
116
+ "inlineData": {
117
+ "mimeType": asset.mime_type,
118
+ "data": encoded,
119
+ }
120
+ }
121
+ )
122
+ return payload
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ from flowent.network import truncate_text
8
+
9
+
10
+ class LLMProviderError(RuntimeError):
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ *,
15
+ transient: bool,
16
+ status_code: int | None = None,
17
+ ) -> None:
18
+ super().__init__(message)
19
+ self.transient = transient
20
+ self.status_code = status_code
21
+
22
+
23
+ def is_transient_status_code(status_code: int) -> bool:
24
+ return status_code == 429 or 500 <= status_code < 600
25
+
26
+
27
+ def build_status_error(
28
+ *,
29
+ provider_name: str,
30
+ provider_type: str,
31
+ model: str,
32
+ base_url: str,
33
+ status_code: int,
34
+ body: str,
35
+ ) -> LLMProviderError:
36
+ return LLMProviderError(
37
+ (
38
+ "LLM API error\n"
39
+ f"Provider: {provider_name}\n"
40
+ f"Type: {provider_type}\n"
41
+ f"Model: {model}\n"
42
+ f"Base URL: {base_url}\n"
43
+ f"Status: {status_code}\n"
44
+ f"Detail: {_normalize_status_detail(body)}"
45
+ ),
46
+ transient=is_transient_status_code(status_code),
47
+ status_code=status_code,
48
+ )
49
+
50
+
51
+ def build_network_error(
52
+ *,
53
+ provider_name: str,
54
+ provider_type: str,
55
+ model: str,
56
+ base_url: str,
57
+ error: Exception,
58
+ ) -> LLMProviderError:
59
+ return LLMProviderError(
60
+ (
61
+ "LLM API network error\n"
62
+ f"Provider: {provider_name}\n"
63
+ f"Type: {provider_type}\n"
64
+ f"Model: {model}\n"
65
+ f"Base URL: {base_url}\n"
66
+ f"Error: {_normalize_network_error(error)}"
67
+ ),
68
+ transient=True,
69
+ )
70
+
71
+
72
+ def build_configuration_error(
73
+ *,
74
+ provider_name: str,
75
+ provider_type: str,
76
+ model: str,
77
+ base_url: str,
78
+ detail: str,
79
+ ) -> LLMProviderError:
80
+ return LLMProviderError(
81
+ (
82
+ "LLM configuration error\n"
83
+ f"Provider: {provider_name}\n"
84
+ f"Type: {provider_type}\n"
85
+ f"Model: {model}\n"
86
+ f"Base URL: {base_url}\n"
87
+ f"Detail: {_compact_text(detail)}"
88
+ ),
89
+ transient=False,
90
+ )
91
+
92
+
93
+ def build_access_blocked_error(
94
+ *,
95
+ provider_name: str,
96
+ provider_type: str,
97
+ model: str,
98
+ base_url: str,
99
+ status_code: int | None = None,
100
+ detail: str,
101
+ ) -> LLMProviderError:
102
+ status_line = f"Status: {status_code}\n" if status_code is not None else ""
103
+ return LLMProviderError(
104
+ (
105
+ "LLM API access blocked\n"
106
+ f"Provider: {provider_name}\n"
107
+ f"Type: {provider_type}\n"
108
+ f"Model: {model}\n"
109
+ f"Base URL: {base_url}\n"
110
+ f"{status_line}"
111
+ f"Detail: {_normalize_access_blocked_detail(detail)}"
112
+ ),
113
+ transient=False,
114
+ status_code=status_code,
115
+ )
116
+
117
+
118
+ def _normalize_status_detail(body: str) -> str:
119
+ stripped = body.strip()
120
+ if not stripped:
121
+ return "Provider returned an empty error response"
122
+ if _looks_like_html(stripped):
123
+ return "Provider returned a non-API HTML response"
124
+ parsed = _parse_json_body(stripped)
125
+ if parsed is not None:
126
+ detail = _extract_detail(parsed)
127
+ if detail is not None:
128
+ return detail
129
+ first_line = stripped.splitlines()[0]
130
+ detail = _compact_text(first_line)
131
+ if not detail or detail.lower().startswith("traceback"):
132
+ return "Provider returned an unexpected error response"
133
+ return detail
134
+
135
+
136
+ def _normalize_network_error(error: Exception) -> str:
137
+ detail = str(error).strip()
138
+ detail = re.sub(r"^[A-Za-z_][A-Za-z0-9_]*(?:Error|Exception):\s*", "", detail)
139
+ detail = re.sub(r"^Failed to perform,\s*", "", detail)
140
+ detail = re.sub(r"^curl:\s*\(\d+\)\s*", "", detail, flags=re.IGNORECASE)
141
+ detail = re.sub(r"^curl\s+error:\s*", "", detail, flags=re.IGNORECASE)
142
+ detail = re.sub(r"See https?://\S+ for more details\.?", "", detail)
143
+ detail = _compact_text(detail)
144
+ if not detail:
145
+ return "Request failed before the provider returned a response"
146
+ return detail
147
+
148
+
149
+ def _normalize_access_blocked_detail(detail: str) -> str:
150
+ stripped = detail.strip()
151
+ if not stripped:
152
+ return "Challenge or interstitial response from upstream"
153
+ if _looks_like_html(stripped):
154
+ return "Challenge or interstitial HTML response from upstream"
155
+ normalized = _compact_text(stripped)
156
+ if not normalized:
157
+ return "Challenge or interstitial response from upstream"
158
+ return normalized
159
+
160
+
161
+ def _parse_json_body(body: str) -> Any | None:
162
+ try:
163
+ return json.loads(body)
164
+ except json.JSONDecodeError:
165
+ return None
166
+
167
+
168
+ def _extract_detail(value: Any) -> str | None:
169
+ if isinstance(value, str):
170
+ return _normalize_detail_text(value)
171
+ if isinstance(value, list):
172
+ for item in value:
173
+ detail = _extract_detail(item)
174
+ if detail is not None:
175
+ return detail
176
+ return None
177
+ if not isinstance(value, dict):
178
+ return None
179
+
180
+ nested_error = value.get("error")
181
+ if nested_error is not None:
182
+ detail = _extract_detail(nested_error)
183
+ if detail is not None:
184
+ return detail
185
+
186
+ for key in (
187
+ "message",
188
+ "detail",
189
+ "error_description",
190
+ "title",
191
+ "status",
192
+ "code",
193
+ ):
194
+ detail = _extract_detail(value.get(key))
195
+ if detail is not None:
196
+ return detail
197
+
198
+ for key in ("details", "errors"):
199
+ detail = _extract_detail(value.get(key))
200
+ if detail is not None:
201
+ return detail
202
+
203
+ return None
204
+
205
+
206
+ def _normalize_detail_text(value: str) -> str | None:
207
+ normalized = _compact_text(value)
208
+ if not normalized:
209
+ return None
210
+ if normalized.lower().startswith("traceback"):
211
+ return "Upstream returned an unexpected error response"
212
+ return normalized
213
+
214
+
215
+ def _compact_text(value: str, *, limit: int = 240) -> str:
216
+ return truncate_text(re.sub(r"\s+", " ", value).strip(), limit=limit)
217
+
218
+
219
+ def _looks_like_html(value: str) -> bool:
220
+ lowered = value.lstrip().lower()
221
+ return lowered.startswith("<!doctype html") or (
222
+ lowered.startswith("<") and ">" in lowered
223
+ )
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from loguru import logger
8
+
9
+ from flowent.models import LLMResponse, ModelInfo
10
+ from flowent.providers import LLMProvider
11
+ from flowent.providers.errors import build_configuration_error
12
+ from flowent.settings import merge_model_params
13
+
14
+ MODEL_DIRECTORY_TIMEOUT_SECONDS = 120.0
15
+
16
+ ProviderCacheKey = tuple[
17
+ str,
18
+ str,
19
+ str,
20
+ str,
21
+ tuple[tuple[str, str], ...],
22
+ str,
23
+ str,
24
+ int,
25
+ ]
26
+
27
+
28
+ class ProviderGateway:
29
+ def __init__(self) -> None:
30
+ self._cache: dict[ProviderCacheKey, LLMProvider] = {}
31
+ self._lock = threading.Lock()
32
+
33
+ def invalidate_cache(self) -> None:
34
+ with self._lock:
35
+ self._cache.clear()
36
+
37
+ def chat(
38
+ self,
39
+ messages: list[dict[str, Any]],
40
+ tools: list[dict[str, Any]] | None = None,
41
+ on_chunk: Callable[[str, str], None] | None = None,
42
+ register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
43
+ role_name: str | None = None,
44
+ ) -> LLMResponse:
45
+ from flowent.settings import find_role, get_settings
46
+
47
+ settings = get_settings()
48
+ role_cfg = find_role(settings, role_name) if role_name else None
49
+ provider = self._resolve(settings=settings, role_cfg=role_cfg)
50
+ model_params = merge_model_params(
51
+ settings.model.params,
52
+ role_cfg.model_params if role_cfg is not None else None,
53
+ )
54
+ return provider.chat(
55
+ messages,
56
+ tools,
57
+ on_chunk,
58
+ register_interrupt,
59
+ model_params,
60
+ )
61
+
62
+ def list_models_for(
63
+ self,
64
+ provider_id: str,
65
+ register_interrupt: Callable[[Callable[[], None] | None], None] | None = None,
66
+ ) -> list[ModelInfo]:
67
+ from flowent.providers.registry import create_provider
68
+ from flowent.settings import find_provider, get_settings
69
+
70
+ settings = get_settings()
71
+ cfg = find_provider(settings, provider_id)
72
+ if cfg is None:
73
+ return []
74
+
75
+ provider = create_provider(
76
+ provider_type=cfg.type,
77
+ base_url=cfg.base_url,
78
+ api_key=cfg.api_key,
79
+ headers=cfg.headers,
80
+ model="",
81
+ provider_name=cfg.name,
82
+ request_timeout_seconds=MODEL_DIRECTORY_TIMEOUT_SECONDS,
83
+ )
84
+ return provider.list_models(register_interrupt)
85
+
86
+ def _resolve(
87
+ self,
88
+ *,
89
+ settings=None,
90
+ role_cfg=None,
91
+ role_name: str | None = None,
92
+ ) -> LLMProvider:
93
+ from flowent.providers.registry import create_provider
94
+ from flowent.settings import find_provider, find_role, get_settings
95
+
96
+ if settings is None:
97
+ settings = get_settings()
98
+ provider_id = settings.model.active_provider_id
99
+ model = settings.model.active_model
100
+
101
+ if role_cfg is None and role_name:
102
+ role_cfg = find_role(settings, role_name)
103
+ if (
104
+ role_cfg is not None
105
+ and role_cfg.model is not None
106
+ and role_cfg.model.provider_id
107
+ and role_cfg.model.model
108
+ ):
109
+ provider_id = role_cfg.model.provider_id
110
+ model = role_cfg.model.model
111
+
112
+ if not provider_id:
113
+ raise RuntimeError("No active provider configured")
114
+
115
+ cfg = find_provider(settings, provider_id)
116
+ if cfg is None:
117
+ raise RuntimeError(f"Provider '{provider_id}' not found")
118
+
119
+ if not model.strip():
120
+ raise build_configuration_error(
121
+ provider_name=cfg.name,
122
+ provider_type=cfg.type,
123
+ model=model,
124
+ base_url=cfg.base_url,
125
+ detail="No active model configured",
126
+ )
127
+
128
+ provider_type = cfg.type
129
+ base_url = cfg.base_url
130
+ api_key = cfg.api_key
131
+ provider_name = cfg.name
132
+ request_timeout_seconds = settings.model.timeout_ms / 1000
133
+ cache_key = (
134
+ cfg.id,
135
+ provider_type,
136
+ base_url,
137
+ api_key,
138
+ tuple(sorted(cfg.headers.items())),
139
+ provider_name,
140
+ model,
141
+ settings.model.timeout_ms,
142
+ )
143
+
144
+ with self._lock:
145
+ if cache_key in self._cache:
146
+ return self._cache[cache_key]
147
+
148
+ logger.debug(
149
+ "ProviderGateway resolved: name={}, type={}, model={}",
150
+ provider_name,
151
+ provider_type,
152
+ model,
153
+ )
154
+
155
+ provider = create_provider(
156
+ provider_type=provider_type,
157
+ base_url=base_url,
158
+ api_key=api_key,
159
+ headers=cfg.headers,
160
+ model=model,
161
+ provider_name=provider_name,
162
+ request_timeout_seconds=request_timeout_seconds,
163
+ )
164
+
165
+ self._cache[cache_key] = provider
166
+ return provider
167
+
168
+
169
+ gateway = ProviderGateway()