flowent 0.0.1 → 0.0.5

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 (484) hide show
  1. package/README.md +20 -9
  2. package/backend/.python-version +1 -0
  3. package/backend/README.md +74 -0
  4. package/backend/pyproject.toml +58 -0
  5. package/backend/src/flowent/__init__.py +3 -0
  6. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/__pycache__/observability_service.cpython-313.pyc +0 -0
  23. package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
  24. package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
  25. package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
  26. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  27. package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
  28. package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
  29. package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
  30. package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
  31. package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
  32. package/backend/src/flowent/_version.py +7 -0
  33. package/backend/src/flowent/access.py +247 -0
  34. package/backend/src/flowent/agent.py +3120 -0
  35. package/backend/src/flowent/assistant_commands.py +115 -0
  36. package/backend/src/flowent/channels/__init__.py +3 -0
  37. package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
  39. package/backend/src/flowent/channels/telegram.py +615 -0
  40. package/backend/src/flowent/cli.py +85 -0
  41. package/backend/src/flowent/config.py +14 -0
  42. package/backend/src/flowent/dev.py +3 -0
  43. package/backend/src/flowent/events.py +157 -0
  44. package/backend/src/flowent/graph_runtime.py +60 -0
  45. package/backend/src/flowent/graph_service.py +2508 -0
  46. package/backend/src/flowent/image_assets.py +356 -0
  47. package/backend/src/flowent/logging.py +155 -0
  48. package/backend/src/flowent/main.py +124 -0
  49. package/backend/src/flowent/mcp_service.py +1918 -0
  50. package/backend/src/flowent/model_metadata.py +102 -0
  51. package/backend/src/flowent/models/__init__.py +125 -0
  52. package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
  54. package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
  55. package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
  56. package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
  57. package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
  58. package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
  59. package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
  60. package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
  61. package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
  62. package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
  63. package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
  64. package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
  65. package/backend/src/flowent/models/agent.py +34 -0
  66. package/backend/src/flowent/models/base.py +24 -0
  67. package/backend/src/flowent/models/blueprint.py +176 -0
  68. package/backend/src/flowent/models/content.py +164 -0
  69. package/backend/src/flowent/models/delta.py +44 -0
  70. package/backend/src/flowent/models/event.py +51 -0
  71. package/backend/src/flowent/models/graph.py +472 -0
  72. package/backend/src/flowent/models/history.py +272 -0
  73. package/backend/src/flowent/models/llm.py +62 -0
  74. package/backend/src/flowent/models/message.py +33 -0
  75. package/backend/src/flowent/models/tab.py +85 -0
  76. package/backend/src/flowent/models/todo.py +10 -0
  77. package/backend/src/flowent/network.py +146 -0
  78. package/backend/src/flowent/observability_service.py +218 -0
  79. package/backend/src/flowent/prompts/__init__.py +67 -0
  80. package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
  82. package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
  83. package/backend/src/flowent/prompts/common.py +250 -0
  84. package/backend/src/flowent/prompts/steward.py +64 -0
  85. package/backend/src/flowent/providers/__init__.py +23 -0
  86. package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  87. package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
  88. package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
  89. package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
  90. package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
  91. package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
  92. package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
  93. package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
  94. package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
  95. package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
  96. package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
  97. package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
  98. package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
  99. package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
  100. package/backend/src/flowent/providers/anthropic.py +468 -0
  101. package/backend/src/flowent/providers/base_url.py +60 -0
  102. package/backend/src/flowent/providers/configuration.py +189 -0
  103. package/backend/src/flowent/providers/content.py +122 -0
  104. package/backend/src/flowent/providers/errors.py +223 -0
  105. package/backend/src/flowent/providers/gateway.py +169 -0
  106. package/backend/src/flowent/providers/gemini.py +447 -0
  107. package/backend/src/flowent/providers/headers.py +20 -0
  108. package/backend/src/flowent/providers/management.py +96 -0
  109. package/backend/src/flowent/providers/ollama.py +293 -0
  110. package/backend/src/flowent/providers/openai.py +422 -0
  111. package/backend/src/flowent/providers/openai_responses.py +655 -0
  112. package/backend/src/flowent/providers/registry.py +144 -0
  113. package/backend/src/flowent/providers/sse.py +31 -0
  114. package/backend/src/flowent/providers/thinking.py +79 -0
  115. package/backend/src/flowent/registry.py +73 -0
  116. package/backend/src/flowent/role_management.py +267 -0
  117. package/backend/src/flowent/routes/__init__.py +28 -0
  118. package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
  120. package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
  121. package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
  122. package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
  123. package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
  124. package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
  125. package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
  126. package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
  127. package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
  128. package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
  129. package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
  130. package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
  131. package/backend/src/flowent/routes/access.py +48 -0
  132. package/backend/src/flowent/routes/assistant.py +155 -0
  133. package/backend/src/flowent/routes/image_assets.py +33 -0
  134. package/backend/src/flowent/routes/mcp.py +125 -0
  135. package/backend/src/flowent/routes/meta.py +28 -0
  136. package/backend/src/flowent/routes/nodes.py +413 -0
  137. package/backend/src/flowent/routes/prompts.py +46 -0
  138. package/backend/src/flowent/routes/providers_route.py +365 -0
  139. package/backend/src/flowent/routes/roles.py +207 -0
  140. package/backend/src/flowent/routes/settings.py +328 -0
  141. package/backend/src/flowent/routes/tabs.py +310 -0
  142. package/backend/src/flowent/routes/ws.py +33 -0
  143. package/backend/src/flowent/runtime.py +165 -0
  144. package/backend/src/flowent/sandbox.py +45 -0
  145. package/backend/src/flowent/security.py +57 -0
  146. package/backend/src/flowent/settings.py +2518 -0
  147. package/backend/src/flowent/settings_management.py +298 -0
  148. package/backend/src/flowent/state_db.py +120 -0
  149. package/backend/src/flowent/static/assets/AssistantPage-VBohhz4d.js +1 -0
  150. package/backend/src/flowent/static/assets/ChannelsPage-CIydPZA_.js +1 -0
  151. package/backend/src/flowent/static/assets/McpPage-CHPm2TPY.js +7 -0
  152. package/backend/src/flowent/static/assets/PageScaffold-DteOA8V7.js +1 -0
  153. package/backend/src/flowent/static/assets/PromptsPage-CSmJ3sZg.js +1 -0
  154. package/backend/src/flowent/static/assets/ProvidersPage-sl2jeG4e.js +3 -0
  155. package/backend/src/flowent/static/assets/RolesPage-DCe7W6Km.js +1 -0
  156. package/backend/src/flowent/static/assets/SettingsPage-Bix9e63E.js +3 -0
  157. package/backend/src/flowent/static/assets/ToolsPage-favNkj5C.js +1 -0
  158. package/backend/src/flowent/static/assets/WorkspaceCommandDialog-DRS6wiD6.js +1 -0
  159. package/backend/src/flowent/static/assets/WorkspacePage-KuaDjt_D.js +3 -0
  160. package/backend/src/flowent/static/assets/WorkspacePanels-BZxBw8M5.js +1 -0
  161. package/backend/src/flowent/static/assets/alert-dialog-DIBUCmqM.js +1 -0
  162. package/backend/src/flowent/static/assets/datetime-eJqd0V2S.js +1 -0
  163. package/backend/src/flowent/static/assets/dialog-BOvHIBrg.js +1 -0
  164. package/backend/src/flowent/static/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  165. package/backend/src/flowent/static/assets/graph-vendor-CHpVij2M.css +1 -0
  166. package/backend/src/flowent/static/assets/graph-vendor-DRq_-6fV.js +7 -0
  167. package/backend/src/flowent/static/assets/index-Biio-CoI.js +10 -0
  168. package/backend/src/flowent/static/assets/index-CmQvO7sl.css +1 -0
  169. package/backend/src/flowent/static/assets/layout.worker-jMHqAFbP.js +24 -0
  170. package/backend/src/flowent/static/assets/markdown-vendor-C9RtvaJh.js +29 -0
  171. package/backend/src/flowent/static/assets/modelParams-DcEhGnu0.js +1 -0
  172. package/backend/src/flowent/static/assets/react-vendor-mEs_JJxa.js +9 -0
  173. package/backend/src/flowent/static/assets/roles-BbIEIMeG.js +1 -0
  174. package/backend/src/flowent/static/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  175. package/backend/src/flowent/static/assets/select-D9SwnlXF.js +1 -0
  176. package/backend/src/flowent/static/assets/surface-Bzr1FRG4.js +1 -0
  177. package/backend/src/flowent/static/assets/triState-DgLlKdRR.js +1 -0
  178. package/backend/src/flowent/static/assets/ui-vendor-UazN8rcv.js +51 -0
  179. package/backend/src/flowent/static/index.html +35 -0
  180. package/backend/src/flowent/tools/__init__.py +275 -0
  181. package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  182. package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
  183. package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
  184. package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
  185. package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
  186. package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
  187. package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
  188. package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
  189. package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
  190. package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
  191. package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
  192. package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
  193. package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
  194. package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
  195. package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
  196. package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
  197. package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
  198. package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
  199. package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
  200. package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
  201. package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
  202. package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
  203. package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
  204. package/backend/src/flowent/tools/connect.py +100 -0
  205. package/backend/src/flowent/tools/contacts.py +22 -0
  206. package/backend/src/flowent/tools/create_agent.py +191 -0
  207. package/backend/src/flowent/tools/create_tab.py +61 -0
  208. package/backend/src/flowent/tools/delete_tab.py +39 -0
  209. package/backend/src/flowent/tools/edit.py +142 -0
  210. package/backend/src/flowent/tools/exec.py +118 -0
  211. package/backend/src/flowent/tools/fetch.py +85 -0
  212. package/backend/src/flowent/tools/idle.py +27 -0
  213. package/backend/src/flowent/tools/list_roles.py +75 -0
  214. package/backend/src/flowent/tools/list_tabs.py +100 -0
  215. package/backend/src/flowent/tools/list_tools.py +28 -0
  216. package/backend/src/flowent/tools/manage_prompts.py +102 -0
  217. package/backend/src/flowent/tools/manage_providers.py +220 -0
  218. package/backend/src/flowent/tools/manage_roles.py +275 -0
  219. package/backend/src/flowent/tools/manage_settings.py +364 -0
  220. package/backend/src/flowent/tools/mcp.py +199 -0
  221. package/backend/src/flowent/tools/read.py +152 -0
  222. package/backend/src/flowent/tools/send.py +68 -0
  223. package/backend/src/flowent/tools/set_permissions.py +99 -0
  224. package/backend/src/flowent/tools/sleep.py +41 -0
  225. package/backend/src/flowent/tools/todo.py +51 -0
  226. package/backend/src/flowent/workspace_store.py +479 -0
  227. package/backend/tests/__init__.py +0 -0
  228. package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  229. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  230. package/backend/tests/conftest.py +6 -0
  231. package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  232. package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
  233. package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
  234. package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
  235. package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
  236. package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
  237. package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
  238. package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
  239. package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
  240. package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
  241. package/backend/tests/integration/api/conftest.py +29 -0
  242. package/backend/tests/integration/api/test_access_api.py +182 -0
  243. package/backend/tests/integration/api/test_assistant_api.py +354 -0
  244. package/backend/tests/integration/api/test_frontend_mounting.py +61 -0
  245. package/backend/tests/integration/api/test_mcp_api.py +116 -0
  246. package/backend/tests/integration/api/test_meta_api.py +33 -0
  247. package/backend/tests/integration/api/test_nodes_api.py +722 -0
  248. package/backend/tests/integration/api/test_prompts_api.py +47 -0
  249. package/backend/tests/integration/api/test_roles_api.py +228 -0
  250. package/backend/tests/integration/api/test_tabs_api.py +802 -0
  251. package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
  252. package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  253. package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  254. package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
  255. package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
  256. package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
  257. package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
  258. package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  259. package/backend/tests/unit/agent/test_agent_public_api.py +837 -0
  260. package/backend/tests/unit/agent/test_agent_runtime.py +2942 -0
  261. package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
  262. package/backend/tests/unit/channels/test_telegram_channel.py +552 -0
  263. package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  264. package/backend/tests/unit/logging/test_logging.py +132 -0
  265. package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
  266. package/backend/tests/unit/prompts/test_prompts.py +570 -0
  267. package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  268. package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
  269. package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
  270. package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  271. package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
  272. package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
  273. package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
  274. package/backend/tests/unit/providers/test_anthropic_provider.py +185 -0
  275. package/backend/tests/unit/providers/test_errors.py +68 -0
  276. package/backend/tests/unit/providers/test_extract_delta_parts.py +22 -0
  277. package/backend/tests/unit/providers/test_openai_provider.py +139 -0
  278. package/backend/tests/unit/providers/test_openai_responses.py +402 -0
  279. package/backend/tests/unit/providers/test_provider_gateway.py +359 -0
  280. package/backend/tests/unit/providers/test_think_tag_parser.py +36 -0
  281. package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  282. package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
  283. package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  284. package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  285. package/backend/tests/unit/routes/test_prompts_routes.py +104 -0
  286. package/backend/tests/unit/routes/test_providers_route.py +370 -0
  287. package/backend/tests/unit/routes/test_roles_routes.py +535 -0
  288. package/backend/tests/unit/routes/test_settings_routes.py +1142 -0
  289. package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  290. package/backend/tests/unit/runtime/test_bootstrap_runtime.py +1002 -0
  291. package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  292. package/backend/tests/unit/sandbox/test_sandbox_tools.py +78 -0
  293. package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
  294. package/backend/tests/unit/security/test_security.py +124 -0
  295. package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
  296. package/backend/tests/unit/settings/test_settings_roles.py +751 -0
  297. package/backend/tests/unit/test_access.py +45 -0
  298. package/backend/tests/unit/test_cli.py +124 -0
  299. package/backend/tests/unit/test_graph_runtime.py +72 -0
  300. package/backend/tests/unit/test_network.py +51 -0
  301. package/backend/tests/unit/test_state_sqlite_storage.py +159 -0
  302. package/backend/tests/unit/test_workspace_store.py +231 -0
  303. package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  304. package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  305. package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  306. package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  307. package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  308. package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  309. package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  310. package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  311. package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  312. package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  313. package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  314. package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  315. package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  316. package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
  317. package/backend/tests/unit/tools/test_connect_tool.py +228 -0
  318. package/backend/tests/unit/tools/test_create_agent_tool.py +436 -0
  319. package/backend/tests/unit/tools/test_delete_tab_tool.py +116 -0
  320. package/backend/tests/unit/tools/test_edit_tool.py +115 -0
  321. package/backend/tests/unit/tools/test_exec_tool.py +81 -0
  322. package/backend/tests/unit/tools/test_fetch_tool.py +65 -0
  323. package/backend/tests/unit/tools/test_manage_prompts_tool.py +117 -0
  324. package/backend/tests/unit/tools/test_manage_providers_tool.py +460 -0
  325. package/backend/tests/unit/tools/test_manage_roles_tool.py +411 -0
  326. package/backend/tests/unit/tools/test_manage_settings_tool.py +611 -0
  327. package/backend/tests/unit/tools/test_read_tool.py +33 -0
  328. package/backend/tests/unit/tools/test_set_permissions_tool.py +595 -0
  329. package/backend/tests/unit/tools/test_todo_tool.py +37 -0
  330. package/backend/tests/unit/tools/test_tool_registry.py +194 -0
  331. package/backend/uv.lock +1144 -0
  332. package/bin/flowent.mjs +62 -36
  333. package/dist/frontend/assets/AssistantPage-VBohhz4d.js +1 -0
  334. package/dist/frontend/assets/ChannelsPage-CIydPZA_.js +1 -0
  335. package/dist/frontend/assets/McpPage-CHPm2TPY.js +7 -0
  336. package/dist/frontend/assets/PageScaffold-DteOA8V7.js +1 -0
  337. package/dist/frontend/assets/PromptsPage-CSmJ3sZg.js +1 -0
  338. package/dist/frontend/assets/ProvidersPage-sl2jeG4e.js +3 -0
  339. package/dist/frontend/assets/RolesPage-DCe7W6Km.js +1 -0
  340. package/dist/frontend/assets/SettingsPage-Bix9e63E.js +3 -0
  341. package/dist/frontend/assets/ToolsPage-favNkj5C.js +1 -0
  342. package/dist/frontend/assets/WorkspaceCommandDialog-DRS6wiD6.js +1 -0
  343. package/dist/frontend/assets/WorkspacePage-KuaDjt_D.js +3 -0
  344. package/dist/frontend/assets/WorkspacePanels-BZxBw8M5.js +1 -0
  345. package/dist/frontend/assets/alert-dialog-DIBUCmqM.js +1 -0
  346. package/dist/frontend/assets/datetime-eJqd0V2S.js +1 -0
  347. package/dist/frontend/assets/dialog-BOvHIBrg.js +1 -0
  348. package/dist/frontend/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  349. package/dist/frontend/assets/graph-vendor-CHpVij2M.css +1 -0
  350. package/dist/frontend/assets/graph-vendor-DRq_-6fV.js +7 -0
  351. package/dist/frontend/assets/index-Biio-CoI.js +10 -0
  352. package/dist/frontend/assets/index-CmQvO7sl.css +1 -0
  353. package/dist/frontend/assets/layout.worker-jMHqAFbP.js +24 -0
  354. package/dist/frontend/assets/markdown-vendor-C9RtvaJh.js +29 -0
  355. package/dist/frontend/assets/modelParams-DcEhGnu0.js +1 -0
  356. package/dist/frontend/assets/react-vendor-mEs_JJxa.js +9 -0
  357. package/dist/frontend/assets/roles-BbIEIMeG.js +1 -0
  358. package/dist/frontend/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  359. package/dist/frontend/assets/select-D9SwnlXF.js +1 -0
  360. package/dist/frontend/assets/surface-Bzr1FRG4.js +1 -0
  361. package/dist/frontend/assets/triState-DgLlKdRR.js +1 -0
  362. package/dist/frontend/assets/ui-vendor-UazN8rcv.js +51 -0
  363. package/dist/frontend/index.html +35 -0
  364. package/package.json +27 -41
  365. package/dist/.next/BUILD_ID +0 -1
  366. package/dist/.next/app-path-routes-manifest.json +0 -6
  367. package/dist/.next/build-manifest.json +0 -20
  368. package/dist/.next/package.json +0 -1
  369. package/dist/.next/prerender-manifest.json +0 -114
  370. package/dist/.next/required-server-files.json +0 -333
  371. package/dist/.next/routes-manifest.json +0 -69
  372. package/dist/.next/server/app/_global-error/page/app-paths-manifest.json +0 -3
  373. package/dist/.next/server/app/_global-error/page/build-manifest.json +0 -16
  374. package/dist/.next/server/app/_global-error/page/next-font-manifest.json +0 -6
  375. package/dist/.next/server/app/_global-error/page/react-loadable-manifest.json +0 -1
  376. package/dist/.next/server/app/_global-error/page/server-reference-manifest.json +0 -4
  377. package/dist/.next/server/app/_global-error/page.js +0 -9
  378. package/dist/.next/server/app/_global-error/page.js.map +0 -5
  379. package/dist/.next/server/app/_global-error/page.js.nft.json +0 -1
  380. package/dist/.next/server/app/_global-error/page_client-reference-manifest.js +0 -3
  381. package/dist/.next/server/app/_global-error.html +0 -1
  382. package/dist/.next/server/app/_global-error.meta +0 -15
  383. package/dist/.next/server/app/_global-error.rsc +0 -14
  384. package/dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +0 -5
  385. package/dist/.next/server/app/_global-error.segments/_full.segment.rsc +0 -14
  386. package/dist/.next/server/app/_global-error.segments/_head.segment.rsc +0 -5
  387. package/dist/.next/server/app/_global-error.segments/_index.segment.rsc +0 -5
  388. package/dist/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
  389. package/dist/.next/server/app/_not-found/page/app-paths-manifest.json +0 -3
  390. package/dist/.next/server/app/_not-found/page/build-manifest.json +0 -16
  391. package/dist/.next/server/app/_not-found/page/next-font-manifest.json +0 -10
  392. package/dist/.next/server/app/_not-found/page/react-loadable-manifest.json +0 -1
  393. package/dist/.next/server/app/_not-found/page/server-reference-manifest.json +0 -4
  394. package/dist/.next/server/app/_not-found/page.js +0 -13
  395. package/dist/.next/server/app/_not-found/page.js.map +0 -5
  396. package/dist/.next/server/app/_not-found/page.js.nft.json +0 -1
  397. package/dist/.next/server/app/_not-found/page_client-reference-manifest.js +0 -3
  398. package/dist/.next/server/app/_not-found.html +0 -1
  399. package/dist/.next/server/app/_not-found.meta +0 -16
  400. package/dist/.next/server/app/_not-found.rsc +0 -16
  401. package/dist/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
  402. package/dist/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
  403. package/dist/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
  404. package/dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
  405. package/dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
  406. package/dist/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
  407. package/dist/.next/server/app/icon.svg/route/app-paths-manifest.json +0 -3
  408. package/dist/.next/server/app/icon.svg/route/build-manifest.json +0 -9
  409. package/dist/.next/server/app/icon.svg/route.js +0 -6
  410. package/dist/.next/server/app/icon.svg/route.js.map +0 -5
  411. package/dist/.next/server/app/icon.svg/route.js.nft.json +0 -1
  412. package/dist/.next/server/app/icon.svg.meta +0 -1
  413. package/dist/.next/server/app/index.html +0 -1
  414. package/dist/.next/server/app/index.meta +0 -14
  415. package/dist/.next/server/app/index.rsc +0 -15
  416. package/dist/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -5
  417. package/dist/.next/server/app/index.segments/_full.segment.rsc +0 -15
  418. package/dist/.next/server/app/index.segments/_head.segment.rsc +0 -6
  419. package/dist/.next/server/app/index.segments/_index.segment.rsc +0 -5
  420. package/dist/.next/server/app/index.segments/_tree.segment.rsc +0 -3
  421. package/dist/.next/server/app/page/app-paths-manifest.json +0 -3
  422. package/dist/.next/server/app/page/build-manifest.json +0 -16
  423. package/dist/.next/server/app/page/next-font-manifest.json +0 -10
  424. package/dist/.next/server/app/page/react-loadable-manifest.json +0 -1
  425. package/dist/.next/server/app/page/server-reference-manifest.json +0 -4
  426. package/dist/.next/server/app/page.js +0 -14
  427. package/dist/.next/server/app/page.js.map +0 -5
  428. package/dist/.next/server/app/page.js.nft.json +0 -1
  429. package/dist/.next/server/app/page_client-reference-manifest.js +0 -3
  430. package/dist/.next/server/app-paths-manifest.json +0 -6
  431. package/dist/.next/server/chunks/[externals]_next_dist_0arv.vj._.js +0 -3
  432. package/dist/.next/server/chunks/[root-of-the-server]__0vcj1q1._.js +0 -13
  433. package/dist/.next/server/chunks/[turbopack]_runtime.js +0 -903
  434. package/dist/.next/server/chunks/_next-internal_server_app_icon_svg_route_actions_0-0ehc~.js +0 -3
  435. package/dist/.next/server/chunks/ssr/05w9_next_dist_0ihu0u9._.js +0 -6
  436. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_12u3mib._.js +0 -3
  437. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_forbidden_04fbe_..js +0 -3
  438. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_global-error_0brpl_..js +0 -3
  439. package/dist/.next/server/chunks/ssr/05w9_next_dist_client_components_builtin_unauthorized_0~2g66g.js +0 -3
  440. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_0~cyr1_.js +0 -4
  441. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_1105emf.js +0 -4
  442. package/dist/.next/server/chunks/ssr/05w9_next_dist_esm_build_templates_app-page_11uhyqv.js +0 -4
  443. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0.t9_75._.js +0 -33
  444. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0c0ud_z._.js +0 -3
  445. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0f9_8d4._.js +0 -3
  446. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0l5ko41._.js +0 -19
  447. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0mn6z7i._.js +0 -3
  448. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0npxxst._.js +0 -33
  449. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0qjhaca._.js +0 -3
  450. package/dist/.next/server/chunks/ssr/[root-of-the-server]__0rwgw3s._.js +0 -3
  451. package/dist/.next/server/chunks/ssr/[turbopack]_runtime.js +0 -903
  452. package/dist/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0k77kol.js +0 -3
  453. package/dist/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0eq97pa.js +0 -3
  454. package/dist/.next/server/chunks/ssr/_next-internal_server_app_page_actions_09-gtaw.js +0 -3
  455. package/dist/.next/server/chunks/ssr/node_modules__pnpm_056~6.6._.js +0 -3
  456. package/dist/.next/server/chunks/ssr/node_modules__pnpm_0~j0k.e._.js +0 -33
  457. package/dist/.next/server/functions-config-manifest.json +0 -4
  458. package/dist/.next/server/middleware-build-manifest.js +0 -20
  459. package/dist/.next/server/middleware-manifest.json +0 -6
  460. package/dist/.next/server/next-font-manifest.js +0 -1
  461. package/dist/.next/server/next-font-manifest.json +0 -13
  462. package/dist/.next/server/pages/404.html +0 -1
  463. package/dist/.next/server/pages/500.html +0 -1
  464. package/dist/.next/server/pages-manifest.json +0 -4
  465. package/dist/.next/server/prefetch-hints.json +0 -1
  466. package/dist/.next/server/server-reference-manifest.js +0 -1
  467. package/dist/.next/server/server-reference-manifest.json +0 -5
  468. package/dist/.next/static/SMWpxFVvkpYFxY7uuFvGB/_buildManifest.js +0 -11
  469. package/dist/.next/static/SMWpxFVvkpYFxY7uuFvGB/_clientMiddlewareManifest.js +0 -1
  470. package/dist/.next/static/SMWpxFVvkpYFxY7uuFvGB/_ssgManifest.js +0 -1
  471. package/dist/.next/static/chunks/01qk2~bgf76vu.js +0 -1
  472. package/dist/.next/static/chunks/03~yq9q893hmn.js +0 -1
  473. package/dist/.next/static/chunks/080queev.r2uy.js +0 -31
  474. package/dist/.next/static/chunks/0v3lyuj75aq50.js +0 -1
  475. package/dist/.next/static/chunks/10b~xdx5c-i7s.js +0 -5
  476. package/dist/.next/static/chunks/14gla2ascffgv.css +0 -2
  477. package/dist/.next/static/chunks/turbopack-0m-970~qvs7sc.js +0 -1
  478. package/dist/.next/static/media/7178b3e590c64307-s.11.cyxs5p-0z~.woff2 +0 -0
  479. package/dist/.next/static/media/8a480f0b521d4e75-s.06d3mdzz5bre_.woff2 +0 -0
  480. package/dist/.next/static/media/caa3a2e1cccd8315-s.p.16t1db8_9y2o~.woff2 +0 -0
  481. package/dist/package.json +0 -88
  482. package/dist/server.js +0 -38
  483. /package/{dist/.next/server/app/icon.svg.body → backend/src/flowent/static/favicon.svg} +0 -0
  484. /package/dist/{.next/static/media/icon.0.r~afrtrocz9.svg → frontend/favicon.svg} +0 -0
