flowent 0.0.6 → 0.0.10

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 (398) hide show
  1. package/README.md +1 -4
  2. package/backend/README.md +1 -4
  3. package/backend/pyproject.toml +2 -8
  4. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/agent.py +217 -3094
  17. package/backend/src/flowent/cli.py +19 -24
  18. package/backend/src/flowent/context.py +127 -0
  19. package/backend/src/flowent/llm.py +256 -0
  20. package/backend/src/flowent/logging.py +170 -129
  21. package/backend/src/flowent/main.py +321 -70
  22. package/backend/src/flowent/patch.py +182 -0
  23. package/backend/src/flowent/paths.py +11 -0
  24. package/backend/src/flowent/sandbox.py +214 -40
  25. package/backend/src/flowent/static/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  26. package/backend/src/flowent/static/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  27. package/backend/src/flowent/static/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  28. package/backend/src/flowent/static/assets/index-C76K95ty.js +81 -0
  29. package/backend/src/flowent/static/assets/index-iUMNKvlU.css +2 -0
  30. package/backend/src/flowent/static/flowent.png +0 -0
  31. package/backend/src/flowent/static/index.html +5 -25
  32. package/backend/src/flowent/storage.py +302 -0
  33. package/backend/src/flowent/tools.py +364 -0
  34. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  35. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  36. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  37. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  38. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  39. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  40. package/backend/tests/test_agent_tools.py +449 -0
  41. package/backend/tests/test_health.py +12 -0
  42. package/backend/tests/test_llm_providers.py +113 -0
  43. package/backend/tests/test_logging.py +182 -0
  44. package/backend/tests/test_persistence.py +125 -0
  45. package/backend/tests/test_workspace_chat.py +578 -0
  46. package/backend/uv.lock +803 -99
  47. package/dist/frontend/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  48. package/dist/frontend/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  49. package/dist/frontend/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  50. package/dist/frontend/assets/index-C76K95ty.js +81 -0
  51. package/dist/frontend/assets/index-iUMNKvlU.css +2 -0
  52. package/dist/frontend/flowent.png +0 -0
  53. package/dist/frontend/index.html +5 -25
  54. package/package.json +1 -2
  55. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  56. package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
  57. package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
  58. package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
  59. package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
  60. package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
  61. package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
  62. package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
  63. package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
  64. package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
  65. package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
  66. package/backend/src/flowent/__pycache__/observability_service.cpython-313.pyc +0 -0
  67. package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
  68. package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
  69. package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
  70. package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
  71. package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
  72. package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
  73. package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
  74. package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
  75. package/backend/src/flowent/access.py +0 -247
  76. package/backend/src/flowent/assistant_commands.py +0 -115
  77. package/backend/src/flowent/channels/__init__.py +0 -3
  78. package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
  79. package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
  80. package/backend/src/flowent/channels/telegram.py +0 -615
  81. package/backend/src/flowent/config.py +0 -14
  82. package/backend/src/flowent/dev.py +0 -3
  83. package/backend/src/flowent/events.py +0 -157
  84. package/backend/src/flowent/graph_runtime.py +0 -60
  85. package/backend/src/flowent/graph_service.py +0 -2508
  86. package/backend/src/flowent/image_assets.py +0 -356
  87. package/backend/src/flowent/mcp_service.py +0 -1918
  88. package/backend/src/flowent/model_metadata.py +0 -102
  89. package/backend/src/flowent/models/__init__.py +0 -125
  90. package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
  91. package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
  92. package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
  93. package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
  94. package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
  95. package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
  96. package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
  97. package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
  98. package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
  99. package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
  100. package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
  101. package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
  102. package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
  103. package/backend/src/flowent/models/agent.py +0 -34
  104. package/backend/src/flowent/models/base.py +0 -24
  105. package/backend/src/flowent/models/blueprint.py +0 -176
  106. package/backend/src/flowent/models/content.py +0 -164
  107. package/backend/src/flowent/models/delta.py +0 -44
  108. package/backend/src/flowent/models/event.py +0 -51
  109. package/backend/src/flowent/models/graph.py +0 -472
  110. package/backend/src/flowent/models/history.py +0 -272
  111. package/backend/src/flowent/models/llm.py +0 -62
  112. package/backend/src/flowent/models/message.py +0 -33
  113. package/backend/src/flowent/models/tab.py +0 -85
  114. package/backend/src/flowent/models/todo.py +0 -10
  115. package/backend/src/flowent/network.py +0 -146
  116. package/backend/src/flowent/observability_service.py +0 -218
  117. package/backend/src/flowent/prompts/__init__.py +0 -67
  118. package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
  120. package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
  121. package/backend/src/flowent/prompts/common.py +0 -250
  122. package/backend/src/flowent/prompts/steward.py +0 -64
  123. package/backend/src/flowent/providers/__init__.py +0 -23
  124. package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  125. package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
  126. package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
  127. package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
  128. package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
  129. package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
  130. package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
  131. package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
  132. package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
  133. package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
  134. package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
  135. package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
  136. package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
  137. package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
  138. package/backend/src/flowent/providers/anthropic.py +0 -468
  139. package/backend/src/flowent/providers/base_url.py +0 -60
  140. package/backend/src/flowent/providers/configuration.py +0 -189
  141. package/backend/src/flowent/providers/content.py +0 -122
  142. package/backend/src/flowent/providers/errors.py +0 -223
  143. package/backend/src/flowent/providers/gateway.py +0 -169
  144. package/backend/src/flowent/providers/gemini.py +0 -447
  145. package/backend/src/flowent/providers/headers.py +0 -20
  146. package/backend/src/flowent/providers/management.py +0 -96
  147. package/backend/src/flowent/providers/ollama.py +0 -293
  148. package/backend/src/flowent/providers/openai.py +0 -422
  149. package/backend/src/flowent/providers/openai_responses.py +0 -655
  150. package/backend/src/flowent/providers/registry.py +0 -144
  151. package/backend/src/flowent/providers/sse.py +0 -31
  152. package/backend/src/flowent/providers/thinking.py +0 -79
  153. package/backend/src/flowent/registry.py +0 -73
  154. package/backend/src/flowent/role_management.py +0 -267
  155. package/backend/src/flowent/routes/__init__.py +0 -28
  156. package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  157. package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
  158. package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
  159. package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
  160. package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
  161. package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
  162. package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
  163. package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
  164. package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
  165. package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
  166. package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
  167. package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
  168. package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
  169. package/backend/src/flowent/routes/access.py +0 -48
  170. package/backend/src/flowent/routes/assistant.py +0 -155
  171. package/backend/src/flowent/routes/image_assets.py +0 -33
  172. package/backend/src/flowent/routes/mcp.py +0 -125
  173. package/backend/src/flowent/routes/meta.py +0 -28
  174. package/backend/src/flowent/routes/nodes.py +0 -413
  175. package/backend/src/flowent/routes/prompts.py +0 -46
  176. package/backend/src/flowent/routes/providers_route.py +0 -365
  177. package/backend/src/flowent/routes/roles.py +0 -207
  178. package/backend/src/flowent/routes/settings.py +0 -328
  179. package/backend/src/flowent/routes/tabs.py +0 -310
  180. package/backend/src/flowent/routes/ws.py +0 -33
  181. package/backend/src/flowent/runtime.py +0 -165
  182. package/backend/src/flowent/security.py +0 -57
  183. package/backend/src/flowent/settings.py +0 -2518
  184. package/backend/src/flowent/settings_management.py +0 -298
  185. package/backend/src/flowent/state_db.py +0 -120
  186. package/backend/src/flowent/static/assets/AssistantPage-VBohhz4d.js +0 -1
  187. package/backend/src/flowent/static/assets/ChannelsPage-CIydPZA_.js +0 -1
  188. package/backend/src/flowent/static/assets/McpPage-CHPm2TPY.js +0 -7
  189. package/backend/src/flowent/static/assets/PageScaffold-DteOA8V7.js +0 -1
  190. package/backend/src/flowent/static/assets/PromptsPage-CSmJ3sZg.js +0 -1
  191. package/backend/src/flowent/static/assets/ProvidersPage-sl2jeG4e.js +0 -3
  192. package/backend/src/flowent/static/assets/RolesPage-DCe7W6Km.js +0 -1
  193. package/backend/src/flowent/static/assets/SettingsPage-Bix9e63E.js +0 -3
  194. package/backend/src/flowent/static/assets/ToolsPage-favNkj5C.js +0 -1
  195. package/backend/src/flowent/static/assets/WorkspaceCommandDialog-DRS6wiD6.js +0 -1
  196. package/backend/src/flowent/static/assets/WorkspacePage-KuaDjt_D.js +0 -3
  197. package/backend/src/flowent/static/assets/WorkspacePanels-BZxBw8M5.js +0 -1
  198. package/backend/src/flowent/static/assets/alert-dialog-DIBUCmqM.js +0 -1
  199. package/backend/src/flowent/static/assets/datetime-eJqd0V2S.js +0 -1
  200. package/backend/src/flowent/static/assets/dialog-BOvHIBrg.js +0 -1
  201. package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +0 -6312
  202. package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +0 -1
  203. package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +0 -7
  204. package/backend/src/flowent/static/assets/index-Biio-CoI.js +0 -10
  205. package/backend/src/flowent/static/assets/index-CmQvO7sl.css +0 -1
  206. package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +0 -24
  207. package/backend/src/flowent/static/assets/markdown-vendor-C9RtvaJh.js +0 -29
  208. package/backend/src/flowent/static/assets/modelParams-DcEhGnu0.js +0 -1
  209. package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +0 -9
  210. package/backend/src/flowent/static/assets/roles-BbIEIMeG.js +0 -1
  211. package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +0 -1
  212. package/backend/src/flowent/static/assets/select-D9SwnlXF.js +0 -1
  213. package/backend/src/flowent/static/assets/surface-Bzr1FRG4.js +0 -1
  214. package/backend/src/flowent/static/assets/triState-DgLlKdRR.js +0 -1
  215. package/backend/src/flowent/static/assets/ui-vendor-UazN8rcv.js +0 -51
  216. package/backend/src/flowent/static/favicon.svg +0 -4
  217. package/backend/src/flowent/tools/__init__.py +0 -275
  218. package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  219. package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
  220. package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
  221. package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
  222. package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
  223. package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
  224. package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
  225. package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
  226. package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
  227. package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
  228. package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
  229. package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
  230. package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
  231. package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
  232. package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
  233. package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
  234. package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
  235. package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
  236. package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
  237. package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
  238. package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
  239. package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
  240. package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
  241. package/backend/src/flowent/tools/connect.py +0 -100
  242. package/backend/src/flowent/tools/contacts.py +0 -22
  243. package/backend/src/flowent/tools/create_agent.py +0 -191
  244. package/backend/src/flowent/tools/create_tab.py +0 -61
  245. package/backend/src/flowent/tools/delete_tab.py +0 -39
  246. package/backend/src/flowent/tools/edit.py +0 -142
  247. package/backend/src/flowent/tools/exec.py +0 -118
  248. package/backend/src/flowent/tools/fetch.py +0 -85
  249. package/backend/src/flowent/tools/idle.py +0 -27
  250. package/backend/src/flowent/tools/list_roles.py +0 -75
  251. package/backend/src/flowent/tools/list_tabs.py +0 -100
  252. package/backend/src/flowent/tools/list_tools.py +0 -28
  253. package/backend/src/flowent/tools/manage_prompts.py +0 -102
  254. package/backend/src/flowent/tools/manage_providers.py +0 -220
  255. package/backend/src/flowent/tools/manage_roles.py +0 -275
  256. package/backend/src/flowent/tools/manage_settings.py +0 -364
  257. package/backend/src/flowent/tools/mcp.py +0 -199
  258. package/backend/src/flowent/tools/read.py +0 -152
  259. package/backend/src/flowent/tools/send.py +0 -68
  260. package/backend/src/flowent/tools/set_permissions.py +0 -99
  261. package/backend/src/flowent/tools/sleep.py +0 -41
  262. package/backend/src/flowent/tools/todo.py +0 -51
  263. package/backend/src/flowent/workspace_store.py +0 -479
  264. package/backend/tests/__init__.py +0 -0
  265. package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  266. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  267. package/backend/tests/conftest.py +0 -6
  268. package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  269. package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
  270. package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
  271. package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
  272. package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
  273. package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
  274. package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
  275. package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
  276. package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
  277. package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
  278. package/backend/tests/integration/api/conftest.py +0 -29
  279. package/backend/tests/integration/api/test_access_api.py +0 -182
  280. package/backend/tests/integration/api/test_assistant_api.py +0 -354
  281. package/backend/tests/integration/api/test_frontend_mounting.py +0 -61
  282. package/backend/tests/integration/api/test_mcp_api.py +0 -116
  283. package/backend/tests/integration/api/test_meta_api.py +0 -33
  284. package/backend/tests/integration/api/test_nodes_api.py +0 -722
  285. package/backend/tests/integration/api/test_prompts_api.py +0 -47
  286. package/backend/tests/integration/api/test_roles_api.py +0 -228
  287. package/backend/tests/integration/api/test_tabs_api.py +0 -802
  288. package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
  289. package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  290. package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  291. package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
  292. package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
  293. package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
  294. package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
  295. package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  296. package/backend/tests/unit/agent/test_agent_public_api.py +0 -837
  297. package/backend/tests/unit/agent/test_agent_runtime.py +0 -2942
  298. package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
  299. package/backend/tests/unit/channels/test_telegram_channel.py +0 -552
  300. package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  301. package/backend/tests/unit/logging/test_logging.py +0 -132
  302. package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
  303. package/backend/tests/unit/prompts/test_prompts.py +0 -570
  304. package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  305. package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
  306. package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
  307. package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  308. package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
  309. package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
  310. package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
  311. package/backend/tests/unit/providers/test_anthropic_provider.py +0 -185
  312. package/backend/tests/unit/providers/test_errors.py +0 -68
  313. package/backend/tests/unit/providers/test_extract_delta_parts.py +0 -22
  314. package/backend/tests/unit/providers/test_openai_provider.py +0 -139
  315. package/backend/tests/unit/providers/test_openai_responses.py +0 -402
  316. package/backend/tests/unit/providers/test_provider_gateway.py +0 -359
  317. package/backend/tests/unit/providers/test_think_tag_parser.py +0 -36
  318. package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  319. package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
  320. package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  321. package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  322. package/backend/tests/unit/routes/test_prompts_routes.py +0 -104
  323. package/backend/tests/unit/routes/test_providers_route.py +0 -370
  324. package/backend/tests/unit/routes/test_roles_routes.py +0 -535
  325. package/backend/tests/unit/routes/test_settings_routes.py +0 -1142
  326. package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  327. package/backend/tests/unit/runtime/test_bootstrap_runtime.py +0 -1002
  328. package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  329. package/backend/tests/unit/sandbox/test_sandbox_tools.py +0 -78
  330. package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
  331. package/backend/tests/unit/security/test_security.py +0 -124
  332. package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
  333. package/backend/tests/unit/settings/test_settings_roles.py +0 -751
  334. package/backend/tests/unit/test_access.py +0 -45
  335. package/backend/tests/unit/test_cli.py +0 -124
  336. package/backend/tests/unit/test_graph_runtime.py +0 -72
  337. package/backend/tests/unit/test_network.py +0 -51
  338. package/backend/tests/unit/test_state_sqlite_storage.py +0 -159
  339. package/backend/tests/unit/test_workspace_store.py +0 -231
  340. package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  341. package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  342. package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  343. package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  344. package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  345. package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  346. package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  347. package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  348. package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  349. package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  350. package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  351. package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  352. package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  353. package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
  354. package/backend/tests/unit/tools/test_connect_tool.py +0 -228
  355. package/backend/tests/unit/tools/test_create_agent_tool.py +0 -436
  356. package/backend/tests/unit/tools/test_delete_tab_tool.py +0 -116
  357. package/backend/tests/unit/tools/test_edit_tool.py +0 -115
  358. package/backend/tests/unit/tools/test_exec_tool.py +0 -81
  359. package/backend/tests/unit/tools/test_fetch_tool.py +0 -65
  360. package/backend/tests/unit/tools/test_manage_prompts_tool.py +0 -117
  361. package/backend/tests/unit/tools/test_manage_providers_tool.py +0 -460
  362. package/backend/tests/unit/tools/test_manage_roles_tool.py +0 -411
  363. package/backend/tests/unit/tools/test_manage_settings_tool.py +0 -611
  364. package/backend/tests/unit/tools/test_read_tool.py +0 -33
  365. package/backend/tests/unit/tools/test_set_permissions_tool.py +0 -595
  366. package/backend/tests/unit/tools/test_todo_tool.py +0 -37
  367. package/backend/tests/unit/tools/test_tool_registry.py +0 -194
  368. package/dist/frontend/assets/AssistantPage-VBohhz4d.js +0 -1
  369. package/dist/frontend/assets/ChannelsPage-CIydPZA_.js +0 -1
  370. package/dist/frontend/assets/McpPage-CHPm2TPY.js +0 -7
  371. package/dist/frontend/assets/PageScaffold-DteOA8V7.js +0 -1
  372. package/dist/frontend/assets/PromptsPage-CSmJ3sZg.js +0 -1
  373. package/dist/frontend/assets/ProvidersPage-sl2jeG4e.js +0 -3
  374. package/dist/frontend/assets/RolesPage-DCe7W6Km.js +0 -1
  375. package/dist/frontend/assets/SettingsPage-Bix9e63E.js +0 -3
  376. package/dist/frontend/assets/ToolsPage-favNkj5C.js +0 -1
  377. package/dist/frontend/assets/WorkspaceCommandDialog-DRS6wiD6.js +0 -1
  378. package/dist/frontend/assets/WorkspacePage-KuaDjt_D.js +0 -3
  379. package/dist/frontend/assets/WorkspacePanels-BZxBw8M5.js +0 -1
  380. package/dist/frontend/assets/alert-dialog-DIBUCmqM.js +0 -1
  381. package/dist/frontend/assets/datetime-eJqd0V2S.js +0 -1
  382. package/dist/frontend/assets/dialog-BOvHIBrg.js +0 -1
  383. package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +0 -6312
  384. package/dist/frontend/assets/graph-vendor-CHpVij2M.css +0 -1
  385. package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +0 -7
  386. package/dist/frontend/assets/index-Biio-CoI.js +0 -10
  387. package/dist/frontend/assets/index-CmQvO7sl.css +0 -1
  388. package/dist/frontend/assets/layout.worker-jMHqAFbP.js +0 -24
  389. package/dist/frontend/assets/markdown-vendor-C9RtvaJh.js +0 -29
  390. package/dist/frontend/assets/modelParams-DcEhGnu0.js +0 -1
  391. package/dist/frontend/assets/react-vendor-mEs_JJxa.js +0 -9
  392. package/dist/frontend/assets/roles-BbIEIMeG.js +0 -1
  393. package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +0 -1
  394. package/dist/frontend/assets/select-D9SwnlXF.js +0 -1
  395. package/dist/frontend/assets/surface-Bzr1FRG4.js +0 -1
  396. package/dist/frontend/assets/triState-DgLlKdRR.js +0 -1
  397. package/dist/frontend/assets/ui-vendor-UazN8rcv.js +0 -51
  398. package/dist/frontend/favicon.svg +0 -4
