flowent 0.0.7 → 0.0.11

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 (387) hide show
  1. package/README.md +0 -3
  2. package/backend/README.md +0 -3
  3. package/backend/pyproject.toml +2 -8
  4. package/backend/src/flowent/__init__.py +6 -2
  5. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/agent.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 +213 -3173
  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 +376 -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 +477 -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__/cli.cpython-313.pyc +0 -0
  59. package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
  60. package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
  61. package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
  62. package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
  63. package/backend/src/flowent/__pycache__/image_assets.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 -2401
  86. package/backend/src/flowent/image_assets.py +0 -356
  87. package/backend/src/flowent/model_metadata.py +0 -102
  88. package/backend/src/flowent/models/__init__.py +0 -125
  89. package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
  90. package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
  91. package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
  92. package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
  93. package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
  94. package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
  95. package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
  96. package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
  97. package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
  98. package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
  99. package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
  100. package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
  101. package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
  102. package/backend/src/flowent/models/agent.py +0 -34
  103. package/backend/src/flowent/models/base.py +0 -24
  104. package/backend/src/flowent/models/blueprint.py +0 -176
  105. package/backend/src/flowent/models/content.py +0 -164
  106. package/backend/src/flowent/models/delta.py +0 -44
  107. package/backend/src/flowent/models/event.py +0 -51
  108. package/backend/src/flowent/models/graph.py +0 -472
  109. package/backend/src/flowent/models/history.py +0 -272
  110. package/backend/src/flowent/models/llm.py +0 -62
  111. package/backend/src/flowent/models/message.py +0 -33
  112. package/backend/src/flowent/models/tab.py +0 -85
  113. package/backend/src/flowent/models/todo.py +0 -10
  114. package/backend/src/flowent/network.py +0 -146
  115. package/backend/src/flowent/observability_service.py +0 -218
  116. package/backend/src/flowent/prompts/__init__.py +0 -67
  117. package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  118. package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
  119. package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
  120. package/backend/src/flowent/prompts/common.py +0 -250
  121. package/backend/src/flowent/prompts/steward.py +0 -64
  122. package/backend/src/flowent/providers/__init__.py +0 -23
  123. package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  124. package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
  125. package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
  126. package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
  127. package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
  128. package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
  129. package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
  130. package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
  131. package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
  132. package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
  133. package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
  134. package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
  135. package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
  136. package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
  137. package/backend/src/flowent/providers/anthropic.py +0 -468
  138. package/backend/src/flowent/providers/base_url.py +0 -60
  139. package/backend/src/flowent/providers/configuration.py +0 -189
  140. package/backend/src/flowent/providers/content.py +0 -122
  141. package/backend/src/flowent/providers/errors.py +0 -223
  142. package/backend/src/flowent/providers/gateway.py +0 -169
  143. package/backend/src/flowent/providers/gemini.py +0 -447
  144. package/backend/src/flowent/providers/headers.py +0 -20
  145. package/backend/src/flowent/providers/management.py +0 -96
  146. package/backend/src/flowent/providers/ollama.py +0 -293
  147. package/backend/src/flowent/providers/openai.py +0 -422
  148. package/backend/src/flowent/providers/openai_responses.py +0 -655
  149. package/backend/src/flowent/providers/registry.py +0 -144
  150. package/backend/src/flowent/providers/sse.py +0 -31
  151. package/backend/src/flowent/providers/thinking.py +0 -79
  152. package/backend/src/flowent/registry.py +0 -73
  153. package/backend/src/flowent/role_management.py +0 -270
  154. package/backend/src/flowent/routes/__init__.py +0 -26
  155. package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  156. package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
  157. package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
  158. package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
  159. package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
  160. package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
  161. package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
  162. package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
  163. package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
  164. package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
  165. package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
  166. package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
  167. package/backend/src/flowent/routes/access.py +0 -48
  168. package/backend/src/flowent/routes/assistant.py +0 -158
  169. package/backend/src/flowent/routes/image_assets.py +0 -33
  170. package/backend/src/flowent/routes/meta.py +0 -28
  171. package/backend/src/flowent/routes/nodes.py +0 -423
  172. package/backend/src/flowent/routes/prompts.py +0 -46
  173. package/backend/src/flowent/routes/providers_route.py +0 -365
  174. package/backend/src/flowent/routes/roles.py +0 -207
  175. package/backend/src/flowent/routes/settings.py +0 -379
  176. package/backend/src/flowent/routes/tabs.py +0 -298
  177. package/backend/src/flowent/routes/ws.py +0 -33
  178. package/backend/src/flowent/runtime.py +0 -160
  179. package/backend/src/flowent/security.py +0 -37
  180. package/backend/src/flowent/settings.py +0 -2112
  181. package/backend/src/flowent/settings_management.py +0 -394
  182. package/backend/src/flowent/state_db.py +0 -108
  183. package/backend/src/flowent/static/assets/AssistantPage-BW7XAd9I.js +0 -1
  184. package/backend/src/flowent/static/assets/ChannelsPage-tCJHgt6m.js +0 -1
  185. package/backend/src/flowent/static/assets/PageScaffold-f6g2l7XN.js +0 -1
  186. package/backend/src/flowent/static/assets/PromptsPage-C3Sxn2D7.js +0 -1
  187. package/backend/src/flowent/static/assets/ProvidersPage-BfmdXmNt.js +0 -3
  188. package/backend/src/flowent/static/assets/RolesPage-DET8wO4r.js +0 -1
  189. package/backend/src/flowent/static/assets/SettingsPage-D-g3deMm.js +0 -3
  190. package/backend/src/flowent/static/assets/ToolsPage-CDmtE2g4.js +0 -1
  191. package/backend/src/flowent/static/assets/WorkspacePage-AZsJ0sD0.js +0 -3
  192. package/backend/src/flowent/static/assets/WorkspacePanels-CteCjolX.js +0 -1
  193. package/backend/src/flowent/static/assets/alert-dialog-Duorp_S-.js +0 -1
  194. package/backend/src/flowent/static/assets/dialog-C3ixjGjN.js +0 -1
  195. package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +0 -6312
  196. package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +0 -1
  197. package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +0 -7
  198. package/backend/src/flowent/static/assets/index--o_0fv0N.css +0 -1
  199. package/backend/src/flowent/static/assets/index-C9HuekJm.js +0 -10
  200. package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +0 -24
  201. package/backend/src/flowent/static/assets/markdown-vendor-C9RtvaJh.js +0 -29
  202. package/backend/src/flowent/static/assets/modelParams-DmnF2hwR.js +0 -1
  203. package/backend/src/flowent/static/assets/providerTypes-DT3Ahwl_.js +0 -1
  204. package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +0 -9
  205. package/backend/src/flowent/static/assets/roles-CuRT_chR.js +0 -1
  206. package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +0 -1
  207. package/backend/src/flowent/static/assets/select-DCfeNu-F.js +0 -1
  208. package/backend/src/flowent/static/assets/surface-pWwG5ogx.js +0 -1
  209. package/backend/src/flowent/static/assets/ui-vendor-C5pJa8N7.js +0 -51
  210. package/backend/src/flowent/static/assets/useAppRoute-FgSHBKhV.js +0 -1
  211. package/backend/src/flowent/static/favicon.svg +0 -4
  212. package/backend/src/flowent/tools/__init__.py +0 -176
  213. package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  214. package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
  215. package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
  216. package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
  217. package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
  218. package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
  219. package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
  220. package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
  221. package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
  222. package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
  223. package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
  224. package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
  225. package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
  226. package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
  227. package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
  228. package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
  229. package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
  230. package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
  231. package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
  232. package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
  233. package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
  234. package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
  235. package/backend/src/flowent/tools/connect.py +0 -100
  236. package/backend/src/flowent/tools/contacts.py +0 -22
  237. package/backend/src/flowent/tools/create_agent.py +0 -191
  238. package/backend/src/flowent/tools/create_tab.py +0 -61
  239. package/backend/src/flowent/tools/delete_tab.py +0 -39
  240. package/backend/src/flowent/tools/edit.py +0 -142
  241. package/backend/src/flowent/tools/exec.py +0 -118
  242. package/backend/src/flowent/tools/fetch.py +0 -85
  243. package/backend/src/flowent/tools/idle.py +0 -27
  244. package/backend/src/flowent/tools/list_roles.py +0 -68
  245. package/backend/src/flowent/tools/list_tabs.py +0 -100
  246. package/backend/src/flowent/tools/list_tools.py +0 -28
  247. package/backend/src/flowent/tools/manage_prompts.py +0 -102
  248. package/backend/src/flowent/tools/manage_providers.py +0 -220
  249. package/backend/src/flowent/tools/manage_roles.py +0 -275
  250. package/backend/src/flowent/tools/manage_settings.py +0 -326
  251. package/backend/src/flowent/tools/read.py +0 -152
  252. package/backend/src/flowent/tools/send.py +0 -68
  253. package/backend/src/flowent/tools/set_permissions.py +0 -99
  254. package/backend/src/flowent/tools/sleep.py +0 -41
  255. package/backend/src/flowent/tools/todo.py +0 -51
  256. package/backend/src/flowent/workspace_store.py +0 -479
  257. package/backend/tests/__init__.py +0 -0
  258. package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  259. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  260. package/backend/tests/conftest.py +0 -6
  261. package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  262. package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
  263. package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
  264. package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
  265. package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
  266. package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
  267. package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
  268. package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
  269. package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
  270. package/backend/tests/integration/api/conftest.py +0 -29
  271. package/backend/tests/integration/api/test_access_api.py +0 -182
  272. package/backend/tests/integration/api/test_assistant_api.py +0 -422
  273. package/backend/tests/integration/api/test_frontend_mounting.py +0 -61
  274. package/backend/tests/integration/api/test_meta_api.py +0 -32
  275. package/backend/tests/integration/api/test_nodes_api.py +0 -787
  276. package/backend/tests/integration/api/test_prompts_api.py +0 -47
  277. package/backend/tests/integration/api/test_roles_api.py +0 -228
  278. package/backend/tests/integration/api/test_tabs_api.py +0 -688
  279. package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
  280. package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  281. package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  282. package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
  283. package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
  284. package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
  285. package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
  286. package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  287. package/backend/tests/unit/agent/test_agent_public_api.py +0 -822
  288. package/backend/tests/unit/agent/test_agent_runtime.py +0 -3088
  289. package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
  290. package/backend/tests/unit/channels/test_telegram_channel.py +0 -552
  291. package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  292. package/backend/tests/unit/logging/test_logging.py +0 -132
  293. package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
  294. package/backend/tests/unit/prompts/test_prompts.py +0 -570
  295. package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  296. package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
  297. package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
  298. package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  299. package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
  300. package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
  301. package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
  302. package/backend/tests/unit/providers/test_anthropic_provider.py +0 -185
  303. package/backend/tests/unit/providers/test_errors.py +0 -68
  304. package/backend/tests/unit/providers/test_extract_delta_parts.py +0 -22
  305. package/backend/tests/unit/providers/test_openai_provider.py +0 -139
  306. package/backend/tests/unit/providers/test_openai_responses.py +0 -402
  307. package/backend/tests/unit/providers/test_provider_gateway.py +0 -359
  308. package/backend/tests/unit/providers/test_think_tag_parser.py +0 -36
  309. package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  310. package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
  311. package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  312. package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  313. package/backend/tests/unit/routes/test_prompts_routes.py +0 -82
  314. package/backend/tests/unit/routes/test_providers_route.py +0 -370
  315. package/backend/tests/unit/routes/test_roles_routes.py +0 -539
  316. package/backend/tests/unit/routes/test_settings_routes.py +0 -1123
  317. package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  318. package/backend/tests/unit/runtime/test_bootstrap_runtime.py +0 -1002
  319. package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  320. package/backend/tests/unit/sandbox/test_sandbox_tools.py +0 -78
  321. package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
  322. package/backend/tests/unit/security/test_security.py +0 -124
  323. package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
  324. package/backend/tests/unit/settings/test_settings_roles.py +0 -703
  325. package/backend/tests/unit/test_access.py +0 -45
  326. package/backend/tests/unit/test_cli.py +0 -102
  327. package/backend/tests/unit/test_graph_runtime.py +0 -72
  328. package/backend/tests/unit/test_network.py +0 -51
  329. package/backend/tests/unit/test_state_sqlite_storage.py +0 -87
  330. package/backend/tests/unit/test_workspace_store.py +0 -228
  331. package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  332. package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  333. package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  334. package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  335. package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  336. package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  337. package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  338. package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  339. package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  340. package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  341. package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  342. package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  343. package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  344. package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
  345. package/backend/tests/unit/tools/test_connect_tool.py +0 -228
  346. package/backend/tests/unit/tools/test_create_agent_tool.py +0 -404
  347. package/backend/tests/unit/tools/test_delete_tab_tool.py +0 -116
  348. package/backend/tests/unit/tools/test_edit_tool.py +0 -115
  349. package/backend/tests/unit/tools/test_exec_tool.py +0 -81
  350. package/backend/tests/unit/tools/test_fetch_tool.py +0 -65
  351. package/backend/tests/unit/tools/test_manage_prompts_tool.py +0 -92
  352. package/backend/tests/unit/tools/test_manage_providers_tool.py +0 -460
  353. package/backend/tests/unit/tools/test_manage_roles_tool.py +0 -411
  354. package/backend/tests/unit/tools/test_manage_settings_tool.py +0 -611
  355. package/backend/tests/unit/tools/test_read_tool.py +0 -33
  356. package/backend/tests/unit/tools/test_set_permissions_tool.py +0 -595
  357. package/backend/tests/unit/tools/test_todo_tool.py +0 -37
  358. package/backend/tests/unit/tools/test_tool_registry.py +0 -199
  359. package/dist/frontend/assets/AssistantPage-BW7XAd9I.js +0 -1
  360. package/dist/frontend/assets/ChannelsPage-tCJHgt6m.js +0 -1
  361. package/dist/frontend/assets/PageScaffold-f6g2l7XN.js +0 -1
  362. package/dist/frontend/assets/PromptsPage-C3Sxn2D7.js +0 -1
  363. package/dist/frontend/assets/ProvidersPage-BfmdXmNt.js +0 -3
  364. package/dist/frontend/assets/RolesPage-DET8wO4r.js +0 -1
  365. package/dist/frontend/assets/SettingsPage-D-g3deMm.js +0 -3
  366. package/dist/frontend/assets/ToolsPage-CDmtE2g4.js +0 -1
  367. package/dist/frontend/assets/WorkspacePage-AZsJ0sD0.js +0 -3
  368. package/dist/frontend/assets/WorkspacePanels-CteCjolX.js +0 -1
  369. package/dist/frontend/assets/alert-dialog-Duorp_S-.js +0 -1
  370. package/dist/frontend/assets/dialog-C3ixjGjN.js +0 -1
  371. package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +0 -6312
  372. package/dist/frontend/assets/graph-vendor-CHpVij2M.css +0 -1
  373. package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +0 -7
  374. package/dist/frontend/assets/index--o_0fv0N.css +0 -1
  375. package/dist/frontend/assets/index-C9HuekJm.js +0 -10
  376. package/dist/frontend/assets/layout.worker-jMHqAFbP.js +0 -24
  377. package/dist/frontend/assets/markdown-vendor-C9RtvaJh.js +0 -29
  378. package/dist/frontend/assets/modelParams-DmnF2hwR.js +0 -1
  379. package/dist/frontend/assets/providerTypes-DT3Ahwl_.js +0 -1
  380. package/dist/frontend/assets/react-vendor-mEs_JJxa.js +0 -9
  381. package/dist/frontend/assets/roles-CuRT_chR.js +0 -1
  382. package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +0 -1
  383. package/dist/frontend/assets/select-DCfeNu-F.js +0 -1
  384. package/dist/frontend/assets/surface-pWwG5ogx.js +0 -1
  385. package/dist/frontend/assets/ui-vendor-C5pJa8N7.js +0 -51
  386. package/dist/frontend/assets/useAppRoute-FgSHBKhV.js +0 -1
  387. 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