flock-core 0.5.0b28__py3-none-any.whl → 0.5.56b0__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (359) hide show
  1. flock/__init__.py +12 -217
  2. flock/agent.py +678 -0
  3. flock/api/themes.py +71 -0
  4. flock/artifacts.py +79 -0
  5. flock/cli.py +75 -0
  6. flock/components.py +173 -0
  7. flock/dashboard/__init__.py +28 -0
  8. flock/dashboard/collector.py +283 -0
  9. flock/dashboard/events.py +182 -0
  10. flock/dashboard/launcher.py +230 -0
  11. flock/dashboard/service.py +537 -0
  12. flock/dashboard/websocket.py +235 -0
  13. flock/engines/__init__.py +6 -0
  14. flock/engines/dspy_engine.py +856 -0
  15. flock/examples.py +128 -0
  16. flock/{core/util → helper}/cli_helper.py +4 -3
  17. flock/{core/logging → logging}/__init__.py +2 -3
  18. flock/{core/logging → logging}/formatters/enum_builder.py +3 -4
  19. flock/{core/logging → logging}/formatters/theme_builder.py +19 -44
  20. flock/{core/logging → logging}/formatters/themed_formatter.py +69 -115
  21. flock/{core/logging → logging}/logging.py +77 -61
  22. flock/{core/logging → logging}/telemetry.py +20 -26
  23. flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
  24. flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +6 -9
  25. flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
  26. flock/{core/logging → logging}/trace_and_logged.py +20 -24
  27. flock/mcp/__init__.py +91 -0
  28. flock/{core/mcp/mcp_client.py → mcp/client.py} +103 -154
  29. flock/{core/mcp/mcp_config.py → mcp/config.py} +62 -117
  30. flock/mcp/manager.py +255 -0
  31. flock/mcp/servers/sse/__init__.py +1 -1
  32. flock/mcp/servers/sse/flock_sse_server.py +11 -53
  33. flock/mcp/servers/stdio/__init__.py +1 -1
  34. flock/mcp/servers/stdio/flock_stdio_server.py +8 -48
  35. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +17 -62
  36. flock/mcp/servers/websockets/flock_websocket_server.py +7 -40
  37. flock/{core/mcp/flock_mcp_tool.py → mcp/tool.py} +16 -26
  38. flock/mcp/types/__init__.py +42 -0
  39. flock/{core/mcp → mcp}/types/callbacks.py +9 -15
  40. flock/{core/mcp → mcp}/types/factories.py +7 -6
  41. flock/{core/mcp → mcp}/types/handlers.py +13 -18
  42. flock/{core/mcp → mcp}/types/types.py +70 -74
  43. flock/{core/mcp → mcp}/util/helpers.py +1 -1
  44. flock/orchestrator.py +645 -0
  45. flock/registry.py +148 -0
  46. flock/runtime.py +262 -0
  47. flock/service.py +140 -0
  48. flock/store.py +69 -0
  49. flock/subscription.py +111 -0
  50. flock/themes/andromeda.toml +1 -1
  51. flock/themes/apple-system-colors.toml +1 -1
  52. flock/themes/arcoiris.toml +1 -1
  53. flock/themes/atomonelight.toml +1 -1
  54. flock/themes/ayu copy.toml +1 -1
  55. flock/themes/ayu-light.toml +1 -1
  56. flock/themes/belafonte-day.toml +1 -1
  57. flock/themes/belafonte-night.toml +1 -1
  58. flock/themes/blulocodark.toml +1 -1
  59. flock/themes/breeze.toml +1 -1
  60. flock/themes/broadcast.toml +1 -1
  61. flock/themes/brogrammer.toml +1 -1
  62. flock/themes/builtin-dark.toml +1 -1
  63. flock/themes/builtin-pastel-dark.toml +1 -1
  64. flock/themes/catppuccin-latte.toml +1 -1
  65. flock/themes/catppuccin-macchiato.toml +1 -1
  66. flock/themes/catppuccin-mocha.toml +1 -1
  67. flock/themes/cga.toml +1 -1
  68. flock/themes/chalk.toml +1 -1
  69. flock/themes/ciapre.toml +1 -1
  70. flock/themes/coffee-theme.toml +1 -1
  71. flock/themes/cyberpunkscarletprotocol.toml +1 -1
  72. flock/themes/dark+.toml +1 -1
  73. flock/themes/darkermatrix.toml +1 -1
  74. flock/themes/darkside.toml +1 -1
  75. flock/themes/desert.toml +1 -1
  76. flock/themes/django.toml +1 -1
  77. flock/themes/djangosmooth.toml +1 -1
  78. flock/themes/doomone.toml +1 -1
  79. flock/themes/dotgov.toml +1 -1
  80. flock/themes/dracula+.toml +1 -1
  81. flock/themes/duckbones.toml +1 -1
  82. flock/themes/encom.toml +1 -1
  83. flock/themes/espresso.toml +1 -1
  84. flock/themes/everblush.toml +1 -1
  85. flock/themes/fairyfloss.toml +1 -1
  86. flock/themes/fideloper.toml +1 -1
  87. flock/themes/fishtank.toml +1 -1
  88. flock/themes/flexoki-light.toml +1 -1
  89. flock/themes/floraverse.toml +1 -1
  90. flock/themes/framer.toml +1 -1
  91. flock/themes/galizur.toml +1 -1
  92. flock/themes/github.toml +1 -1
  93. flock/themes/grass.toml +1 -1
  94. flock/themes/grey-green.toml +1 -1
  95. flock/themes/gruvboxlight.toml +1 -1
  96. flock/themes/guezwhoz.toml +1 -1
  97. flock/themes/harper.toml +1 -1
  98. flock/themes/hax0r-blue.toml +1 -1
  99. flock/themes/hopscotch.256.toml +1 -1
  100. flock/themes/ic-green-ppl.toml +1 -1
  101. flock/themes/iceberg-dark.toml +1 -1
  102. flock/themes/japanesque.toml +1 -1
  103. flock/themes/jubi.toml +1 -1
  104. flock/themes/kibble.toml +1 -1
  105. flock/themes/kolorit.toml +1 -1
  106. flock/themes/kurokula.toml +1 -1
  107. flock/themes/materialdesigncolors.toml +1 -1
  108. flock/themes/matrix.toml +1 -1
  109. flock/themes/mellifluous.toml +1 -1
  110. flock/themes/midnight-in-mojave.toml +1 -1
  111. flock/themes/monokai-remastered.toml +1 -1
  112. flock/themes/monokai-soda.toml +1 -1
  113. flock/themes/neon.toml +1 -1
  114. flock/themes/neopolitan.toml +1 -1
  115. flock/themes/nord-light.toml +1 -1
  116. flock/themes/ocean.toml +1 -1
  117. flock/themes/onehalfdark.toml +1 -1
  118. flock/themes/onehalflight.toml +1 -1
  119. flock/themes/palenighthc.toml +1 -1
  120. flock/themes/paulmillr.toml +1 -1
  121. flock/themes/pencildark.toml +1 -1
  122. flock/themes/pnevma.toml +1 -1
  123. flock/themes/purple-rain.toml +1 -1
  124. flock/themes/purplepeter.toml +1 -1
  125. flock/themes/raycast-dark.toml +1 -1
  126. flock/themes/red-sands.toml +1 -1
  127. flock/themes/relaxed.toml +1 -1
  128. flock/themes/retro.toml +1 -1
  129. flock/themes/rose-pine.toml +1 -1
  130. flock/themes/royal.toml +1 -1
  131. flock/themes/ryuuko.toml +1 -1
  132. flock/themes/sakura.toml +1 -1
  133. flock/themes/scarlet-protocol.toml +1 -1
  134. flock/themes/seoulbones-dark.toml +1 -1
  135. flock/themes/shades-of-purple.toml +1 -1
  136. flock/themes/smyck.toml +1 -1
  137. flock/themes/softserver.toml +1 -1
  138. flock/themes/solarized-darcula.toml +1 -1
  139. flock/themes/square.toml +1 -1
  140. flock/themes/sugarplum.toml +1 -1
  141. flock/themes/thayer-bright.toml +1 -1
  142. flock/themes/tokyonight.toml +1 -1
  143. flock/themes/tomorrow.toml +1 -1
  144. flock/themes/ubuntu.toml +1 -1
  145. flock/themes/ultradark.toml +1 -1
  146. flock/themes/ultraviolent.toml +1 -1
  147. flock/themes/unikitty.toml +1 -1
  148. flock/themes/urple.toml +1 -1
  149. flock/themes/vesper.toml +1 -1
  150. flock/themes/vimbones.toml +1 -1
  151. flock/themes/wildcherry.toml +1 -1
  152. flock/themes/wilmersdorf.toml +1 -1
  153. flock/themes/wryan.toml +1 -1
  154. flock/themes/xcodedarkhc.toml +1 -1
  155. flock/themes/xcodelight.toml +1 -1
  156. flock/themes/zenbones-light.toml +1 -1
  157. flock/themes/zenwritten-dark.toml +1 -1
  158. flock/utilities.py +301 -0
  159. flock/{components/utility → utility}/output_utility_component.py +68 -53
  160. flock/visibility.py +107 -0
  161. flock_core-0.5.56b0.dist-info/METADATA +747 -0
  162. flock_core-0.5.56b0.dist-info/RECORD +398 -0
  163. flock_core-0.5.56b0.dist-info/entry_points.txt +2 -0
  164. {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/licenses/LICENSE +1 -1
  165. flock/adapter/__init__.py +0 -14
  166. flock/adapter/azure_adapter.py +0 -68
  167. flock/adapter/chroma_adapter.py +0 -73
  168. flock/adapter/faiss_adapter.py +0 -97
  169. flock/adapter/pinecone_adapter.py +0 -51
  170. flock/adapter/vector_base.py +0 -47
  171. flock/cli/assets/release_notes.md +0 -140
  172. flock/cli/config.py +0 -8
  173. flock/cli/constants.py +0 -36
  174. flock/cli/create_agent.py +0 -1
  175. flock/cli/create_flock.py +0 -280
  176. flock/cli/execute_flock.py +0 -620
  177. flock/cli/load_agent.py +0 -1
  178. flock/cli/load_examples.py +0 -1
  179. flock/cli/load_flock.py +0 -192
  180. flock/cli/load_release_notes.py +0 -20
  181. flock/cli/loaded_flock_cli.py +0 -254
  182. flock/cli/manage_agents.py +0 -459
  183. flock/cli/registry_management.py +0 -889
  184. flock/cli/runner.py +0 -41
  185. flock/cli/settings.py +0 -857
  186. flock/cli/utils.py +0 -135
  187. flock/cli/view_results.py +0 -29
  188. flock/cli/yaml_editor.py +0 -396
  189. flock/components/__init__.py +0 -30
  190. flock/components/evaluation/__init__.py +0 -9
  191. flock/components/evaluation/declarative_evaluation_component.py +0 -606
  192. flock/components/routing/__init__.py +0 -15
  193. flock/components/routing/conditional_routing_component.py +0 -494
  194. flock/components/routing/default_routing_component.py +0 -103
  195. flock/components/routing/llm_routing_component.py +0 -206
  196. flock/components/utility/__init__.py +0 -22
  197. flock/components/utility/example_utility_component.py +0 -250
  198. flock/components/utility/feedback_utility_component.py +0 -206
  199. flock/components/utility/memory_utility_component.py +0 -550
  200. flock/components/utility/metrics_utility_component.py +0 -700
  201. flock/config.py +0 -61
  202. flock/core/__init__.py +0 -110
  203. flock/core/agent/__init__.py +0 -16
  204. flock/core/agent/default_agent.py +0 -216
  205. flock/core/agent/flock_agent_components.py +0 -104
  206. flock/core/agent/flock_agent_execution.py +0 -101
  207. flock/core/agent/flock_agent_integration.py +0 -260
  208. flock/core/agent/flock_agent_lifecycle.py +0 -186
  209. flock/core/agent/flock_agent_serialization.py +0 -381
  210. flock/core/api/__init__.py +0 -10
  211. flock/core/api/custom_endpoint.py +0 -45
  212. flock/core/api/endpoints.py +0 -254
  213. flock/core/api/main.py +0 -162
  214. flock/core/api/models.py +0 -97
  215. flock/core/api/run_store.py +0 -224
  216. flock/core/api/runner.py +0 -44
  217. flock/core/api/service.py +0 -214
  218. flock/core/component/__init__.py +0 -15
  219. flock/core/component/agent_component_base.py +0 -309
  220. flock/core/component/evaluation_component.py +0 -62
  221. flock/core/component/routing_component.py +0 -74
  222. flock/core/component/utility_component.py +0 -69
  223. flock/core/config/flock_agent_config.py +0 -58
  224. flock/core/config/scheduled_agent_config.py +0 -40
  225. flock/core/context/context.py +0 -213
  226. flock/core/context/context_manager.py +0 -37
  227. flock/core/context/context_vars.py +0 -10
  228. flock/core/evaluation/utils.py +0 -396
  229. flock/core/execution/batch_executor.py +0 -369
  230. flock/core/execution/evaluation_executor.py +0 -438
  231. flock/core/execution/local_executor.py +0 -31
  232. flock/core/execution/opik_executor.py +0 -103
  233. flock/core/execution/temporal_executor.py +0 -164
  234. flock/core/flock.py +0 -634
  235. flock/core/flock_agent.py +0 -336
  236. flock/core/flock_factory.py +0 -613
  237. flock/core/flock_scheduler.py +0 -166
  238. flock/core/flock_server_manager.py +0 -136
  239. flock/core/interpreter/python_interpreter.py +0 -689
  240. flock/core/mcp/__init__.py +0 -1
  241. flock/core/mcp/flock_mcp_server.py +0 -680
  242. flock/core/mcp/mcp_client_manager.py +0 -201
  243. flock/core/mcp/types/__init__.py +0 -1
  244. flock/core/mixin/dspy_integration.py +0 -403
  245. flock/core/mixin/prompt_parser.py +0 -125
  246. flock/core/orchestration/__init__.py +0 -15
  247. flock/core/orchestration/flock_batch_processor.py +0 -94
  248. flock/core/orchestration/flock_evaluator.py +0 -113
  249. flock/core/orchestration/flock_execution.py +0 -295
  250. flock/core/orchestration/flock_initialization.py +0 -149
  251. flock/core/orchestration/flock_server_manager.py +0 -67
  252. flock/core/orchestration/flock_web_server.py +0 -117
  253. flock/core/registry/__init__.py +0 -45
  254. flock/core/registry/agent_registry.py +0 -69
  255. flock/core/registry/callable_registry.py +0 -139
  256. flock/core/registry/component_discovery.py +0 -142
  257. flock/core/registry/component_registry.py +0 -64
  258. flock/core/registry/config_mapping.py +0 -64
  259. flock/core/registry/decorators.py +0 -137
  260. flock/core/registry/registry_hub.py +0 -205
  261. flock/core/registry/server_registry.py +0 -57
  262. flock/core/registry/type_registry.py +0 -86
  263. flock/core/serialization/__init__.py +0 -13
  264. flock/core/serialization/callable_registry.py +0 -52
  265. flock/core/serialization/flock_serializer.py +0 -832
  266. flock/core/serialization/json_encoder.py +0 -41
  267. flock/core/serialization/secure_serializer.py +0 -175
  268. flock/core/serialization/serializable.py +0 -342
  269. flock/core/serialization/serialization_utils.py +0 -412
  270. flock/core/util/file_path_utils.py +0 -223
  271. flock/core/util/hydrator.py +0 -309
  272. flock/core/util/input_resolver.py +0 -164
  273. flock/core/util/loader.py +0 -59
  274. flock/core/util/splitter.py +0 -219
  275. flock/di.py +0 -27
  276. flock/platform/docker_tools.py +0 -49
  277. flock/platform/jaeger_install.py +0 -86
  278. flock/webapp/__init__.py +0 -1
  279. flock/webapp/app/__init__.py +0 -0
  280. flock/webapp/app/api/__init__.py +0 -0
  281. flock/webapp/app/api/agent_management.py +0 -241
  282. flock/webapp/app/api/execution.py +0 -709
  283. flock/webapp/app/api/flock_management.py +0 -129
  284. flock/webapp/app/api/registry_viewer.py +0 -30
  285. flock/webapp/app/chat.py +0 -665
  286. flock/webapp/app/config.py +0 -104
  287. flock/webapp/app/dependencies.py +0 -117
  288. flock/webapp/app/main.py +0 -1070
  289. flock/webapp/app/middleware.py +0 -113
  290. flock/webapp/app/models_ui.py +0 -7
  291. flock/webapp/app/services/__init__.py +0 -0
  292. flock/webapp/app/services/feedback_file_service.py +0 -363
  293. flock/webapp/app/services/flock_service.py +0 -337
  294. flock/webapp/app/services/sharing_models.py +0 -81
  295. flock/webapp/app/services/sharing_store.py +0 -762
  296. flock/webapp/app/templates/theme_mapper.html +0 -326
  297. flock/webapp/app/theme_mapper.py +0 -812
  298. flock/webapp/app/utils.py +0 -85
  299. flock/webapp/run.py +0 -215
  300. flock/webapp/static/css/chat.css +0 -301
  301. flock/webapp/static/css/components.css +0 -167
  302. flock/webapp/static/css/header.css +0 -39
  303. flock/webapp/static/css/layout.css +0 -46
  304. flock/webapp/static/css/sidebar.css +0 -127
  305. flock/webapp/static/css/two-pane.css +0 -48
  306. flock/webapp/templates/base.html +0 -200
  307. flock/webapp/templates/chat.html +0 -152
  308. flock/webapp/templates/chat_settings.html +0 -19
  309. flock/webapp/templates/flock_editor.html +0 -16
  310. flock/webapp/templates/index.html +0 -12
  311. flock/webapp/templates/partials/_agent_detail_form.html +0 -93
  312. flock/webapp/templates/partials/_agent_list.html +0 -18
  313. flock/webapp/templates/partials/_agent_manager_view.html +0 -51
  314. flock/webapp/templates/partials/_agent_tools_checklist.html +0 -14
  315. flock/webapp/templates/partials/_chat_container.html +0 -15
  316. flock/webapp/templates/partials/_chat_messages.html +0 -57
  317. flock/webapp/templates/partials/_chat_settings_form.html +0 -85
  318. flock/webapp/templates/partials/_create_flock_form.html +0 -50
  319. flock/webapp/templates/partials/_dashboard_flock_detail.html +0 -17
  320. flock/webapp/templates/partials/_dashboard_flock_file_list.html +0 -16
  321. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +0 -28
  322. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +0 -16
  323. flock/webapp/templates/partials/_dynamic_input_form_content.html +0 -22
  324. flock/webapp/templates/partials/_env_vars_table.html +0 -23
  325. flock/webapp/templates/partials/_execution_form.html +0 -118
  326. flock/webapp/templates/partials/_execution_view_container.html +0 -28
  327. flock/webapp/templates/partials/_flock_file_list.html +0 -23
  328. flock/webapp/templates/partials/_flock_properties_form.html +0 -52
  329. flock/webapp/templates/partials/_flock_upload_form.html +0 -16
  330. flock/webapp/templates/partials/_header_flock_status.html +0 -5
  331. flock/webapp/templates/partials/_load_manager_view.html +0 -49
  332. flock/webapp/templates/partials/_registry_table.html +0 -25
  333. flock/webapp/templates/partials/_registry_viewer_content.html +0 -70
  334. flock/webapp/templates/partials/_results_display.html +0 -78
  335. flock/webapp/templates/partials/_settings_env_content.html +0 -9
  336. flock/webapp/templates/partials/_settings_theme_content.html +0 -14
  337. flock/webapp/templates/partials/_settings_view.html +0 -36
  338. flock/webapp/templates/partials/_share_chat_link_snippet.html +0 -11
  339. flock/webapp/templates/partials/_share_link_snippet.html +0 -35
  340. flock/webapp/templates/partials/_sidebar.html +0 -74
  341. flock/webapp/templates/partials/_streaming_results_container.html +0 -195
  342. flock/webapp/templates/partials/_structured_data_view.html +0 -40
  343. flock/webapp/templates/partials/_theme_preview.html +0 -36
  344. flock/webapp/templates/registry_viewer.html +0 -84
  345. flock/webapp/templates/shared_run_page.html +0 -140
  346. flock/workflow/__init__.py +0 -0
  347. flock/workflow/activities.py +0 -196
  348. flock/workflow/agent_activities.py +0 -24
  349. flock/workflow/agent_execution_activity.py +0 -202
  350. flock/workflow/flock_workflow.py +0 -214
  351. flock/workflow/temporal_config.py +0 -96
  352. flock/workflow/temporal_setup.py +0 -68
  353. flock_core-0.5.0b28.dist-info/METADATA +0 -274
  354. flock_core-0.5.0b28.dist-info/RECORD +0 -561
  355. flock_core-0.5.0b28.dist-info/entry_points.txt +0 -2
  356. /flock/{core/logging → logging}/formatters/themes.py +0 -0
  357. /flock/{core/logging → logging}/span_middleware/baggage_span_processor.py +0 -0
  358. /flock/{core/mcp → mcp}/util/__init__.py +0 -0
  359. {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/WHEEL +0 -0
flock/webapp/app/main.py DELETED
@@ -1,1070 +0,0 @@
1
- # src/flock/webapp/app/main.py
2
- import asyncio
3
- import json
4
- import os # Added import
5
- import shutil
6
-
7
- # Added for share link creation
8
- import uuid
9
- from contextlib import asynccontextmanager
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- import markdown2 # Import markdown2
14
- from fastapi import (
15
- Depends,
16
- FastAPI,
17
- File,
18
- Form,
19
- HTTPException,
20
- Query,
21
- Request,
22
- UploadFile,
23
- )
24
- from fastapi.responses import HTMLResponse, RedirectResponse
25
- from fastapi.staticfiles import StaticFiles
26
- from fastapi.templating import Jinja2Templates
27
- from pydantic import BaseModel
28
-
29
- from flock.core.api.endpoints import create_api_router
30
- from flock.core.api.run_store import RunStore
31
-
32
- # Import core Flock components and API related modules
33
- from flock.core.flock import Flock # For type hinting
34
- from flock.core.flock_scheduler import FlockScheduler
35
- from flock.core.logging.logging import get_logger # For logging
36
- from flock.core.util.splitter import parse_schema
37
-
38
- # Import UI-specific routers
39
- from flock.webapp.app.api import (
40
- agent_management,
41
- execution,
42
- flock_management,
43
- registry_viewer,
44
- )
45
- from flock.webapp.app.config import (
46
- DEFAULT_THEME_NAME,
47
- FLOCK_FILES_DIR,
48
- THEMES_DIR,
49
- get_current_theme_name,
50
- )
51
-
52
- # Import dependency management and config
53
- from flock.webapp.app.dependencies import (
54
- get_pending_custom_endpoints_and_clear,
55
- get_shared_link_store,
56
- set_global_flock_services,
57
- set_global_shared_link_store,
58
- )
59
-
60
- # Import service functions (which now expect app_state)
61
- from flock.webapp.app.middleware import ProxyHeadersMiddleware
62
- from flock.webapp.app.services.flock_service import (
63
- clear_current_flock_service,
64
- create_new_flock_service,
65
- get_available_flock_files,
66
- get_flock_preview_service,
67
- load_flock_from_file_service,
68
- # Note: get_current_flock_instance/filename are removed from service,
69
- # as main.py will use request.app.state for this.
70
- )
71
-
72
- # Added for share link creation
73
- from flock.webapp.app.services.sharing_models import SharedLinkConfig
74
- from flock.webapp.app.services.sharing_store import (
75
- SharedLinkStoreInterface,
76
- create_shared_link_store,
77
- )
78
- from flock.webapp.app.theme_mapper import alacritty_to_pico
79
-
80
- logger = get_logger("webapp.main")
81
-
82
-
83
- try:
84
- from flock.core.logging.formatters.themed_formatter import (
85
- load_theme_from_file,
86
- )
87
- THEME_LOADER_AVAILABLE = True
88
- except ImportError:
89
- logger.warning("Could not import flock.core theme loading utilities.")
90
- THEME_LOADER_AVAILABLE = False
91
-
92
- # --- .env helpers (copied from original main.py for self-containment) ---
93
- ENV_FILE_PATH = Path(".env") #Path(os.getenv("FLOCK_WEB_ENV_FILE", Path.home() / ".flock" / ".env"))
94
- #ENV_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
95
- SHOW_SECRETS_KEY = "SHOW_SECRETS"
96
-
97
- def load_env_file_web() -> dict[str, str]:
98
- env_vars: dict[str, str] = {}
99
- if not ENV_FILE_PATH.exists(): return env_vars
100
- with open(ENV_FILE_PATH) as f: lines = f.readlines()
101
- for line in lines:
102
- line = line.strip()
103
- if not line: env_vars[""] = ""; continue
104
- if line.startswith("#"): env_vars[line] = ""; continue
105
- if "=" in line: k, v = line.split("=", 1); env_vars[k] = v
106
- else: env_vars[line] = ""
107
- return env_vars
108
-
109
- def save_env_file_web(env_vars: dict[str, str]):
110
- try:
111
- with open(ENV_FILE_PATH, "w") as f:
112
- for k, v in env_vars.items():
113
- if k.startswith("#"): f.write(f"{k}\n")
114
- elif not k: f.write("\n")
115
- else: f.write(f"{k}={v}\n")
116
- except Exception as e: logger.error(f"[Settings] Failed to save .env: {e}")
117
-
118
- def is_sensitive_web(key: str) -> bool:
119
- patterns = ["key", "token", "secret", "password", "api", "pat"]; low = key.lower()
120
- return any(p in low for p in patterns)
121
-
122
- def mask_sensitive_value_web(value: str) -> str:
123
- if not value: return value
124
- if len(value) <= 4: return "••••"
125
- return value[:2] + "•" * (len(value) - 4) + value[-2:]
126
-
127
- def create_hx_trigger_header(triggers: dict[str, Any]) -> str:
128
- """Helper function to create HX-Trigger header with JSON serialization."""
129
- return json.dumps(triggers)
130
-
131
- def get_show_secrets_setting_web(env_vars: dict[str, str]) -> bool:
132
- return env_vars.get(SHOW_SECRETS_KEY, "false").lower() == "true"
133
-
134
- def set_show_secrets_setting_web(show: bool):
135
- env_vars = load_env_file_web()
136
- env_vars[SHOW_SECRETS_KEY] = str(show)
137
- save_env_file_web(env_vars)
138
- # --- End .env helpers ---
139
-
140
-
141
- @asynccontextmanager
142
- async def lifespan(app: FastAPI):
143
- logger.info("FastAPI application starting up...")
144
- # Flock instance and RunStore are expected to be set on app.state
145
- # by `start_unified_server` in `webapp/run.py` *before* uvicorn starts the app.
146
- # The call to `set_global_flock_services` also happens there. # Initialize and set the SharedLinkStore
147
- try:
148
- logger.info("Initializing SharedLinkStore using factory...")
149
- shared_link_store = create_shared_link_store()
150
- await shared_link_store.initialize() # Create tables if they don't exist
151
- set_global_shared_link_store(shared_link_store)
152
- logger.info("SharedLinkStore initialized and set globally.")
153
- except Exception as e:
154
- logger.error(f"Failed to initialize SharedLinkStore: {e}", exc_info=True)# Configure chat features with clear precedence:
155
- # 1. Value set by start_unified_server (programmatic)
156
- # 2. Environment variables (standalone mode)
157
- programmatic_chat_enabled = getattr(app.state, "chat_enabled", None)
158
- env_start_mode = os.environ.get("FLOCK_START_MODE")
159
- env_chat_enabled = os.environ.get("FLOCK_CHAT_ENABLED", "false").lower() == "true"
160
-
161
- if programmatic_chat_enabled is not None:
162
- # Programmatic setting takes precedence (from start_unified_server)
163
- should_enable_chat_routes = programmatic_chat_enabled
164
- logger.info(f"Using programmatic chat_enabled setting: {should_enable_chat_routes}")
165
- elif env_start_mode == "chat":
166
- should_enable_chat_routes = True
167
- app.state.initial_redirect_to_chat = True
168
- app.state.chat_enabled = True
169
- logger.info("FLOCK_START_MODE='chat'. Enabling chat routes and setting redirect.")
170
- elif env_chat_enabled:
171
- should_enable_chat_routes = True
172
- app.state.chat_enabled = True
173
- logger.info("FLOCK_CHAT_ENABLED='true'. Enabling chat routes.")
174
- else:
175
- should_enable_chat_routes = False
176
- app.state.chat_enabled = False
177
- logger.info("Chat routes disabled (no programmatic or environment setting).")
178
-
179
- if should_enable_chat_routes:
180
- try:
181
- from flock.webapp.app.chat import router as chat_router
182
- app.include_router(chat_router, tags=["Chat"])
183
- logger.info("Chat routes included in the application.")
184
- except Exception as e:
185
- logger.error(f"Failed to include chat routes during lifespan startup: {e}", exc_info=True) # If in standalone chat mode, strip non-essential UI routes
186
- if env_start_mode == "chat":
187
- from fastapi.routing import APIRoute
188
- logger.info("FLOCK_START_MODE='chat'. Stripping non-chat UI routes.")
189
-
190
- # Define tags for routes to KEEP.
191
- # "Chat" for primary chat functionality.
192
- # "Chat Sharing" for shared chat links & pages.
193
- # API tags might be needed if chat agents make internal API calls or for general health/docs.
194
- # Public static files (/static/...) are typically handled by app.mount and not in app.router.routes directly this way.
195
- allowed_tags_for_chat_mode = {
196
- "Chat",
197
- "Chat Sharing",
198
- "Flock API Core", # Keep core API for potential underlying needs
199
- "Flock API Custom Endpoints" # Keep custom API endpoints
200
- }
201
-
202
- def _route_is_allowed_in_chat_mode(route: APIRoute) -> bool:
203
- # Keep documentation (e.g. /docs, /openapi.json - usually no tags or specific tags)
204
- # and non-API utility routes (often no tags).
205
- if not hasattr(route, "tags") or not route.tags:
206
- # Check common doc paths explicitly as they might not have tags or might have default tags
207
- if route.path in ["/docs", "/openapi.json", "/redoc"]:
208
- return True
209
- # Allow other untagged routes for now, assuming they are essential (e.g. static mounts if they appeared here)
210
- # This might need refinement if untagged UI routes exist.
211
- return True
212
- return any(tag in allowed_tags_for_chat_mode for tag in route.tags)
213
-
214
- original_route_count = len(app.router.routes)
215
- app.router.routes = [r for r in app.router.routes if _route_is_allowed_in_chat_mode(r)]
216
- num_removed = original_route_count - len(app.router.routes)
217
- logger.info(f"Stripped {num_removed} routes for chat-only mode. {len(app.router.routes)} routes remaining.")
218
-
219
- if num_removed > 0 and hasattr(app, "openapi_schema"):
220
- app.openapi_schema = None # Clear cached OpenAPI schema to regenerate
221
- logger.info("Cleared OpenAPI schema cache due to route removal.")
222
-
223
- # Add custom routes if any were passed during server startup
224
- # These are retrieved from the dependency module where `start_unified_server` stored them.
225
- pending_endpoints = get_pending_custom_endpoints_and_clear()
226
- if pending_endpoints:
227
- flock_instance_from_state: Flock | None = getattr(app.state, "flock_instance", None)
228
- if flock_instance_from_state:
229
- from flock.core.api.main import (
230
- FlockAPI, # Local import for this specific task
231
- )
232
- # Create a temporary FlockAPI service object just for adding routes
233
- temp_flock_api_service = FlockAPI(
234
- flock_instance_from_state,
235
- custom_endpoints=pending_endpoints
236
- )
237
- temp_flock_api_service.add_custom_routes_to_app(app)
238
- logger.info(f"Lifespan: Added {len(pending_endpoints)} custom API routes to main app.")
239
- else:
240
- logger.warning("Lifespan: Pending custom endpoints found, but no Flock instance in app.state. Cannot add custom routes.")
241
-
242
- # --- Add Scheduler Startup Logic ---
243
- flock_instance_from_state: Flock | None = getattr(app.state, "flock_instance", None)
244
- if flock_instance_from_state:
245
- # Create and start the scheduler
246
- scheduler = FlockScheduler(flock_instance_from_state)
247
- app.state.flock_scheduler = scheduler # Store for access during shutdown
248
-
249
- scheduler_loop_task = await scheduler.start() # Start returns the task
250
- if scheduler_loop_task:
251
- app.state.flock_scheduler_task = scheduler_loop_task # Store the task
252
- logger.info("FlockScheduler background task started.")
253
- else:
254
- app.state.flock_scheduler_task = None
255
- logger.info("FlockScheduler initialized, but no scheduled agents found or loop not started.")
256
- else:
257
- app.state.flock_scheduler = None
258
- app.state.flock_scheduler_task = None
259
- logger.warning("No Flock instance found in app.state; FlockScheduler not started.")
260
- # --- End Scheduler Startup Logic ---
261
-
262
- yield
263
- logger.info("FastAPI application shutting down...")
264
-
265
- # --- Add Scheduler Shutdown Logic ---
266
- logger.info("FastAPI application initiating shutdown...")
267
- scheduler_to_stop: FlockScheduler | None = getattr(app.state, "flock_scheduler", None)
268
- scheduler_task_to_await: asyncio.Task | None = getattr(app.state, "flock_scheduler_task", None)
269
-
270
- if scheduler_to_stop:
271
- logger.info("Attempting to stop FlockScheduler...")
272
- await scheduler_to_stop.stop() # Signal the scheduler loop to stop
273
-
274
- if scheduler_task_to_await and not scheduler_task_to_await.done():
275
- logger.info("Waiting for FlockScheduler task to complete...")
276
- try:
277
- await asyncio.wait_for(scheduler_task_to_await, timeout=10.0) # Wait for graceful exit
278
- logger.info("FlockScheduler task completed gracefully.")
279
- except asyncio.TimeoutError:
280
- logger.warning("FlockScheduler task did not complete in time, cancelling.")
281
- scheduler_task_to_await.cancel()
282
- try:
283
- await scheduler_task_to_await # Await cancellation
284
- except asyncio.CancelledError:
285
- logger.info("FlockScheduler task cancelled.")
286
- except Exception as e:
287
- logger.error(f"Error during FlockScheduler task finalization: {e}", exc_info=True)
288
- elif scheduler_task_to_await and scheduler_task_to_await.done():
289
- logger.info("FlockScheduler task was already done.")
290
- else:
291
- logger.info("FlockScheduler instance found, but no running task was stored to await.")
292
- else:
293
- logger.info("No active FlockScheduler found to stop.")
294
-
295
- logger.info("FastAPI application finished shutdown sequence.")
296
- # --- End Scheduler Shutdown Logic ---
297
-
298
- app = FastAPI(title="Flock Web UI & API", lifespan=lifespan, docs_url="/docs",
299
- openapi_url="/openapi.json", root_path=os.getenv("FLOCK_ROOT_PATH", ""))
300
-
301
- # Add middleware for handling proxy headers (HTTPS detection)
302
- # You can force HTTPS by setting FLOCK_FORCE_HTTPS=true
303
- force_https = os.getenv("FLOCK_FORCE_HTTPS", "false").lower() == "true"
304
- app.add_middleware(ProxyHeadersMiddleware, force_https=force_https)
305
- logger.info(f"FastAPI booting complete with proxy headers middleware (force_https={force_https}).")
306
-
307
- BASE_DIR = Path(__file__).resolve().parent.parent
308
- app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
309
- templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
310
-
311
- # Add markdown2 filter to Jinja2 environment
312
- def markdown_filter(text):
313
- return markdown2.markdown(text, extras=["tables", "fenced-code-blocks"])
314
-
315
- templates.env.filters['markdown'] = markdown_filter
316
-
317
- core_api_router = create_api_router()
318
- app.include_router(core_api_router, prefix="/api", tags=["Flock API Core"])
319
- app.include_router(flock_management.router, prefix="/ui/api/flock", tags=["UI Flock Management"])
320
- app.include_router(agent_management.router, prefix="/ui/api/flock", tags=["UI Agent Management"])
321
- app.include_router(execution.router, prefix="/ui/api/flock", tags=["UI Execution"])
322
- app.include_router(registry_viewer.router, prefix="/ui/api/registry", tags=["UI Registry"])
323
-
324
- # --- Share Link API Models and Endpoint ---
325
- class CreateShareLinkRequest(BaseModel):
326
- agent_name: str
327
-
328
- class CreateShareLinkResponse(BaseModel):
329
- share_url: str
330
-
331
- @app.post("/api/v1/share/link", response_model=CreateShareLinkResponse, tags=["UI Sharing"])
332
- async def create_share_link(
333
- request: Request,
334
- request_data: CreateShareLinkRequest,
335
- store: SharedLinkStoreInterface = Depends(get_shared_link_store)
336
- ):
337
- """Creates a new shareable link for an agent."""
338
- share_id = uuid.uuid4().hex
339
- agent_name = request_data.agent_name
340
-
341
- if not agent_name: # Basic validation
342
- raise HTTPException(status_code=400, detail="Agent name cannot be empty.")
343
-
344
- current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
345
- current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
346
-
347
- if not current_flock_instance or not current_flock_filename:
348
- logger.error("Cannot create share link: No Flock is currently loaded in the application state.")
349
- raise HTTPException(status_code=400, detail="No Flock loaded. Cannot create share link.")
350
-
351
- if agent_name not in current_flock_instance.agents:
352
- logger.error(f"Agent '{agent_name}' not found in currently loaded Flock '{current_flock_instance.name}'.")
353
- raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found in current Flock.")
354
-
355
- try:
356
- flock_file_path = FLOCK_FILES_DIR / current_flock_filename
357
- if not flock_file_path.is_file():
358
- logger.warning(f"Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
359
- flock_definition_str = current_flock_instance.to_yaml()
360
- else:
361
- flock_definition_str = flock_file_path.read_text()
362
- except Exception as e:
363
- logger.error(f"Failed to get flock definition for sharing: {e}", exc_info=True)
364
- raise HTTPException(status_code=500, detail="Could not retrieve Flock definition for sharing.")
365
-
366
- config = SharedLinkConfig(
367
- share_id=share_id,
368
- agent_name=agent_name,
369
- flock_definition=flock_definition_str
370
- )
371
- try:
372
- await store.save_config(config)
373
- share_url = f"/ui/shared-run/{share_id}" # Relative URL for client-side navigation
374
- logger.info(f"Created share link for agent '{agent_name}' in Flock '{current_flock_instance.name}' with ID '{share_id}'. URL: {share_url}")
375
- return CreateShareLinkResponse(share_url=share_url)
376
- except Exception as e:
377
- logger.error(f"Failed to create share link for agent '{agent_name}': {e}", exc_info=True)
378
- raise HTTPException(status_code=500, detail=f"Failed to create share link: {e!s}")
379
-
380
- # --- End Share Link API ---
381
-
382
- # --- HTMX Endpoint for Generating Share Link Snippet ---
383
- @app.post("/ui/htmx/share/generate-link", response_class=HTMLResponse, tags=["UI Sharing HTMX"])
384
- async def htmx_generate_share_link(
385
- request: Request,
386
- start_agent_name: str | None = Form(None),
387
- store: SharedLinkStoreInterface = Depends(get_shared_link_store)
388
- ):
389
- if not start_agent_name:
390
- logger.warning("HTMX generate share link: Agent name not provided.")
391
- return templates.TemplateResponse(
392
- "partials/_share_link_snippet.html",
393
- {"request": request, "error_message": "No agent selected to share."}
394
- )
395
-
396
- current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
397
- current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
398
-
399
- if not current_flock_instance or not current_flock_filename:
400
- logger.error("HTMX: Cannot create share link: No Flock is currently loaded.")
401
- return templates.TemplateResponse(
402
- "partials/_share_link_snippet.html",
403
- {"request": request, "error_message": "No Flock loaded. Cannot create share link."}
404
- )
405
-
406
- if start_agent_name not in current_flock_instance.agents:
407
- logger.error(f"HTMX: Agent '{start_agent_name}' not found in Flock '{current_flock_instance.name}'.")
408
- return templates.TemplateResponse(
409
- "partials/_share_link_snippet.html",
410
- {"request": request, "error_message": f"Agent '{start_agent_name}' not found in current Flock."}
411
- )
412
-
413
- try:
414
- flock_file_path = FLOCK_FILES_DIR / current_flock_filename
415
- if not flock_file_path.is_file():
416
- logger.warning(f"HTMX: Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
417
- flock_definition_str = current_flock_instance.to_yaml()
418
- else:
419
- flock_definition_str = flock_file_path.read_text()
420
- except Exception as e:
421
- logger.error(f"HTMX: Failed to get flock definition for sharing: {e}", exc_info=True)
422
- return templates.TemplateResponse(
423
- "partials/_share_link_snippet.html",
424
- {"request": request, "error_message": "Could not retrieve Flock definition for sharing."}
425
- )
426
-
427
- share_id = uuid.uuid4().hex
428
- config = SharedLinkConfig(
429
- share_id=share_id,
430
- agent_name=start_agent_name,
431
- flock_definition=flock_definition_str
432
- )
433
-
434
- try:
435
- await store.save_config(config)
436
- base_url = str(request.base_url)
437
- full_share_url = f"{base_url.rstrip('/')}/ui/shared-run/{share_id}"
438
-
439
- logger.info(f"HTMX: Generated share link for agent '{start_agent_name}' in Flock '{current_flock_instance.name}' with ID '{share_id}'. URL: {full_share_url}")
440
- return templates.TemplateResponse(
441
- "partials/_share_link_snippet.html",
442
- {"request": request, "share_url": full_share_url, "flock_name": current_flock_instance.name, "agent_name": start_agent_name}
443
- )
444
- except Exception as e:
445
- logger.error(f"HTMX: Failed to create share link for agent '{start_agent_name}': {e}", exc_info=True)
446
- return templates.TemplateResponse(
447
- "partials/_share_link_snippet.html",
448
- {"request": request, "error_message": f"Could not generate link: {e!s}"}
449
- )
450
- # --- End HTMX Endpoint ---
451
-
452
- # --- HTMX Endpoint for Generating SHARED CHAT Link Snippet ---
453
- @app.post("/ui/htmx/share/chat/generate-link", response_class=HTMLResponse, tags=["UI Sharing HTMX"])
454
- async def htmx_generate_share_chat_link(
455
- request: Request,
456
- agent_name: str | None = Form(None), # This is the chat agent
457
- message_key: str | None = Form(None), # Changed default to None
458
- history_key: str | None = Form(None), # Changed default to None
459
- response_key: str | None = Form(None), # Changed default to None
460
- store: SharedLinkStoreInterface = Depends(get_shared_link_store)
461
- ):
462
- if not agent_name:
463
- logger.warning("HTMX generate share chat link: Agent name not provided.")
464
- return templates.TemplateResponse(
465
- "partials/_share_chat_link_snippet.html", # Will create this template
466
- {"request": request, "error_message": "No agent selected for chat sharing."}
467
- )
468
-
469
- current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
470
- current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
471
-
472
- if not current_flock_instance or not current_flock_filename:
473
- logger.error("HTMX Chat Share: Cannot create share link: No Flock is currently loaded.")
474
- return templates.TemplateResponse(
475
- "partials/_share_chat_link_snippet.html",
476
- {"request": request, "error_message": "No Flock loaded. Cannot create share link."}
477
- )
478
-
479
- if agent_name not in current_flock_instance.agents:
480
- logger.error(f"HTMX Chat Share: Agent '{agent_name}' not found in Flock '{current_flock_instance.name}'.")
481
- return templates.TemplateResponse(
482
- "partials/_share_chat_link_snippet.html",
483
- {"request": request, "error_message": f"Agent '{agent_name}' not found in current Flock."}
484
- )
485
-
486
- try:
487
- flock_file_path = FLOCK_FILES_DIR / current_flock_filename
488
- if not flock_file_path.is_file():
489
- logger.warning(f"HTMX Chat Share: Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
490
- flock_definition_str = current_flock_instance.to_yaml()
491
- else:
492
- flock_definition_str = flock_file_path.read_text()
493
- except Exception as e:
494
- logger.error(f"HTMX Chat Share: Failed to get flock definition for sharing: {e}", exc_info=True)
495
- return templates.TemplateResponse(
496
- "partials/_share_chat_link_snippet.html",
497
- {"request": request, "error_message": "Could not retrieve Flock definition for sharing."}
498
- )
499
-
500
- share_id = uuid.uuid4().hex
501
-
502
- # Explicitly convert empty strings from form to None for optional keys
503
- actual_message_key = message_key if message_key else None
504
- actual_history_key = history_key if history_key else None
505
- actual_response_key = response_key if response_key else None
506
-
507
- config = SharedLinkConfig(
508
- share_id=share_id,
509
- agent_name=agent_name, # agent_name from form is the chat agent
510
- flock_definition=flock_definition_str,
511
- share_type="chat",
512
- chat_message_key=actual_message_key,
513
- chat_history_key=actual_history_key,
514
- chat_response_key=actual_response_key
515
- )
516
-
517
- try:
518
- await store.save_config(config)
519
- base_url = str(request.base_url)
520
- # Link to the new /chat/shared/{share_id} endpoint
521
- full_share_url = f"{base_url.rstrip('/')}/chat/shared/{share_id}"
522
-
523
- logger.info(f"HTMX: Generated share CHAT link for agent '{agent_name}' in Flock '{current_flock_instance.name}' with ID '{share_id}'. URL: {full_share_url}")
524
- return templates.TemplateResponse(
525
- "partials/_share_chat_link_snippet.html", # Will create this template
526
- {"request": request, "share_url": full_share_url, "flock_name": current_flock_instance.name, "agent_name": agent_name}
527
- )
528
- except Exception as e:
529
- logger.error(f"HTMX Chat Share: Failed to create share link for agent '{agent_name}': {e}", exc_info=True)
530
- return templates.TemplateResponse(
531
- "partials/_share_chat_link_snippet.html",
532
- {"request": request, "error_message": f"Could not generate chat link: {e!s}"}
533
- )
534
-
535
- # --- Route for Shared Run Page ---
536
- @app.get("/ui/shared-run/{share_id}", response_class=HTMLResponse, tags=["UI Sharing"])
537
- async def page_shared_run(
538
- request: Request,
539
- share_id: str,
540
- store: SharedLinkStoreInterface = Depends(get_shared_link_store),
541
- ):
542
- logger.info(f"Accessed shared run page with share_id: {share_id}")
543
- shared_config = await store.get_config(share_id)
544
-
545
- if not shared_config:
546
- logger.warning(f"Share ID {share_id} not found.")
547
- return templates.TemplateResponse(
548
- "error_page.html",
549
- {"request": request, "error_title": "Link Not Found", "error_message": "The shared link does not exist or may have expired."},
550
- status_code=404
551
- )
552
-
553
- agent_name_from_link = shared_config.agent_name
554
- flock_definition_str = shared_config.flock_definition
555
- context: dict[str, Any] = {"request": request, "is_shared_run_page": True, "share_id": share_id}
556
-
557
- try:
558
- from flock.core.flock import Flock as ConcreteFlock
559
- loaded_flock = ConcreteFlock.from_yaml(flock_definition_str)
560
-
561
- # Store the loaded_flock instance in app.state for later retrieval
562
- if not hasattr(request.app.state, 'shared_flocks'):
563
- request.app.state.shared_flocks = {}
564
- request.app.state.shared_flocks[share_id] = loaded_flock
565
- logger.info(f"Shared Run Page: Stored Flock instance for share_id {share_id} in app.state.")
566
-
567
- context["flock"] = loaded_flock
568
- context["selected_agent_name"] = agent_name_from_link # For pre-selection & hidden field
569
- # flock_definition_str is no longer needed in the template for a hidden field if we reuse the instance
570
- # context["flock_definition_str"] = flock_definition_str
571
- logger.info(f"Shared Run Page: Loaded Flock '{loaded_flock.name}' for agent '{agent_name_from_link}'.")
572
-
573
- if agent_name_from_link not in loaded_flock.agents:
574
- context["error_message"] = f"Agent '{agent_name_from_link}' not found in the shared Flock definition."
575
- logger.warning(context["error_message"])
576
- else:
577
- agent = loaded_flock.agents[agent_name_from_link]
578
- input_fields = []
579
- if agent.input and isinstance(agent.input, str):
580
- try:
581
- parsed_spec = parse_schema(agent.input) # parse_schema is imported at top of main.py
582
- for name, type_str, description in parsed_spec:
583
- field_info = {"name": name, "type": type_str.lower(), "description": description or ""}
584
- if "bool" in field_info["type"]: field_info["html_type"] = "checkbox"
585
- elif "int" in field_info["type"] or "float" in field_info["type"]: field_info["html_type"] = "number"
586
- elif "list" in field_info["type"] or "dict" in field_info["type"]:
587
- field_info["html_type"] = "textarea"; field_info["placeholder"] = f"Enter JSON for {field_info['type']}"
588
- else: field_info["html_type"] = "text"
589
- input_fields.append(field_info)
590
- context["input_fields"] = input_fields
591
- except Exception as e_parse:
592
- logger.error(f"Shared Run Page: Error parsing input for '{agent_name_from_link}': {e_parse}", exc_info=True)
593
- context["error_message"] = f"Could not parse inputs for agent '{agent_name_from_link}'."
594
- else:
595
- context["input_fields"] = [] # Agent has no inputs defined
596
-
597
- except Exception as e_load:
598
- logger.error(f"Shared Run Page: Failed to load Flock from definition for share_id {share_id}: {e_load}", exc_info=True)
599
- context["error_message"] = f"Fatal: Could not load the shared Flock configuration: {e_load!s}"
600
- context["flock"] = None
601
- context["selected_agent_name"] = agent_name_from_link # Still pass for potential error display
602
- context["input_fields"] = []
603
- # context["flock_definition_str"] = flock_definition_str # Not needed if not sent to template
604
-
605
- try:
606
- current_theme_name = get_current_theme_name()
607
- context["theme_css"] = generate_theme_css_web(current_theme_name)
608
- context["active_theme_name"] = current_theme_name or DEFAULT_THEME_NAME
609
- except Exception as e_theme:
610
- logger.error(f"Shared Run Page: Error generating theme: {e_theme}", exc_info=True)
611
- context["theme_css"] = ""
612
- context["active_theme_name"] = DEFAULT_THEME_NAME
613
-
614
- # The shared_run_page.html will now be a simple wrapper that includes _execution_form.html
615
- return templates.TemplateResponse("shared_run_page.html", context)
616
-
617
- # --- End Route for Shared Run Page ---
618
-
619
- def generate_theme_css_web(theme_name: str | None) -> str:
620
- if not THEME_LOADER_AVAILABLE or THEMES_DIR is None: return ""
621
-
622
- chosen_theme_name_input = theme_name or get_current_theme_name() or DEFAULT_THEME_NAME
623
-
624
- # Sanitize the input to get only the filename component
625
- sanitized_name_part = Path(chosen_theme_name_input).name
626
- # Ensure we have a stem
627
- theme_stem_candidate = sanitized_name_part
628
- if theme_stem_candidate.endswith(".toml"):
629
- theme_stem_candidate = theme_stem_candidate[:-5]
630
-
631
- effective_theme_filename = f"{theme_stem_candidate}.toml"
632
- _theme_to_load_stem = theme_stem_candidate # This will be the name of the theme we attempt to load
633
-
634
- try:
635
- resolved_themes_dir = THEMES_DIR.resolve(strict=True) # Ensure THEMES_DIR itself is valid
636
- prospective_theme_path = resolved_themes_dir / effective_theme_filename
637
-
638
- # Resolve the prospective path
639
- resolved_theme_path = prospective_theme_path.resolve()
640
-
641
- # Validate:
642
- # 1. Path is still within the resolved THEMES_DIR
643
- # 2. The final filename component of the resolved path matches the intended filename
644
- # (guards against symlinks or normalization changing the name unexpectedly)
645
- # 3. The file exists
646
- if (
647
- str(resolved_theme_path).startswith(str(resolved_themes_dir)) and
648
- resolved_theme_path.name == effective_theme_filename and
649
- resolved_theme_path.is_file() # is_file checks existence too
650
- ):
651
- theme_path = resolved_theme_path
652
- else:
653
- logger.warning(
654
- f"Validation failed or theme '{effective_theme_filename}' not found in '{resolved_themes_dir}'. "
655
- f"Attempted path: '{prospective_theme_path}'. Resolved to: '{resolved_theme_path}'. "
656
- f"Falling back to default theme: {DEFAULT_THEME_NAME}.toml"
657
- )
658
- _theme_to_load_stem = DEFAULT_THEME_NAME
659
- theme_path = resolved_themes_dir / f"{DEFAULT_THEME_NAME}.toml"
660
- if not theme_path.is_file():
661
- logger.error(f"Default theme file '{theme_path}' not found. No theme CSS will be generated.")
662
- return ""
663
- except FileNotFoundError: # THEMES_DIR does not exist
664
- logger.error(f"Themes directory '{THEMES_DIR}' not found. Falling back to default theme.")
665
- _theme_to_load_stem = DEFAULT_THEME_NAME
666
- # Attempt to use a conceptual default path if THEMES_DIR was bogus, though it's unlikely to succeed
667
- theme_path = Path(f"{DEFAULT_THEME_NAME}.toml") # This won't be in THEMES_DIR if THEMES_DIR is bad
668
- if not theme_path.exists(): # Check existence without assuming a base directory
669
- logger.error(f"Default theme file '{DEFAULT_THEME_NAME}.toml' not found at root or THEMES_DIR is inaccessible. No theme CSS.")
670
- return ""
671
- except Exception as e:
672
- logger.error(f"Error during theme path resolution for '{effective_theme_filename}': {e}. Falling back to default.")
673
- _theme_to_load_stem = DEFAULT_THEME_NAME
674
- theme_path = THEMES_DIR / f"{DEFAULT_THEME_NAME}.toml" if THEMES_DIR else Path(f"{DEFAULT_THEME_NAME}.toml")
675
- if not theme_path.exists():
676
- logger.error(f"Default theme file '{theme_path}' not found after error. No theme CSS.")
677
- return ""
678
-
679
- try:
680
- theme_dict = load_theme_from_file(str(theme_path))
681
- logger.debug(f"Successfully loaded theme '{_theme_to_load_stem}' from '{theme_path}'")
682
- except Exception as e:
683
- logger.error(f"Error loading theme file '{theme_path}' (intended: '{_theme_to_load_stem}.toml'): {e}")
684
- return ""
685
-
686
- pico_vars = alacritty_to_pico(theme_dict)
687
- if not pico_vars: return ""
688
- css_rules = [f" {name}: {value};" for name, value in pico_vars.items()]
689
- css_string = ":root {\n" + "\n".join(css_rules) + "\n}"
690
- return css_string
691
-
692
- def get_base_context_web(
693
- request: Request, error: str = None, success: str = None, ui_mode: str = "standalone"
694
- ) -> dict:
695
- flock_instance_from_state: Flock | None = getattr(request.app.state, "flock_instance", None)
696
- current_flock_filename_from_state: str | None = getattr(request.app.state, "flock_filename", None)
697
- theme_name = get_current_theme_name()
698
- theme_css = generate_theme_css_web(theme_name)
699
-
700
- return {
701
- "request": request,
702
- "current_flock": flock_instance_from_state,
703
- "current_filename": current_flock_filename_from_state,
704
- "error_message": error,
705
- "success_message": success,
706
- "ui_mode": ui_mode,
707
- "theme_css": theme_css,
708
- "active_theme_name": theme_name,
709
- "chat_enabled": getattr(request.app.state, "chat_enabled", False), # Reverted to app.state
710
- }
711
-
712
- @app.get("/", response_class=HTMLResponse, tags=["UI Pages"])
713
- async def page_dashboard(
714
- request: Request, error: str = None, success: str = None, ui_mode: str = Query(None)
715
- ):
716
- # Handle initial redirect if flagged during app startup
717
- if getattr(request.app.state, "initial_redirect_to_chat", False):
718
- logger.info("Initial redirect to CHAT page triggered from dashboard (FLOCK_START_MODE='chat').")
719
- # Use url_for to respect the root_path setting
720
- chat_url = str(request.url_for("page_chat"))
721
- return RedirectResponse(url=chat_url, status_code=307)
722
-
723
- effective_ui_mode = ui_mode
724
- flock_is_preloaded = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
725
-
726
- if effective_ui_mode is None:
727
- effective_ui_mode = "scoped" if flock_is_preloaded else "standalone"
728
- if effective_ui_mode == "scoped":
729
- # Manually construct URL with root_path to ensure it works with proxy setups
730
- root_path = request.scope.get("root_path", "")
731
- redirect_url = f"{root_path}/?ui_mode=scoped&initial_load=true"
732
- logger.info(f"Dashboard redirect: {redirect_url} (root_path: '{root_path}')")
733
- return RedirectResponse(url=redirect_url, status_code=307)
734
-
735
- if effective_ui_mode == "standalone" and flock_is_preloaded:
736
- clear_current_flock_service(request.app.state) # Pass app.state
737
- logger.info("Switched to standalone mode, cleared preloaded Flock instance from app.state.")
738
-
739
- context = get_base_context_web(request, error, success, effective_ui_mode)
740
- flock_in_state = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
741
-
742
- if effective_ui_mode == "scoped":
743
- context["initial_content_url"] = str(request.url_for("htmx_get_execution_view_container")) if flock_in_state else str(request.url_for("htmx_scoped_no_flock_view"))
744
- else:
745
- context["initial_content_url"] = str(request.url_for("htmx_get_load_flock_view"))
746
- return templates.TemplateResponse("base.html", context)
747
-
748
- @app.get("/ui/editor/{section:path}", response_class=HTMLResponse, tags=["UI Pages"])
749
- async def page_editor_section(
750
- request: Request, section: str, success: str = None, error: str = None, ui_mode: str = Query("standalone")
751
- ):
752
- flock_instance_from_state: Flock | None = getattr(request.app.state, "flock_instance", None)
753
- if not flock_instance_from_state:
754
- err_msg = "No flock loaded. Please load or create a flock first."
755
- # Use url_for to respect the root_path setting
756
- redirect_url = str(request.url_for("page_dashboard").include_query_params(error=err_msg))
757
- if ui_mode == "scoped":
758
- redirect_url = str(request.url_for("page_dashboard").include_query_params(error=err_msg, ui_mode="scoped"))
759
- return RedirectResponse(url=redirect_url, status_code=303)
760
-
761
- context = get_base_context_web(request, error, success, ui_mode)
762
- root_path = request.scope.get("root_path", "")
763
- content_map = {
764
- "properties": f"{root_path}/ui/api/flock/htmx/flock-properties-form",
765
- "agents": f"{root_path}/ui/htmx/agent-manager-view",
766
- "execute": f"{root_path}/ui/htmx/execution-view-container"
767
- }
768
- context["initial_content_url"] = content_map.get(section, f"{root_path}/ui/htmx/load-flock-view")
769
- if section not in content_map: context["error_message"] = "Invalid editor section."
770
- return templates.TemplateResponse("base.html", context)
771
-
772
- @app.get("/ui/registry", response_class=HTMLResponse, tags=["UI Pages"])
773
- async def page_registry(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
774
- context = get_base_context_web(request, error, success, ui_mode)
775
- root_path = request.scope.get("root_path", "")
776
- context["initial_content_url"] = f"{root_path}/ui/htmx/registry-viewer"
777
- return templates.TemplateResponse("base.html", context)
778
-
779
- @app.get("/ui/create", response_class=HTMLResponse, tags=["UI Pages"])
780
- async def page_create(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
781
- clear_current_flock_service(request.app.state) # Pass app.state
782
- context = get_base_context_web(request, error, success, "standalone")
783
- root_path = request.scope.get("root_path", "")
784
- context["initial_content_url"] = f"{root_path}/ui/htmx/create-flock-form"
785
- return templates.TemplateResponse("base.html", context)
786
-
787
- @app.get("/ui/htmx/sidebar", response_class=HTMLResponse, tags=["UI HTMX Partials"])
788
- async def htmx_get_sidebar(request: Request, ui_mode: str = Query("standalone")):
789
- return templates.TemplateResponse("partials/_sidebar.html", get_base_context_web(request, ui_mode=ui_mode))
790
-
791
- @app.get("/ui/htmx/header-flock-status", response_class=HTMLResponse, tags=["UI HTMX Partials"])
792
- async def htmx_get_header_flock_status(request: Request, ui_mode: str = Query("standalone")):
793
- return templates.TemplateResponse("partials/_header_flock_status.html", get_base_context_web(request, ui_mode=ui_mode))
794
-
795
- @app.get("/ui/htmx/load-flock-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
796
- async def htmx_get_load_flock_view(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
797
- return templates.TemplateResponse("partials/_load_manager_view.html", get_base_context_web(request, error, success, ui_mode))
798
-
799
- @app.get("/ui/htmx/dashboard-flock-file-list", response_class=HTMLResponse, tags=["UI HTMX Partials"])
800
- async def htmx_get_dashboard_flock_file_list_partial(request: Request):
801
- return templates.TemplateResponse("partials/_dashboard_flock_file_list.html", {"request": request, "flock_files": get_available_flock_files()})
802
-
803
- @app.get("/ui/htmx/dashboard-default-action-pane", response_class=HTMLResponse, tags=["UI HTMX Partials"])
804
- async def htmx_get_dashboard_default_action_pane(request: Request):
805
- return HTMLResponse("""<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;"><p>Select a Flock from the list to view its details and load it into the editor.</p><hr><p>Or, create a new Flock or upload an existing one using the "Create New Flock" option in the sidebar.</p></article>""")
806
-
807
- @app.get("/ui/htmx/dashboard-flock-properties-preview/{filename}", response_class=HTMLResponse, tags=["UI HTMX Partials"])
808
- async def htmx_get_dashboard_flock_properties_preview(request: Request, filename: str):
809
- preview_flock_data = get_flock_preview_service(filename)
810
- return templates.TemplateResponse("partials/_dashboard_flock_properties_preview.html", {"request": request, "selected_filename": filename, "preview_flock": preview_flock_data})
811
-
812
- @app.get("/ui/htmx/create-flock-form", response_class=HTMLResponse, tags=["UI HTMX Partials"])
813
- async def htmx_get_create_flock_form(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
814
- return templates.TemplateResponse("partials/_create_flock_form.html", get_base_context_web(request, error, success, ui_mode))
815
-
816
- @app.get("/ui/htmx/agent-manager-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
817
- async def htmx_get_agent_manager_view(request: Request):
818
- context = get_base_context_web(request) # This gets flock from app.state
819
- if not context.get("current_flock"): # Check if flock exists in the context
820
- return HTMLResponse("<article class='error'><p>No flock loaded. Cannot manage agents.</p></article>")
821
- # Pass the 'current_flock' from the context to the template as 'flock'
822
- return templates.TemplateResponse(
823
- "partials/_agent_manager_view.html",
824
- {"request": request, "flock": context.get("current_flock")}
825
- )
826
-
827
- @app.get("/ui/htmx/registry-viewer", response_class=HTMLResponse, tags=["UI HTMX Partials"])
828
- async def htmx_get_registry_viewer(request: Request):
829
- return templates.TemplateResponse("partials/_registry_viewer_content.html", get_base_context_web(request))
830
-
831
- @app.get("/ui/htmx/execution-view-container", response_class=HTMLResponse, tags=["UI HTMX Partials"])
832
- async def htmx_get_execution_view_container(request: Request):
833
- context = get_base_context_web(request)
834
- if not context.get("current_flock"): return HTMLResponse("<article class='error'><p>No Flock loaded. Cannot execute.</p></article>")
835
- return templates.TemplateResponse("partials/_execution_view_container.html", context)
836
-
837
- @app.get("/ui/htmx/scoped-no-flock-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
838
- async def htmx_scoped_no_flock_view(request: Request):
839
- return HTMLResponse("""<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;"><hgroup><h2>Scoped Flock Mode</h2><h3>No Flock Loaded</h3></hgroup><p>This UI is in a scoped mode, expecting a Flock to be pre-loaded.</p><p>Please ensure the calling application provides a Flock instance.</p></article>""")
840
-
841
- # --- Action Routes (POST requests for UI interactions) ---
842
- @app.post("/ui/load-flock-action/by-name", response_class=HTMLResponse, tags=["UI Actions"])
843
- async def ui_load_flock_by_name_action(request: Request, selected_flock_filename: str = Form(...)):
844
- loaded_flock = load_flock_from_file_service(selected_flock_filename, request.app.state)
845
- response_headers = {}
846
- ui_mode_query = request.query_params.get("ui_mode", "standalone")
847
- if loaded_flock:
848
- success_message_text = f"Flock '{loaded_flock.name}' loaded from '{selected_flock_filename}'."
849
- response_headers["HX-Push-Url"] = "/ui/editor/execute?ui_mode=" + ui_mode_query
850
- response_headers["HX-Trigger"] = create_hx_trigger_header({"flockLoaded": None, "notify": {"type": "success", "message": success_message_text}})
851
- context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
852
- return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
853
- else:
854
- error_message_text = f"Failed to load flock file '{selected_flock_filename}'."
855
- response_headers["HX-Trigger"] = create_hx_trigger_header({"notify": {"type": "error", "message": error_message_text}})
856
- context = get_base_context_web(request, error=error_message_text, ui_mode=ui_mode_query)
857
- context["error_message_inline"] = error_message_text # For direct display in partial
858
- return templates.TemplateResponse("partials/_load_manager_view.html", context, headers=response_headers)
859
-
860
- @app.post("/ui/load-flock-action/by-upload", response_class=HTMLResponse, tags=["UI Actions"])
861
- async def ui_load_flock_by_upload_action(request: Request, flock_file_upload: UploadFile = File(...)):
862
- error_message_text, filename_to_load, response_headers = None, None, {}
863
- ui_mode_query = request.query_params.get("ui_mode", "standalone")
864
-
865
- if flock_file_upload and flock_file_upload.filename:
866
- if not flock_file_upload.filename.endswith((".yaml", ".yml", ".flock")): error_message_text = "Invalid file type."
867
- else:
868
- upload_path = FLOCK_FILES_DIR / flock_file_upload.filename
869
- try:
870
- with upload_path.open("wb") as buffer: shutil.copyfileobj(flock_file_upload.file, buffer)
871
- filename_to_load = flock_file_upload.filename
872
- except Exception as e: error_message_text = f"Upload failed: {e}"
873
- finally: await flock_file_upload.close()
874
- else: error_message_text = "No file uploaded."
875
-
876
- if filename_to_load and not error_message_text:
877
- loaded_flock = load_flock_from_file_service(filename_to_load, request.app.state)
878
- if loaded_flock:
879
- success_message_text = f"Flock '{loaded_flock.name}' loaded from '{filename_to_load}'."
880
- response_headers["HX-Push-Url"] = f"/ui/editor/execute?ui_mode={ui_mode_query}"
881
- response_headers["HX-Trigger"] = create_hx_trigger_header({"flockLoaded": None, "flockFileListChanged": None, "notify": {"type": "success", "message": success_message_text}})
882
- context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
883
- return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
884
- else: error_message_text = f"Failed to process uploaded '{filename_to_load}'."
885
-
886
- final_error_msg = error_message_text or "Upload failed."
887
- response_headers["HX-Trigger"] = create_hx_trigger_header({"notify": {"type": "error", "message": final_error_msg}})
888
- context = get_base_context_web(request, error=final_error_msg, ui_mode=ui_mode_query)
889
- return templates.TemplateResponse("partials/_create_flock_form.html", context, headers=response_headers)
890
-
891
- @app.post("/ui/create-flock", response_class=HTMLResponse, tags=["UI Actions"])
892
- async def ui_create_flock_action(request: Request, flock_name: str = Form(...), default_model: str = Form(None), description: str = Form(None)):
893
- ui_mode_query = request.query_params.get("ui_mode", "standalone")
894
- if not flock_name.strip():
895
- context = get_base_context_web(request, error="Flock name cannot be empty.", ui_mode=ui_mode_query)
896
- return templates.TemplateResponse("partials/_create_flock_form.html", context)
897
-
898
- new_flock = create_new_flock_service(flock_name, default_model, description, request.app.state)
899
- success_msg_text = f"New flock '{new_flock.name}' created. Navigating to Execute page. Configure properties and agents as needed."
900
- response_headers = {"HX-Push-Url": f"/ui/editor/execute?ui_mode={ui_mode_query}", "HX-Trigger": create_hx_trigger_header({"flockLoaded": None, "notify": {"type": "success", "message": success_msg_text}})}
901
- context = get_base_context_web(request, success=success_msg_text, ui_mode=ui_mode_query)
902
- return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
903
-
904
-
905
- # --- Settings Page & Endpoints ---
906
- @app.get("/ui/settings", response_class=HTMLResponse, tags=["UI Pages"])
907
- async def page_settings(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
908
- context = get_base_context_web(request, error, success, ui_mode)
909
- root_path = request.scope.get("root_path", "")
910
- context["initial_content_url"] = f"{root_path}/ui/htmx/settings-view"
911
- return templates.TemplateResponse("base.html", context)
912
-
913
- def _prepare_env_vars_for_template_web():
914
- env_vars_raw = load_env_file_web(); show_secrets = get_show_secrets_setting_web(env_vars_raw)
915
- env_vars_list = []
916
- for name, value in env_vars_raw.items():
917
- if name.startswith("#") or name == "": continue
918
- display_value = value if (not is_sensitive_web(name) or show_secrets) else mask_sensitive_value_web(value)
919
- env_vars_list.append({"name": name, "value": display_value})
920
- return env_vars_list, show_secrets
921
-
922
- @app.get("/ui/htmx/settings-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
923
- async def htmx_get_settings_view(request: Request):
924
- env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
925
- theme_name = get_current_theme_name()
926
- themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")] if THEMES_DIR and THEMES_DIR.exists() else []
927
- return templates.TemplateResponse("partials/_settings_view.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets, "themes": themes_available, "current_theme": theme_name})
928
-
929
- @app.post("/ui/htmx/toggle-show-secrets", response_class=HTMLResponse, tags=["UI Actions"])
930
- async def htmx_toggle_show_secrets(request: Request):
931
- env_vars_raw = load_env_file_web(); current = get_show_secrets_setting_web(env_vars_raw)
932
- set_show_secrets_setting_web(not current)
933
- env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
934
- return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
935
-
936
- @app.post("/ui/htmx/env-delete", response_class=HTMLResponse, tags=["UI Actions"])
937
- async def htmx_env_delete(request: Request, var_name: str = Form(...)):
938
- env_vars_raw = load_env_file_web()
939
- if var_name in env_vars_raw: del env_vars_raw[var_name]; save_env_file_web(env_vars_raw)
940
- env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
941
- return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
942
-
943
- @app.post("/ui/htmx/env-edit", response_class=HTMLResponse, tags=["UI Actions"])
944
- async def htmx_env_edit(request: Request, var_name: str = Form(...)):
945
- new_value = request.headers.get("HX-Prompt")
946
- env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
947
- if new_value is not None:
948
- env_vars_raw = load_env_file_web()
949
- env_vars_raw[var_name] = new_value
950
- save_env_file_web(env_vars_raw)
951
- env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
952
- return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
953
-
954
- @app.get("/ui/htmx/env-add-form", response_class=HTMLResponse, tags=["UI HTMX Partials"])
955
- async def htmx_env_add_form(request: Request):
956
- return HTMLResponse("""<form hx-post='/ui/htmx/env-add' hx-target='#env-vars-container' hx-swap='outerHTML' style='display:flex; gap:0.5rem; margin-bottom:0.5rem;'><input name='var_name' placeholder='NAME' required style='flex:2;'><input name='var_value' placeholder='VALUE' style='flex:3;'><button type='submit'>Add</button></form>""")
957
-
958
- @app.post("/ui/htmx/env-add", response_class=HTMLResponse, tags=["UI Actions"])
959
- async def htmx_env_add(request: Request, var_name: str = Form(...), var_value: str = Form("")):
960
- env_vars_raw = load_env_file_web()
961
- env_vars_raw[var_name] = var_value; save_env_file_web(env_vars_raw)
962
- env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
963
- return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
964
-
965
- @app.get("/ui/htmx/theme-preview", response_class=HTMLResponse, tags=["UI HTMX Partials"])
966
- async def htmx_theme_preview(request: Request, theme: str = Query(None)):
967
- if not THEME_LOADER_AVAILABLE:
968
- return HTMLResponse("<p>Theme loading functionality is not available.</p>", status_code=500)
969
- if THEMES_DIR is None or not THEMES_DIR.exists():
970
- return HTMLResponse("<p>Themes directory is not configured or does not exist.</p>", status_code=500)
971
-
972
- chosen_theme_name_input = theme or get_current_theme_name() or DEFAULT_THEME_NAME
973
-
974
- # Sanitize the input to get only the filename component
975
- sanitized_name_part = Path(chosen_theme_name_input).name
976
- # Ensure we have a stem
977
- theme_stem_from_input = sanitized_name_part
978
- if theme_stem_from_input.endswith(".toml"):
979
- theme_stem_from_input = theme_stem_from_input[:-5]
980
-
981
- theme_filename_to_load = f"{theme_stem_from_input}.toml"
982
- theme_name_for_display = theme_stem_from_input # Use the sanitized stem for display/logging
983
-
984
- try:
985
- resolved_themes_dir = THEMES_DIR.resolve(strict=True)
986
- theme_path_candidate = resolved_themes_dir / theme_filename_to_load
987
- resolved_theme_path = theme_path_candidate.resolve()
988
-
989
- try:
990
- resolved_theme_path.relative_to(resolved_themes_dir)
991
- except ValueError:
992
- logger.warning(f"Invalid theme path access attempt for '{theme_name_for_display}'. "
993
- f"Original input: '{chosen_theme_name_input}', Sanitized filename: '{theme_filename_to_load}', "
994
- f"Attempted path: '{theme_path_candidate}', Resolved to: '{resolved_theme_path}'")
995
- return HTMLResponse(f"<p>Invalid theme name or path for '{theme_name_for_display}'.</p>", status_code=400)
996
- if resolved_theme_path.name != theme_filename_to_load:
997
- logger.warning(f"Invalid theme filename for '{theme_name_for_display}'. "
998
- f"Original input: '{chosen_theme_name_input}', Sanitized filename: '{theme_filename_to_load}', "
999
- f"Attempted path: '{theme_path_candidate}', Resolved to: '{resolved_theme_path}'")
1000
- return HTMLResponse(f"<p>Invalid theme name or path for '{theme_name_for_display}'.</p>", status_code=400)
1001
-
1002
- if not resolved_theme_path.is_file():
1003
- logger.info(f"Theme preview: Theme file '{theme_filename_to_load}' not found at '{resolved_theme_path}'.")
1004
- return HTMLResponse(f"<p>Theme '{theme_name_for_display}' not found.</p>", status_code=404)
1005
-
1006
- theme_path = resolved_theme_path
1007
- theme_data = load_theme_from_file(str(theme_path))
1008
- logger.debug(f"Successfully loaded theme '{theme_name_for_display}' for preview from '{theme_path}'")
1009
-
1010
- except FileNotFoundError: # For THEMES_DIR.resolve(strict=True)
1011
- logger.error(f"Themes directory '{THEMES_DIR}' not found during preview for '{theme_name_for_display}'.")
1012
- return HTMLResponse("<p>Themes directory not found.</p>", status_code=500)
1013
- except Exception as e:
1014
- logger.error(f"Error loading theme '{theme_name_for_display}' for preview (path: '{theme_path_candidate if 'theme_path_candidate' in locals() else 'unknown'}'): {e}")
1015
- return HTMLResponse(f"<p>Error loading theme '{theme_name_for_display}': {e}</p>", status_code=500)
1016
-
1017
- css_vars = alacritty_to_pico(theme_data)
1018
- if not css_vars:
1019
- return HTMLResponse(f"<p>Could not convert theme '{theme_name_for_display}' to CSS variables.</p>")
1020
-
1021
- css_vars_str = ":root {\n" + "\\n".join([f" {k}: {v};" for k, v in css_vars.items()]) + "\\n}"
1022
- main_colors = [("Background", css_vars.get("--pico-background-color")), ("Text", css_vars.get("--pico-color")), ("Primary", css_vars.get("--pico-primary")), ("Secondary", css_vars.get("--pico-secondary")), ("Muted", css_vars.get("--pico-muted-color"))]
1023
- return templates.TemplateResponse("partials/_theme_preview.html", {"request": request, "theme_name": theme_name_for_display, "css_vars_str": css_vars_str, "main_colors": main_colors})
1024
-
1025
- @app.post("/ui/apply-theme", tags=["UI Actions"])
1026
- async def apply_theme(request: Request, theme: str = Form(...)):
1027
- try:
1028
- from flock.webapp.app.config import set_current_theme_name
1029
- set_current_theme_name(theme)
1030
- headers = {"HX-Refresh": "true"}
1031
- return HTMLResponse("", headers=headers)
1032
- except Exception as e: return HTMLResponse(f"Failed to apply theme: {e}", status_code=500)
1033
-
1034
- @app.get("/ui/htmx/settings/env-vars", response_class=HTMLResponse, tags=["UI HTMX Partials"])
1035
- async def htmx_settings_env_vars(request: Request):
1036
- env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
1037
- return templates.TemplateResponse("partials/_settings_env_content.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
1038
-
1039
- @app.get("/ui/htmx/settings/theme", response_class=HTMLResponse, tags=["UI HTMX Partials"])
1040
- async def htmx_settings_theme(request: Request):
1041
- theme_name = get_current_theme_name()
1042
- themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")] if THEMES_DIR and THEMES_DIR.exists() else []
1043
- return templates.TemplateResponse("partials/_settings_theme_content.html", {"request": request, "themes": themes_available, "current_theme": theme_name})
1044
-
1045
- @app.get("/ui/chat", response_class=HTMLResponse, tags=["UI Pages"])
1046
- async def page_chat(request: Request, ui_mode: str = Query("standalone")):
1047
- context = get_base_context_web(request, ui_mode=ui_mode)
1048
- context["initial_content_url"] = "/ui/htmx/chat-view"
1049
- return templates.TemplateResponse("base.html", context)
1050
-
1051
- @app.get("/ui/htmx/chat-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
1052
- async def htmx_get_chat_view(request: Request):
1053
- # Render container partial; session handled in chat router
1054
- return templates.TemplateResponse("partials/_chat_container.html", get_base_context_web(request))
1055
-
1056
- if __name__ == "__main__":
1057
- import uvicorn
1058
- # Ensure the dependency injection system is initialized for standalone run
1059
- temp_run_store = RunStore()
1060
- # Create a default/dummy Flock instance for standalone UI testing
1061
- # This allows the UI to function without being started by `Flock.start_api()`
1062
- dev_flock_instance = Flock(name="DevStandaloneFlock", model="test/dummy", show_flock_banner=False)
1063
-
1064
- set_global_flock_services(dev_flock_instance, temp_run_store)
1065
- app.state.flock_instance = dev_flock_instance
1066
- app.state.run_store = temp_run_store
1067
- app.state.flock_filename = "development_standalone.flock.yaml"
1068
-
1069
- logger.info("Running webapp.app.main directly for development with a dummy Flock instance.")
1070
- uvicorn.run(app, host="127.0.0.1", port=8344, reload=True)