@@ -1,25 +1,76 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
1
+ import json
2
+ import logging
4
3
  import os
5
- from contextlib import asynccontextmanager
4
+ from collections.abc import AsyncIterator
6
5
  from pathlib import Path
6
+ from typing import Literal
7
+ from uuid import uuid4
7
8
 
8
- from fastapi import FastAPI
9
- from fastapi.responses import FileResponse
9
+ from fastapi import FastAPI, HTTPException
10
+ from fastapi.responses import FileResponse, StreamingResponse
10
11
  from fastapi.staticfiles import StaticFiles
11
- from starlette.middleware.sessions import SessionMiddleware
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+ from flowent.agent import run_agent_stream
15
+ from flowent.context import runtime_context_messages
16
+ from flowent.llm import (
17
+ ChatMessage,
18
+ CompletionCallable,
19
+ ProviderConnection,
20
+ ProviderFormat,
21
+ complete_chat,
22
+ list_provider_models,
23
+ )
24
+ from flowent.logging import TRACE_LEVEL, ensure_logging_configured
25
+ from flowent.storage import (
26
+ StateStore,
27
+ StoredMessage,
28
+ StoredProvider,
29
+ StoredSettings,
30
+ StoredState,
31
+ StoredToolItem,
32
+ )
12
33
 