@@ -0,0 +1,615 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ import time
6
+ from contextlib import suppress
7
+ from typing import Any
8
+
9
+ from loguru import logger
10
+
11
+ from flowent.assistant_commands import (
12
+ ConversationCommandError,
13
+ execute_conversation_command_input,
14
+ )
15
+ from flowent.events import event_bus
16
+ from flowent.models import Event, EventType, Message
17
+ from flowent.network import create_async_http_session, is_success_status
18
+ from flowent.providers.errors import LLMProviderError
19
+ from flowent.registry import registry
20
+ from flowent.settings import (
21
+ TelegramPendingChat,
22
+ get_settings,
23
+ save_settings,
24
+ )
25
+
26
+ TELEGRAM_API_BASE_URL = "https://api.telegram.org"
27
+ TELEGRAM_LONG_POLL_TIMEOUT_SECONDS = 30
28
+ TELEGRAM_REQUEST_TIMEOUT_SECONDS = 35
29
+ TELEGRAM_EDIT_INTERVAL_SECONDS = 1.0
30
+ TELEGRAM_EDIT_THRESHOLD_CHARS = 100
31
+ TELEGRAM_TYPING_INTERVAL_SECONDS = 4.0
32
+ TELEGRAM_MAX_TEXT_LENGTH = 4000
33
+ PRIVATE_ONLY_MESSAGE = "🔒 Telegram channel currently supports private chats only."
34
+ IMAGE_INPUT_UNSUPPORTED_MESSAGE = "Telegram currently does not support image input yet."
35
+ IMAGE_OUTPUT_UNSUPPORTED_MESSAGE = (
36
+ "Telegram currently does not support image output yet."
37
+ )
38
+
39
+
40
+ class TelegramChannel:
41
+ def __init__(self) -> None:
42
+ settings = get_settings()
43
+ self._bot_token = settings.telegram.bot_token.strip()
44
+ self._app_loop = event_bus.get_loop()
45
+ self._stop_event = threading.Event()
46
+ self._thread: threading.Thread | None = None
47
+ self._thread_loop: asyncio.AbstractEventLoop | None = None
48
+ self._polling_task: asyncio.Task[None] | None = None
49
+ self._offset: int | None = None
50
+ self._event_lock: asyncio.Lock | None = None
51
+ self._stream_buffer = ""
52
+ self._stream_message_ids: dict[int, int] = {}
53
+ self._typing_task: asyncio.Task[None] | None = None
54
+ self._assistant_running = False
55
+ self._last_edit_at = 0.0
56
+ self._last_sent_length = 0
57
+
58
+ def start(self) -> None:
59
+ if not self._bot_token:
60
+ logger.info("Telegram channel not started: bot token is empty")
61
+ return
62
+ if self._thread is not None and self._thread.is_alive():
63
+ return
64
+
65
+ self._app_loop = event_bus.get_loop()
66
+ if self._app_loop is None or self._app_loop.is_closed():
67
+ raise RuntimeError("Event loop is not available for Telegram channel")
68
+
69
+ self._stop_event.clear()
70
+ self._offset = None
71
+ self._event_lock = None
72
+ self._stream_buffer = ""
73
+ self._stream_message_ids = {}
74
+ self._typing_task = None
75
+ self._assistant_running = False
76
+ self._last_edit_at = 0.0
77
+ self._last_sent_length = 0
78
+
79
+ event_bus.subscribe(self._on_event)
80
+ self._thread = threading.Thread(
81
+ target=self._run_polling_thread,
82
+ name="telegram-channel",
83
+ daemon=True,
84
+ )
85
+ self._thread.start()
86
+ logger.info("Telegram channel started")
87
+
88
+ def stop(self) -> None:
89
+ event_bus.unsubscribe(self._on_event)
90
+ self._stop_event.set()
91
+
92
+ polling_loop = self._thread_loop
93
+ task = self._polling_task
94
+ if polling_loop is not None and task is not None:
95
+ polling_loop.call_soon_threadsafe(task.cancel)
96
+ typing_task = self._typing_task
97
+ app_loop = self._app_loop
98
+ if (
99
+ app_loop is not None
100
+ and not app_loop.is_closed()
101
+ and typing_task is not None
102
+ ):
103
+ app_loop.call_soon_threadsafe(typing_task.cancel)
104
+
105
+ if self._thread is not None and self._thread.is_alive():
106
+ self._thread.join(timeout=5.0)
107
+
108
+ self._thread = None
109
+ self._thread_loop = None
110
+ self._polling_task = None
111
+ self._typing_task = None
112
+ self._assistant_running = False
113
+ self._reset_stream_state()
114
+ logger.info("Telegram channel stopped")
115
+
116
+ def _run_polling_thread(self) -> None:
117
+ loop = asyncio.new_event_loop()
118
+ self._thread_loop = loop
119
+ asyncio.set_event_loop(loop)
120
+ self._polling_task = loop.create_task(self._poll_updates_loop())
121
+ try:
122
+ loop.run_until_complete(self._polling_task)
123
+ except asyncio.CancelledError:
124
+ pass
125
+ finally:
126
+ pending = asyncio.all_tasks(loop)
127
+ for task in pending:
128
+ task.cancel()
129
+ if pending:
130
+ loop.run_until_complete(
131
+ asyncio.gather(*pending, return_exceptions=True)
132
+ )
133
+ loop.run_until_complete(loop.shutdown_asyncgens())
134
+ loop.close()
135
+
136
+ async def _poll_updates_loop(self) -> None:
137
+ while not self._stop_event.is_set():
138
+ try:
139
+ result = await self._call_api(
140
+ "getUpdates",
141
+ {
142
+ "offset": self._offset,
143
+ "timeout": TELEGRAM_LONG_POLL_TIMEOUT_SECONDS,
144
+ },
145
+ parse_mode=None,
146
+ )
147
+ if not isinstance(result, list):
148
+ continue
149
+ for update in result:
150
+ if not isinstance(update, dict):
151
+ continue
152
+ update_id = update.get("update_id")
153
+ if isinstance(update_id, int):
154
+ self._offset = update_id + 1
155
+ await self._process_update(update)
156
+ except asyncio.CancelledError:
157
+ raise
158
+ except Exception:
159
+ logger.exception("Telegram polling loop failed")
160
+ await asyncio.sleep(1.0)
161
+
162
+ async def _process_update(self, update: dict[str, Any]) -> None:
163
+ message = update.get("message")
164
+ if not isinstance(message, dict):
165
+ return
166
+
167
+ from_data = message.get("from")
168
+ chat_data = message.get("chat")
169
+ chat_id = chat_data.get("id") if isinstance(chat_data, dict) else None
170
+ chat_type = chat_data.get("type") if isinstance(chat_data, dict) else None
171
+ text = message.get("text")
172
+ caption = message.get("caption")
173
+ photo = message.get("photo")
174
+ document = message.get("document")
175
+ username = from_data.get("username") if isinstance(from_data, dict) else None
176
+ first_name = (
177
+ from_data.get("first_name") if isinstance(from_data, dict) else None
178
+ )
179
+ last_name = from_data.get("last_name") if isinstance(from_data, dict) else None
180
+
181
+ if not isinstance(chat_id, int):
182
+ return
183
+ if chat_type != "private":
184
+ await self._send_message(chat_id, PRIVATE_ONLY_MESSAGE, markdown=False)
185
+ return
186
+
187
+ display_name = self._build_display_name(first_name, last_name, username)
188
+
189
+ settings = get_settings()
190
+ if any(chat.chat_id == chat_id for chat in settings.telegram.approved_chats):
191
+ document_is_image = isinstance(document, dict) and str(
192
+ document.get("mime_type", "")
193
+ ).startswith("image/")
194
+ has_image_input = (
195
+ isinstance(photo, list) and len(photo) > 0
196
+ ) or document_is_image
197
+ if has_image_input:
198
+ await self._send_message(
199
+ chat_id,
200
+ IMAGE_INPUT_UNSUPPORTED_MESSAGE,
201
+ markdown=False,
202
+ )
203
+ return
204
+
205
+ incoming_text = text if isinstance(text, str) else caption
206
+ if not isinstance(incoming_text, str) or not incoming_text.strip():
207
+ return
208
+
209
+ if not isinstance(text, str) or not text.strip():
210
+ text = incoming_text
211
+
212
+ assistant = registry.get_assistant()
213
+ if assistant is None:
214
+ logger.warning("Telegram message dropped: assistant not available")
215
+ return
216
+
217
+ try:
218
+ executed_command = execute_conversation_command_input(assistant, text)
219
+ except ConversationCommandError as exc:
220
+ await self._send_message(chat_id, str(exc), markdown=False)
221
+ return
222
+ except (RuntimeError, TimeoutError, LLMProviderError) as exc:
223
+ await self._send_message(chat_id, str(exc), markdown=False)
224
+ return
225
+
226
+ if executed_command is not None:
227
+ await self._send_message(
228
+ chat_id, executed_command.feedback, markdown=False
229
+ )
230
+ return
231
+
232
+ assistant.enqueue_message(
233
+ Message(from_id="human", to_id=assistant.uuid, content=text)
234
+ )
235
+ return
236
+
237
+ if self._upsert_pending_chat(settings, chat_id, username, display_name):
238
+ save_settings(settings)
239
+ await self._send_message(
240
+ chat_id,
241
+ (f"⏳ This chat is pending approval in Flowent.\nChat ID: `{chat_id}`"),
242
+ markdown=True,
243
+ )
244
+
245
+ def _on_event(self, event: Event) -> None:
246
+ if event.type not in {
247
+ EventType.ASSISTANT_CONTENT,
248
+ EventType.HISTORY_ENTRY_ADDED,
249
+ EventType.NODE_STATE_CHANGED,
250
+ }:
251
+ return
252
+
253
+ loop = self._app_loop
254
+ if loop is None or loop.is_closed():
255
+ return
256
+
257
+ future = asyncio.run_coroutine_threadsafe(self._process_event(event), loop)
258
+ future.add_done_callback(self._log_event_error)
259
+
260
+ @staticmethod
261
+ def _log_event_error(future: Any) -> None:
262
+ try:
263
+ future.result()
264
+ except asyncio.CancelledError:
265
+ return
266
+ except Exception:
267
+ logger.exception("Telegram event handling failed")
268
+
269
+ async def _process_event(self, event: Event) -> None:
270
+ if self._event_lock is None:
271
+ self._event_lock = asyncio.Lock()
272
+
273
+ async with self._event_lock:
274
+ assistant = registry.get_assistant()
275
+ if assistant is None or event.agent_id != assistant.uuid:
276
+ return
277
+
278
+ if event.type == EventType.ASSISTANT_CONTENT:
279
+ content = event.data.get("content")
280
+ if isinstance(content, str) and content:
281
+ await self._handle_assistant_content(content)
282
+ return
283
+
284
+ if event.type == EventType.HISTORY_ENTRY_ADDED:
285
+ entry_type = event.data.get("type")
286
+ if entry_type != "AssistantText":
287
+ return
288
+ parts = event.data.get("parts")
289
+ if not isinstance(parts, list):
290
+ return
291
+ if not any(
292
+ isinstance(part, dict) and part.get("type") == "image"
293
+ for part in parts
294
+ ):
295
+ return
296
+ for chat_id in self._get_approved_chat_ids():
297
+ await self._send_message(
298
+ chat_id,
299
+ IMAGE_OUTPUT_UNSUPPORTED_MESSAGE,
300
+ markdown=False,
301
+ )
302
+ return
303
+
304
+ if event.type != EventType.NODE_STATE_CHANGED:
305
+ return
306
+
307
+ new_state = event.data.get("new_state")
308
+ if new_state == "running":
309
+ await self._begin_running_feedback()
310
+ return
311
+
312
+ await self._end_running_feedback()
313
+
314
+ @staticmethod
315
+ def _get_approved_chat_ids() -> list[int]:
316
+ settings = get_settings()
317
+ return [chat.chat_id for chat in settings.telegram.approved_chats]
318
+
319
+ async def _handle_assistant_content(self, chunk: str) -> None:
320
+ self._stream_buffer += chunk
321
+ await self._ensure_stream_messages()
322
+ now = time.monotonic()
323
+ if (
324
+ len(self._stream_buffer) - self._last_sent_length
325
+ >= TELEGRAM_EDIT_THRESHOLD_CHARS
326
+ or now - self._last_edit_at >= TELEGRAM_EDIT_INTERVAL_SECONDS
327
+ ):
328
+ await self._flush_stream()
329
+
330
+ async def _finalize_stream(self) -> None:
331
+ if not self._stream_buffer and not self._stream_message_ids:
332
+ self._reset_stream_state()
333
+ return
334
+ await self._flush_stream(force=True)
335
+ self._reset_stream_state()
336
+
337
+ async def _begin_running_feedback(self) -> None:
338
+ if self._assistant_running:
339
+ return
340
+ await self._stop_typing_task()
341
+ self._assistant_running = True
342
+ self._reset_stream_state()
343
+ self._typing_task = asyncio.create_task(self._typing_loop())
344
+
345
+ async def _end_running_feedback(self) -> None:
346
+ self._assistant_running = False
347
+ await self._stop_typing_task()
348
+ await self._finalize_stream()
349
+
350
+ async def _stop_typing_task(self) -> None:
351
+ task = self._typing_task
352
+ if task is None:
353
+ return
354
+ self._typing_task = None
355
+ task.cancel()
356
+ with suppress(asyncio.CancelledError):
357
+ await task
358
+
359
+ async def _typing_loop(self) -> None:
360
+ while self._assistant_running and not self._stop_event.is_set():
361
+ try:
362
+ await self._send_typing_feedback_once()
363
+ except asyncio.CancelledError:
364
+ raise
365
+ except Exception:
366
+ logger.exception("Telegram typing loop failed")
367
+ await asyncio.sleep(TELEGRAM_TYPING_INTERVAL_SECONDS)
368
+
369
+ async def _send_typing_feedback_once(self) -> None:
370
+ settings = get_settings()
371
+ approved_chat_ids = {chat.chat_id for chat in settings.telegram.approved_chats}
372
+ for chat_id in list(self._stream_message_ids):
373
+ if chat_id not in approved_chat_ids:
374
+ self._stream_message_ids.pop(chat_id, None)
375
+ for approved_chat in settings.telegram.approved_chats:
376
+ if approved_chat.chat_id in self._stream_message_ids:
377
+ continue
378
+ await self._send_chat_action(approved_chat.chat_id, action="typing")
379
+
380
+ async def _ensure_stream_messages(self) -> None:
381
+ if not self._stream_buffer:
382
+ return
383
+ settings = get_settings()
384
+ had_stream_messages = bool(self._stream_message_ids)
385
+ for approved_chat in settings.telegram.approved_chats:
386
+ if approved_chat.chat_id in self._stream_message_ids:
387
+ continue
388
+ message_id = await self._send_message(
389
+ approved_chat.chat_id,
390
+ self._stream_buffer,
391
+ markdown=False,
392
+ )
393
+ if message_id is not None:
394
+ self._stream_message_ids[approved_chat.chat_id] = message_id
395
+ if not had_stream_messages and self._stream_message_ids:
396
+ self._last_edit_at = time.monotonic()
397
+ self._last_sent_length = len(self._stream_buffer)
398
+
399
+ async def _flush_stream(self, *, force: bool = False) -> None:
400
+ if not self._stream_buffer and not force:
401
+ return
402
+ await self._ensure_stream_messages()
403
+ if not self._stream_message_ids or not self._stream_buffer:
404
+ return
405
+
406
+ current_chat_ids = {
407
+ chat.chat_id for chat in get_settings().telegram.approved_chats
408
+ }
409
+ text = self._format_text(self._stream_buffer)
410
+ for chat_id, message_id in list(self._stream_message_ids.items()):
411
+ if chat_id not in current_chat_ids:
412
+ self._stream_message_ids.pop(chat_id, None)
413
+ continue
414
+ updated = await self._edit_message(chat_id, message_id, text)
415
+ if not updated:
416
+ self._stream_message_ids.pop(chat_id, None)
417
+
418
+ self._last_edit_at = time.monotonic()
419
+ self._last_sent_length = len(self._stream_buffer)
420
+
421
+ async def _send_message(
422
+ self,
423
+ chat_id: int,
424
+ text: str,
425
+ *,
426
+ markdown: bool,
427
+ ) -> int | None:
428
+ payload: dict[str, Any] = {
429
+ "chat_id": chat_id,
430
+ "text": self._format_text(text),
431
+ }
432
+ result = await self._call_api(
433
+ "sendMessage",
434
+ payload,
435
+ parse_mode="Markdown" if markdown else None,
436
+ )
437
+ if not isinstance(result, dict):
438
+ return None
439
+ message_id = result.get("message_id")
440
+ return message_id if isinstance(message_id, int) else None
441
+
442
+ async def _send_chat_action(self, chat_id: int, *, action: str) -> None:
443
+ await self._call_api(
444
+ "sendChatAction",
445
+ {
446
+ "chat_id": chat_id,
447
+ "action": action,
448
+ },
449
+ parse_mode=None,
450
+ )
451
+
452
+ async def _edit_message(
453
+ self,
454
+ chat_id: int,
455
+ message_id: int,
456
+ text: str,
457
+ ) -> bool:
458
+ payload = {
459
+ "chat_id": chat_id,
460
+ "message_id": message_id,
461
+ "text": self._format_text(text),
462
+ }
463
+ result = await self._call_api(
464
+ "editMessageText",
465
+ payload,
466
+ parse_mode="Markdown",
467
+ )
468
+ return result is not None
469
+
470
+ def _format_text(self, text: str) -> str:
471
+ if len(text) <= TELEGRAM_MAX_TEXT_LENGTH:
472
+ return text
473
+ return text[: TELEGRAM_MAX_TEXT_LENGTH - 1] + "…"
474
+
475
+ def _reset_stream_state(self) -> None:
476
+ self._stream_buffer = ""
477
+ self._stream_message_ids = {}
478
+ self._last_edit_at = 0.0
479
+ self._last_sent_length = 0
480
+
481
+ @staticmethod
482
+ def _build_display_name(
483
+ first_name: object,
484
+ last_name: object,
485
+ username: object,
486
+ ) -> str:
487
+ parts = []
488
+ if isinstance(first_name, str) and first_name.strip():
489
+ parts.append(first_name.strip())
490
+ if isinstance(last_name, str) and last_name.strip():
491
+ parts.append(last_name.strip())
492
+ if parts:
493
+ return " ".join(parts)
494
+ if isinstance(username, str) and username.strip():
495
+ return username.strip()
496
+ return ""
497
+
498
+ @staticmethod
499
+ def _upsert_pending_chat(
500
+ settings: Any,
501
+ chat_id: int,
502
+ username: object,
503
+ display_name: str,
504
+ ) -> bool:
505
+ username_value = (
506
+ username.strip() if isinstance(username, str) and username.strip() else None
507
+ )
508
+ now = time.time()
509
+ for pending_chat in settings.telegram.pending_chats:
510
+ if pending_chat.chat_id != chat_id:
511
+ continue
512
+
513
+ changed = False
514
+ if pending_chat.username != username_value:
515
+ pending_chat.username = username_value
516
+ changed = True
517
+ if pending_chat.display_name != display_name:
518
+ pending_chat.display_name = display_name
519
+ changed = True
520
+ if pending_chat.last_seen_at != now:
521
+ pending_chat.last_seen_at = now
522
+ changed = True
523
+ return changed
524
+
525
+ settings.telegram.pending_chats.append(
526
+ TelegramPendingChat(
527
+ chat_id=chat_id,
528
+ username=username_value,
529
+ display_name=display_name,
530
+ first_seen_at=now,
531
+ last_seen_at=now,
532
+ )
533
+ )
534
+ return True
535
+
536
+ async def _call_api(
537
+ self,
538
+ method: str,
539
+ payload: dict[str, Any],
540
+ *,
541
+ parse_mode: str | None,
542
+ ) -> Any:
543
+ if not self._bot_token:
544
+ return None
545
+
546
+ request_payload = {
547
+ key: value for key, value in payload.items() if value is not None
548
+ }
549
+ if parse_mode is not None:
550
+ request_payload["parse_mode"] = parse_mode
551
+
552
+ while not self._stop_event.is_set():
553
+ try:
554
+ async with create_async_http_session(
555
+ timeout=TELEGRAM_REQUEST_TIMEOUT_SECONDS
556
+ ) as client:
557
+ response = await client.post(
558
+ f"{TELEGRAM_API_BASE_URL}/bot{self._bot_token}/{method}",
559
+ data=request_payload,
560
+ )
561
+ except asyncio.CancelledError:
562
+ raise
563
+ except Exception as exc:
564
+ logger.warning("Telegram API request failed ({}): {}", method, exc)
565
+ return None
566
+
567
+ response_data: dict[str, Any] | None = None
568
+ try:
569
+ response_data = response.json()
570
+ except ValueError:
571
+ response_data = None
572
+
573
+ if response.status_code == 429:
574
+ retry_after = self._extract_retry_after(response_data)
575
+ await asyncio.sleep(retry_after)
576
+ continue
577
+
578
+ if is_success_status(response.status_code) and isinstance(
579
+ response_data, dict
580
+ ):
581
+ if response_data.get("ok") is True:
582
+ return response_data.get("result")
583
+ logger.warning(
584
+ "Telegram API returned error for {}: {}",
585
+ method,
586
+ response_data.get("description", "unknown error"),
587
+ )
588
+ elif parse_mode is not None and method in {
589
+ "sendMessage",
590
+ "editMessageText",
591
+ }:
592
+ request_payload.pop("parse_mode", None)
593
+ parse_mode = None
594
+ continue
595
+ else:
596
+ logger.warning(
597
+ "Telegram API request failed for {} with status {}",
598
+ method,
599
+ response.status_code,
600
+ )
601
+ return None
602
+
603
+ return None
604
+
605
+ @staticmethod
606
+ def _extract_retry_after(response_data: dict[str, Any] | None) -> float:
607
+ if not isinstance(response_data, dict):
608
+ return 1.0
609
+ parameters = response_data.get("parameters")
610
+ if not isinstance(parameters, dict):
611
+ return 1.0
612
+ retry_after = parameters.get("retry_after")
613
+ if isinstance(retry_after, (int, float)) and retry_after > 0:
614
+ return float(retry_after)
615
+ return 1.0
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ APP_DATA_DIR_ENV_VAR = "FLOWENT_APP_DATA_DIR"
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> None:
11
+ parser = argparse.ArgumentParser(
12
+ prog="flowent",
13
+ description="Flowent — multi-agent collaboration framework",
14
+ )
15
+ subparsers = parser.add_subparsers(dest="command")
16
+ access_parser = subparsers.add_parser("access", help="Flowent access commands")
17
+ access_subparsers = access_parser.add_subparsers(dest="access_command")
18
+ access_subparsers.add_parser(
19
+ "refresh",
20
+ help="Generate and persist a new admin access code",
21
+ )
22
+ access_subparsers.add_parser("reset", help="Clear the persisted admin access code")
23
+ parser.add_argument(
24
+ "--host",
25
+ "--hostname",
26
+ default=os.environ.get("HOSTNAME") or "0.0.0.0",
27
+ help="Bind host (default: $HOSTNAME or 0.0.0.0)",
28
+ )
29
+ parser.add_argument(
30
+ "--port",
31
+ "-p",
32
+ type=int,
33
+ default=int(os.environ.get("PORT") or "6873"),
34
+ help="Bind port (default: $PORT or 6873)",
35
+ )
36
+ parser.add_argument(
37
+ "--version",
38
+ "-v",
39
+ action="store_true",
40
+ help="Show version and exit",
41
+ )
42
+ parser.add_argument(
43
+ "--app-data-dir",
44
+ default="",
45
+ help="Override the Flowent app data directory for this process",
46
+ )
47
+ args = parser.parse_args(argv)
48
+
49
+ if isinstance(args.app_data_dir, str) and args.app_data_dir.strip():
50
+ os.environ[APP_DATA_DIR_ENV_VAR] = args.app_data_dir.strip()
51
+
52
+ if args.command == "access" and args.access_command == "refresh":
53
+ from flowent.access import refresh_local_access
54
+
55
+ print(refresh_local_access())
56
+ return
57
+
58
+ if args.command == "access" and args.access_command == "reset":
59
+ from flowent.access import reset_local_access
60
+
61
+ print(reset_local_access())
62
+ return
63
+
64
+ if args.version:
65
+ try:
66
+ from importlib.metadata import version
67
+
68
+ ver = version("flowent")
69
+ except Exception:
70
+ from flowent._version import __version__ as ver
71
+
72
+ print(f"flowent {ver}")
73
+ sys.exit(0)
74
+
75
+ import uvicorn
76
+
77
+ uvicorn.run(
78
+ "flowent.main:app",
79
+ host=args.host,
80
+ port=args.port,
81
+ )
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()