13
- from flowent.access import AccessControlMiddleware
14
- from flowent.config import Config
15
- from flowent.events import event_bus
16
- from flowent.logging import setup_logging
17
- from flowent.runtime import bootstrap_runtime, shutdown_runtime
34
+ logger = logging.getLogger("flowent.main")
18
35
 
19
- config = Config()
20
- setup_logging(config)
21
36
 
22
37
  DEFAULT_STATIC_DIR = Path(__file__).parent / "static"
38
+ COMPACTED_CONTEXT_MARKER = "Context compacted"
39
+ COMPACT_SYSTEM_PROMPT = "You are compacting Flowent workspace context."
40
+
41
+
42
+ class ProviderModelsRequest(BaseModel):
43
+ model_config = ConfigDict(extra="forbid")
44
+
45
+ provider: ProviderFormat
46
+ secret_reference: str
47
+ base_url: str | None = None
48
+
49
+
50
+ class ProviderModelsResponse(BaseModel):
51
+ models: list[str]
52
+
53
+
54
+ class WorkspaceMessagesRequest(BaseModel):
55
+ model_config = ConfigDict(extra="forbid")
56
+
57
+ messages: list[StoredMessage]
58
+
59
+
60
+ class WorkspaceRespondRequest(BaseModel):
61
+ model_config = ConfigDict(extra="forbid")
62
+
63
+ content: str
64
+
65
+
66
+ class WorkspaceCompactResponse(BaseModel):
67
+ model_config = ConfigDict(extra="forbid")
68
+
69
+ message: StoredMessage
70
+
71
+
72
+ def stream_event(event: str, data: dict[str, object]) -> str:
73
+ return f"event: {event}\ndata: {json.dumps(data)}\n\n"
23
74
 
24
75
 
25
76
  def frontend_static_directory() -> Path:
@@ -32,78 +83,278 @@ def frontend_static_directory() -> Path:
32
83
  return DEFAULT_STATIC_DIR
33
84
 
34
85
 
35
- @asynccontextmanager
36
- async def lifespan(app: FastAPI):
37
- from flowent.access import (
38
- initialize_live_access_signature,
39
- refresh_live_access_signature,
86
+ def selected_connection(state: StoredState) -> ProviderConnection:
87
+ provider = next(
88
+ (
89
+ stored_provider
90
+ for stored_provider in state.providers
91
+ if stored_provider.id == state.settings.selected_provider_id
92
+ ),
93
+ None,
40
94
  )
95
+ if provider is None or not state.settings.selected_model:
96
+ logger.warning("Workspace request blocked because provider or model is missing")
97
+ raise HTTPException(
98
+ status_code=400,
99
+ detail="Choose a provider and model before sending.",
100
+ )
101
+ if not provider.api_key:
102
+ logger.warning("Workspace request blocked because selected provider has no key")
103
+ raise HTTPException(status_code=400, detail="Add a key before sending.")
41
104
 
42
- loop = asyncio.get_running_loop()
43
- event_bus.set_loop(loop)
44
-
45
- bootstrap_runtime()
46
- initialize_live_access_signature()
105
+ logger.debug(
106
+ "Workspace request using provider=%s model=%s",
107
+ provider.name,
108
+ state.settings.selected_model,
109
+ )
110
+ return ProviderConnection(
111
+ base_url=provider.base_url or None,
112
+ model=state.settings.selected_model,
113
+ name=provider.name,
114
+ provider=provider.type,
115
+ secret_reference=provider.api_key,
116
+ )
47
117
 
48
- stop_event = asyncio.Event()
49
118
 
50
- async def watch_access_state() -> None:
51
- while True:
52
- try:
53
- await asyncio.wait_for(stop_event.wait(), timeout=1.0)
54
- return
55
- except TimeoutError:
56
- if refresh_live_access_signature():
57
- await event_bus.close_all(
58
- code=4001, reason="Access session updated"
59
- )
119
+ def latest_compacted_context_index(messages: list[StoredMessage]) -> int | None:
120
+ for index in range(len(messages) - 1, -1, -1):
121
+ message = messages[index]
122
+ if message.author == "system" and message.content == COMPACTED_CONTEXT_MARKER:
123
+ return index
124
+ return None
60
125
 
61
- access_watch_task = asyncio.create_task(watch_access_state())
62
126
 
63
- yield
127
+ def workspace_chat_messages(
128
+ messages: list[StoredMessage],
129
+ compacted_context: str = "",
130
+ ) -> list[ChatMessage]:
131
+ chat_messages: list[ChatMessage] = []
132
+ marker_index = latest_compacted_context_index(messages)
133
+ visible_messages = messages
64
134
 
65
- stop_event.set()
66
- await access_watch_task
67
- shutdown_runtime()
135
+ if compacted_context and marker_index is not None:
136
+ chat_messages.extend(
137
+ [
138
+ ChatMessage(role="user", content=COMPACTED_CONTEXT_MARKER),
139
+ ChatMessage(role="assistant", content=compacted_context),
140
+ ]
141
+ )
142
+ visible_messages = messages[marker_index + 1 :]
68
143
 
144
+ for message in visible_messages:
145
+ if message.author == "system" and message.content == COMPACTED_CONTEXT_MARKER:
146
+ continue
147
+ if message.author not in ("user", "assistant"):
148
+ raise HTTPException(status_code=400, detail="Message history is invalid.")
149
+ role: Literal["user", "assistant"] = (
150
+ "user" if message.author == "user" else "assistant"
151
+ )
152
+ chat_messages.append(ChatMessage(role=role, content=message.content))
153
+ return chat_messages
69
154
 
70
- def create_app(*, serve_frontend: bool = True) -> FastAPI:
71
- from flowent.access import ensure_session_signing_secret
72
- from flowent.settings import get_settings, save_settings
73
155
 
74
- settings = get_settings()
75
- if ensure_session_signing_secret(settings):
76
- save_settings(settings)
77
- session_secret = (
78
- config.SESSION_SECRET.strip() or settings.access.session_signing_secret
156
+ def compact_prompt_messages(
157
+ messages: list[StoredMessage],
158
+ compacted_context: str,
159
+ runtime_messages: list[ChatMessage] | None = None,
160
+ ) -> list[ChatMessage]:
161
+ history_messages = [
162
+ *(runtime_messages or []),
163
+ *workspace_chat_messages(messages, compacted_context),
164
+ ]
165
+ history = "\n\n".join(
166
+ f"{message.role}: {message.content}" for message in history_messages
79
167
  )
168
+ return [
169
+ ChatMessage(role="system", content=COMPACT_SYSTEM_PROMPT),
170
+ ChatMessage(
171
+ role="user",
172
+ content=(
173
+ "Compact the current Flowent workspace context for the next turn.\n\n"
174
+ "Keep the details needed to continue accurately, including decisions, "
175
+ "constraints, pending work, and referenced facts.\n\n"
176
+ f"Conversation:\n{history}"
177
+ ),
178
+ ),
179
+ ]
80
180
 
81
- app = FastAPI(
82
- title=config.APP_NAME,
83
- debug=config.DEBUG,
84
- lifespan=lifespan,
85
- )
86
- app.add_middleware(AccessControlMiddleware)
87
- app.add_middleware(
88
- SessionMiddleware,
89
- secret_key=session_secret,
90
- session_cookie=config.SESSION_COOKIE_NAME,
91
- max_age=config.SESSION_MAX_AGE_SECONDS,
92
- same_site="lax",
93
- https_only=False,
94
- )
95
181
 
96
- from flowent.routes import router
182
+ def create_app(
183
+ *,
184
+ serve_frontend: bool = True,
185
+ chat_completion: CompletionCallable | None = None,
186
+ ) -> FastAPI:
187
+ ensure_logging_configured()
188
+
189
+ app = FastAPI(title="Flowent")
190
+ store = StateStore()
97
191
 
98
- app.include_router(router)
99
192
  static_dir = frontend_static_directory().resolve(strict=False)
193
+ logger.debug("Flowent app created serve_frontend=%s", serve_frontend)
194
+ logger.info("Static directory: %s", static_dir)
100
195
 
101
- if serve_frontend and static_dir.is_dir():
102
- app.mount(
103
- "/assets",
104
- StaticFiles(directory=static_dir / "assets"),
105
- name="assets",
196
+ @app.get("/api/health")
197
+ async def health() -> dict[str, str]:
198
+ return {"status": "ok"}
199
+
200
+ @app.get("/api/state")
201
+ async def app_state() -> StoredState:
202
+ return store.read_state()
203
+
204
+ @app.post("/api/providers")
205
+ async def save_provider(provider: StoredProvider) -> StoredProvider:
206
+ return store.save_provider(provider)
207
+
208
+ @app.post("/api/providers/models")
209
+ async def provider_models(request: ProviderModelsRequest) -> ProviderModelsResponse:
210
+ return ProviderModelsResponse(
211
+ models=list_provider_models(
212
+ base_url=request.base_url,
213
+ provider=request.provider,
214
+ secret_reference=request.secret_reference,
215
+ ),
216
+ )
217
+
218
+ @app.put("/api/settings")
219
+ async def save_settings(settings: StoredSettings) -> StoredSettings:
220
+ return store.save_settings(settings)
221
+
222
+ @app.put("/api/workspace/messages")
223
+ async def save_workspace_messages(
224
+ request: WorkspaceMessagesRequest,
225
+ ) -> WorkspaceMessagesRequest:
226
+ return WorkspaceMessagesRequest(messages=store.save_messages(request.messages))
227
+
228
+ @app.post("/api/workspace/compact")
229
+ async def compact_workspace() -> WorkspaceCompactResponse:
230
+ logger.info("Workspace compact requested")
231
+ state = store.read_state()
232
+ connection = selected_connection(state)
233
+ compacted_context = store.read_compacted_context()
234
+ cwd = Path.cwd()
235
+
236
+ try:
237
+ summary = await complete_chat(
238
+ connection,
239
+ compact_prompt_messages(
240
+ state.messages,
241
+ compacted_context,
242
+ runtime_context_messages(cwd),
243
+ ),
244
+ completion=chat_completion,
245
+ )
246
+ except HTTPException:
247
+ raise
248
+ except Exception as error:
249
+ logger.exception("Workspace compact failed")
250
+ raise HTTPException(
251
+ status_code=500,
252
+ detail="Context could not be compacted.",
253
+ ) from error
254
+
255
+ marker = StoredMessage(
256
+ author="system",
257
+ content=COMPACTED_CONTEXT_MARKER,
258
+ id=str(uuid4()),
259
+ )
260
+ store.save_compacted_context(summary.content)
261
+ store.save_messages([*state.messages, marker])
262
+ logger.info(
263
+ "Workspace compact completed summary_length=%s", len(summary.content)
106
264
  )
265
+ logger.log(TRACE_LEVEL, "Workspace compact summary=%r", summary.content)
266
+ return WorkspaceCompactResponse(message=marker)
267
+
268
+ @app.post("/api/workspace/respond")
269
+ async def respond_to_workspace(
270
+ request: WorkspaceRespondRequest,
271
+ ) -> StreamingResponse:
272
+ logger.info(
273
+ "Workspace response requested content_length=%s", len(request.content)
274
+ )
275
+ logger.log(TRACE_LEVEL, "Workspace user content=%r", request.content)
276
+ state = store.read_state()
277
+ connection = selected_connection(state)
278
+ cwd = Path.cwd()
279
+
280
+ user_message = StoredMessage(
281
+ author="user",
282
+ content=request.content,
283
+ id=str(uuid4()),
284
+ )
285
+ next_messages = [*state.messages, user_message]
286
+ store.save_messages(next_messages)
287
+ chat_messages = workspace_chat_messages(
288
+ next_messages,
289
+ store.read_compacted_context(),
290
+ )
291
+ request_messages = [
292
+ message.model_dump()
293
+ for message in [*runtime_context_messages(cwd), *chat_messages]
294
+ ]
295
+
296
+ async def response_stream() -> AsyncIterator[str]:
297
+ assistant_tools: dict[str, StoredToolItem] = {}
298
+ try:
299
+ async for event in run_agent_stream(
300
+ completion=chat_completion,
301
+ connection=connection,
302
+ cwd=cwd,
303
+ messages=request_messages,
304
+ ):
305
+ if event.event == "tool_start":
306
+ tool = event.data.get("tool")
307
+ if isinstance(tool, dict) and isinstance(tool.get("id"), str):
308
+ assistant_tools[tool["id"]] = StoredToolItem.model_validate(
309
+ tool
310
+ )
311
+ if event.event in {"tool_done", "tool_error"}:
312
+ tool_id = event.data.get("id")
313
+ if isinstance(tool_id, str) and tool_id in assistant_tools:
314
+ assistant_tools[tool_id] = StoredToolItem.model_validate(
315
+ {
316
+ **assistant_tools[tool_id].model_dump(
317
+ exclude_none=True
318
+ ),
319
+ **event.data,
320
+ }
321
+ )
322
+ logger.log(
323
+ TRACE_LEVEL,
324
+ "Workspace stream event=%s data=%r",
325
+ event.event,
326
+ event.data,
327
+ )
328
+ if event.event == "done":
329
+ message = event.data.get("message")
330
+ if isinstance(message, dict):
331
+ next_messages.append(
332
+ StoredMessage(
333
+ author="assistant",
334
+ content=str(message.get("content") or ""),
335
+ id=str(message.get("id") or uuid4()),
336
+ tools=list(assistant_tools.values()),
337
+ )
338
+ )
339
+ store.save_messages(next_messages)
340
+ yield stream_event(event.event, event.data)
341
+ except Exception as error:
342
+ logger.exception("Workspace response failed")
343
+ yield stream_event(
344
+ "error",
345
+ {"message": str(error) or "Message could not be sent."},
346
+ )
347
+ return
348
+
349
+ return StreamingResponse(
350
+ response_stream(),
351
+ media_type="text/event-stream",
352
+ )
353
+
354
+ if serve_frontend and static_dir.is_dir():
355
+ assets_dir = static_dir / "assets"
356
+ if assets_dir.is_dir():
357
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
107
358
 
108
359
  @app.get("/{path:path}")
109
360
  async def spa_fallback(path: str) -> FileResponse:
@@ -121,4 +372,4 @@ app = create_app()
121
372
  if __name__ == "__main__":
122
373
  import uvicorn
123
374
 
124
- uvicorn.run(app, reload=config.DEBUG)
375
+ uvicorn.run(app)
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+
8
+ class PatchError(RuntimeError):
9
+ pass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class PatchChange:
14
+ path: Path
15
+ kind: str
16
+ move_path: Path | None = None
17
+
18
+
19
+ def affected_paths(patch: str, cwd: Path) -> list[Path]:
20
+ paths: list[Path] = []
21
+ for line in patch.splitlines():
22
+ if line.startswith(
23
+ ("*** Add File: ", "*** Delete File: ", "*** Update File: ")
24
+ ):
25
+ paths.append((cwd / line.split(": ", 1)[1]).resolve(strict=False))
26
+ elif line.startswith("*** Move to: "):
27
+ paths.append(
28
+ (cwd / line.removeprefix("*** Move to: ")).resolve(strict=False)
29
+ )
30
+ return paths
31
+
32
+
33
+ def parse_patch(patch: str, cwd: Path) -> list[dict[str, object]]:
34
+ lines = patch.splitlines()
35
+ if not lines or lines[0].strip() != "*** Begin Patch":
36
+ raise PatchError("Patch must start with Begin Patch.")
37
+ if lines[-1].strip() != "*** End Patch":
38
+ raise PatchError("Patch must end with End Patch.")
39
+
40
+ operations: list[dict[str, object]] = []
41
+ index = 1
42
+ while index < len(lines) - 1:
43
+ line = lines[index]
44
+ if line.startswith("*** Add File: "):
45
+ target = (cwd / line.removeprefix("*** Add File: ")).resolve(strict=False)
46
+ index += 1
47
+ contents: list[str] = []
48
+ while index < len(lines) - 1 and not lines[index].startswith("*** "):
49
+ content_line = lines[index]
50
+ if not content_line.startswith("+"):
51
+ raise PatchError("Add file lines must start with +.")
52
+ contents.append(content_line[1:])
53
+ index += 1
54
+ operations.append(
55
+ {"kind": "add", "path": target, "content": "\n".join(contents) + "\n"}
56
+ )
57
+ continue
58
+ if line.startswith("*** Delete File: "):
59
+ target = (cwd / line.removeprefix("*** Delete File: ")).resolve(
60
+ strict=False
61
+ )
62
+ operations.append({"kind": "delete", "path": target})
63
+ index += 1
64
+ continue
65
+ if line.startswith("*** Update File: "):
66
+ target = (cwd / line.removeprefix("*** Update File: ")).resolve(
67
+ strict=False
68
+ )
69
+ index += 1
70
+ move_path = None
71
+ if index < len(lines) - 1 and lines[index].startswith("*** Move to: "):
72
+ move_path = (cwd / lines[index].removeprefix("*** Move to: ")).resolve(
73
+ strict=False
74
+ )
75
+ index += 1
76
+ chunks: list[dict[str, list[str]]] = []
77
+ current: dict[str, list[str]] | None = None
78
+ while index < len(lines) - 1 and not lines[index].startswith("*** "):
79
+ content_line = lines[index]
80
+ if content_line.startswith("@@"):
81
+ current = {"remove": [], "add": []}
82
+ chunks.append(current)
83
+ elif content_line.startswith("-"):
84
+ if current is None:
85
+ current = {"remove": [], "add": []}
86
+ chunks.append(current)
87
+ current["remove"].append(content_line[1:])
88
+ elif content_line.startswith("+"):
89
+ if current is None:
90
+ current = {"remove": [], "add": []}
91
+ chunks.append(current)
92
+ current["add"].append(content_line[1:])
93
+ elif content_line.startswith(" ") or content_line == "*** End of File":
94
+ pass
95
+ else:
96
+ raise PatchError(
97
+ "Update lines must start with context, +, -, or @@."
98
+ )
99
+ index += 1
100
+ operations.append(
101
+ {
102
+ "kind": "update",
103
+ "path": target,
104
+ "move_path": move_path,
105
+ "chunks": chunks,
106
+ }
107
+ )
108
+ continue
109
+ if not line.strip():
110
+ index += 1
111
+ continue
112
+ raise PatchError(f"Invalid patch line: {line}")
113
+ return operations
114
+
115
+
116
+ def apply_update(original: str, chunks: list[dict[str, list[str]]]) -> str:
117
+ content = original
118
+ for chunk in chunks:
119
+ remove = "\n".join(chunk["remove"])
120
+ add = "\n".join(chunk["add"])
121
+ if remove:
122
+ candidates = [remove, remove + "\n"]
123
+ for candidate in candidates:
124
+ if candidate in content:
125
+ replacement = add + (
126
+ "\n" if candidate.endswith("\n") and add else ""
127
+ )
128
+ content = content.replace(candidate, replacement, 1)
129
+ break
130
+ else:
131
+ raise PatchError("Patch context was not found.")
132
+ elif add:
133
+ content = (
134
+ content
135
+ + ("" if content.endswith("\n") or not content else "\n")
136
+ + add
137
+ + "\n"
138
+ )
139
+ return content
140
+
141
+
142
+ def apply_patch(patch: str, cwd: Path) -> dict[str, object]:
143
+ operations = parse_patch(patch, cwd)
144
+ changed: list[dict[str, str]] = []
145
+ for operation in operations:
146
+ kind = str(operation["kind"])
147
+ path = operation["path"]
148
+ if not isinstance(path, Path):
149
+ raise PatchError("Patch path is invalid.")
150
+ if kind == "add":
151
+ path.parent.mkdir(parents=True, exist_ok=True)
152
+ path.write_text(str(operation["content"]))
153
+ changed.append({"path": str(path), "status": "added"})
154
+ elif kind == "delete":
155
+ path.unlink()
156
+ changed.append({"path": str(path), "status": "deleted"})
157
+ elif kind == "update":
158
+ original = path.read_text()
159
+ chunks = operation["chunks"]
160
+ if not isinstance(chunks, list):
161
+ raise PatchError("Patch chunks are invalid.")
162
+ new_content = apply_update(original, chunks)
163
+ move_path = operation.get("move_path")
164
+ if isinstance(move_path, Path):
165
+ move_path.parent.mkdir(parents=True, exist_ok=True)
166
+ move_path.write_text(new_content)
167
+ path.unlink()
168
+ changed.append({"path": str(move_path), "status": "modified"})
169
+ else:
170
+ path.write_text(new_content)
171
+ changed.append({"path": str(path), "status": "modified"})
172
+ return {"files": changed}
173
+
174
+
175
+ def run_apply_patch_cli(*, cwd: Path, patch: str) -> int:
176
+ try:
177
+ result = apply_patch(patch, cwd.resolve(strict=False))
178
+ except Exception as error:
179
+ print(json.dumps({"error": str(error)}))
180
+ return 1
181
+ print(json.dumps(result))
182
+ return 0
@@ -0,0 +1,11 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ DEFAULT_DATA_DIR = Path.home() / ".flowent"
5
+
6
+
7
+ def data_directory() -> Path:
8
+ configured_directory = os.environ.get("FLOWENT_DATA_DIR")
9
+ if configured_directory:
10
+ return Path(configured_directory).expanduser()
11
+ return DEFAULT_DATA_DIR