legate 0.1.0

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 (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,111 @@
1
+ # File: lib/legate/web/routes/core_routes.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ module Web
6
+ module CoreRoutes
7
+ def self.registered(app)
8
+ # GET / - Main welcome page with dashboard metrics.
9
+ app.get '/' do
10
+ logger.debug('GET / route handler entered (from CoreRoutes)')
11
+
12
+ # Compute dashboard metrics
13
+ definition_store = instance_variable_get(:@definition_store)
14
+ tool_manager = instance_variable_get(:@tool_manager)
15
+
16
+ @agent_count = 0
17
+ @running_count = 0
18
+ @tool_count = 0
19
+ @auth_scheme_count = 0
20
+
21
+ if definition_store
22
+ definitions = begin
23
+ definition_store.list_definitions
24
+ rescue StandardError
25
+ []
26
+ end
27
+ @agent_count = definitions.size
28
+ end
29
+ # Running count is based on in-memory @agents hash
30
+ active_agents_hash = instance_variable_get(:@agents)
31
+ @running_count = active_agents_hash&.size || 0
32
+
33
+ if tool_manager
34
+ @tool_count = begin
35
+ tool_manager.tools.size
36
+ rescue StandardError
37
+ 0
38
+ end
39
+ end
40
+
41
+ # Count auth schemes (best-effort; the dashboard must never break).
42
+ # Was checking the wrong constant (Legate::Authentication::Manager),
43
+ # so this count never displayed.
44
+ @auth_scheme_count = begin
45
+ Legate::Auth::Manager.instance.schemes.size
46
+ rescue StandardError
47
+ 0
48
+ end
49
+
50
+ # Fetch recent activity
51
+ @recent_activity = if defined?(Legate::ActivityLog)
52
+ begin
53
+ Legate::ActivityLog.recent(8)
54
+ rescue StandardError
55
+ []
56
+ end
57
+ else
58
+ []
59
+ end
60
+
61
+ slim :index
62
+ end
63
+
64
+ # GET /activity/recent - Returns recent activity HTML partial
65
+ app.get '/activity/recent' do
66
+ @recent_activity = if defined?(Legate::ActivityLog)
67
+ begin
68
+ Legate::ActivityLog.recent(8)
69
+ rescue StandardError
70
+ []
71
+ end
72
+ else
73
+ []
74
+ end
75
+
76
+ slim :_activity_list, layout: false
77
+ end
78
+
79
+ # GET /healthz - Standard health check endpoint.
80
+ app.get '/healthz' do
81
+ current_app_instance = self
82
+ definition_store = current_app_instance.instance_variable_get(:@definition_store)
83
+
84
+ store_ok = if definition_store
85
+ definition_store.check_connection
86
+ else
87
+ true # No persistence configured (in-memory mode)
88
+ end
89
+
90
+ unless store_ok
91
+ logger.error('Health check failed: Definition Store unavailable or connection failed (from CoreRoutes).')
92
+ status 503
93
+ body 'Service Unavailable (Persistence)'
94
+ return
95
+ end
96
+
97
+ status 200
98
+ body 'OK'
99
+ rescue Legate::DefinitionStore::StoreError => e
100
+ logger.error("Health check failed (from CoreRoutes): Store error - #{e.message}")
101
+ status 503
102
+ body 'Service Unavailable (Persistence Error)'
103
+ rescue StandardError => e # Catch other unexpected errors
104
+ logger.error("Health check failed (from CoreRoutes): Unexpected error - #{e.class}: #{e.message}")
105
+ status 503
106
+ body 'Service Unavailable (Internal)'
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,220 @@
1
+ # File: lib/legate/web/routes/documentation_routes.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'kramdown' # Required for markdown processing
5
+ require 'kramdown-parser-gfm' # Required for GitHub Flavored Markdown
6
+ require 'pathname' # For robust path operations
7
+
8
+ module Legate
9
+ module Web
10
+ module DocumentationRoutes
11
+ module Helpers
12
+ def render_markdown(file_path)
13
+ public_docs_pathname = Pathname.new(File.expand_path(File.join(settings.root, 'public', 'docs')))
14
+ target_file_pathname = Pathname.new(File.expand_path(file_path))
15
+
16
+ # Check if the target file is within the public_docs_pathname directory
17
+ # and is an actual file that exists.
18
+ # The `ascend` method iterates upwards from the file to its parent directories.
19
+ # We check if any of these parent paths match our intended docs root.
20
+ is_within_docs_dir = false
21
+ target_file_pathname.ascend do |p|
22
+ if p == public_docs_pathname
23
+ is_within_docs_dir = true
24
+ break
25
+ end
26
+ break if p.root? # Stop if we reach the filesystem root
27
+ end
28
+
29
+ unless is_within_docs_dir && target_file_pathname.file? && target_file_pathname.exist?
30
+ logger.warn "Markdown render: Path not valid or file does not exist. Target: '#{target_file_pathname}', Expected base: '#{public_docs_pathname}'"
31
+ return nil
32
+ end
33
+
34
+ markdown_content = File.read(target_file_pathname.to_s, encoding: 'UTF-8')
35
+
36
+ # Configure Kramdown with enhanced rendering options - now using GFM
37
+ html = Kramdown::Document.new(
38
+ markdown_content,
39
+ input: 'GFM', # GitHub Flavored Markdown
40
+ syntax_highlighter: nil, # We'll apply our custom highlighting via CSS
41
+ hard_wrap: false # Don't convert newlines to <br>
42
+ ).to_html
43
+
44
+ # Post-process HTML to add language tags to code blocks
45
+ process_code_blocks(html)
46
+ rescue Errno::ENOENT # Should be caught by File.exist? check now, but good fallback
47
+ logger.warn "Markdown file not found (render_markdown): #{target_file_pathname}"
48
+ nil
49
+ rescue StandardError => e
50
+ logger.error "Error rendering markdown for #{target_file_pathname}: #{e.message}"
51
+ logger.error e.backtrace.join("\n") # Add backtrace to help debugging
52
+ nil
53
+ end
54
+
55
+ # Process code blocks to add language classes and data attributes
56
+ def process_code_blocks(html)
57
+ # Find fenced code blocks with language specifications
58
+ html.gsub(%r{<pre><code\s+class="language-(\w+)">(.*?)</code></pre>}m) do |match|
59
+ language = ::Regexp.last_match(1)
60
+ code_content = ::Regexp.last_match(2)
61
+ # Replace with our enhanced version that adds data-lang attribute
62
+ %(<pre data-lang="#{language}"><code class="language-#{language}">#{code_content}</code></pre>)
63
+ end
64
+ end
65
+
66
+ def generate_summary(markdown_content, max_lines = 5)
67
+ return '' if markdown_content.nil? || markdown_content.empty?
68
+
69
+ lines = markdown_content.lines
70
+ summary_lines = []
71
+ non_empty_lines_count = 0
72
+ lines.each do |line|
73
+ stripped_line = line.strip
74
+ if !stripped_line.empty? && !stripped_line.start_with?('#') # Ignore headers for summary start
75
+ summary_lines << stripped_line
76
+ non_empty_lines_count += 1
77
+ break if non_empty_lines_count >= max_lines
78
+ elsif !summary_lines.empty? && stripped_line.empty? # Break on first blank line after content started
79
+ break
80
+ elsif summary_lines.empty? && stripped_line.start_with?('#') # Skip leading headers
81
+ next
82
+ end
83
+ end
84
+ summary_lines.join(' ').gsub(/\*\*|\*|_|`/, '') # Basic stripping of markdown
85
+ end
86
+ end # module Helpers
87
+
88
+ def self.registered(app)
89
+ app.helpers Helpers # Register the helpers for use in routes
90
+
91
+ # Route for Documentation Index (GET /docs)
92
+ app.get '/docs' do
93
+ logger.debug('--- GET /docs --- (DocumentationRoutes)')
94
+ docs_root_path = Pathname.new(File.join(settings.root, 'public', 'docs'))
95
+ logger.debug("Docs root resolved to: #{docs_root_path}")
96
+
97
+ categorized_documents = {}
98
+
99
+ begin
100
+ # Process files directly in docs_root_path first (General category)
101
+ general_docs = []
102
+ Dir.glob(File.join(docs_root_path, '*.md')).sort.each do |md_file_path|
103
+ pathname = Pathname.new(md_file_path)
104
+ next if pathname.directory? # Skip directories if any found by glob
105
+
106
+ filename_md = pathname.basename.to_s
107
+ filename_no_ext = pathname.basename('.md').to_s
108
+ title = filename_no_ext.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
109
+ file_content = File.read(md_file_path, encoding: 'UTF-8')
110
+ first_h1_match = file_content.match(/^#\s+(.+)$/)
111
+ title = first_h1_match[1].strip if first_h1_match
112
+ summary = generate_summary(file_content)
113
+
114
+ general_docs << {
115
+ title: title,
116
+ path: filename_no_ext, # Path relative to docs_root for linking
117
+ summary: summary
118
+ }
119
+ end
120
+ categorized_documents['General'] = general_docs if general_docs.any?
121
+
122
+ # Process subdirectories for categories
123
+ Dir.glob(File.join(docs_root_path, '*/')).sort.each do |dir_path|
124
+ category_pathname = Pathname.new(dir_path)
125
+ category_name = category_pathname.basename.to_s.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
126
+ category_docs = []
127
+
128
+ Dir.glob(File.join(category_pathname, '*.md')).sort.each do |md_file_path|
129
+ pathname = Pathname.new(md_file_path)
130
+ filename_md = pathname.basename.to_s
131
+ filename_no_ext = pathname.basename('.md').to_s
132
+ full_relative_path = "#{category_pathname.basename}/#{filename_no_ext}"
133
+
134
+ title = filename_no_ext.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
135
+ file_content = File.read(md_file_path, encoding: 'UTF-8')
136
+ first_h1_match = file_content.match(/^#\s+(.+)$/)
137
+ title = first_h1_match[1].strip if first_h1_match
138
+ summary = generate_summary(file_content)
139
+
140
+ category_docs << {
141
+ title: title,
142
+ path: full_relative_path, # Path includes category dir
143
+ summary: summary
144
+ }
145
+ end
146
+ categorized_documents[category_name] = category_docs if category_docs.any?
147
+ end
148
+
149
+ logger.warn("No markdown files or categories found in #{docs_root_path}") if categorized_documents.empty?
150
+ rescue StandardError => e
151
+ logger.error("Error scanning documentation directory: #{e.message}")
152
+ logger.error(e.backtrace.first(5).join("\n"))
153
+ end
154
+
155
+ instance_variable_set(:@categorized_documents, categorized_documents)
156
+ logger.debug("Setting @categorized_documents with #{categorized_documents.keys.count} categories.")
157
+ logger.debug("Content of @categorized_documents: #{@categorized_documents.inspect}")
158
+
159
+ slim :docs_index
160
+ end
161
+
162
+ # Route for Displaying a Single Document (GET /docs/*)
163
+ # The splat parameter will capture the category/filename path
164
+ app.get '/docs/*' do |path_splat|
165
+ logger.debug("GET /docs/#{path_splat} - Entered (from DocumentationRoutes)")
166
+
167
+ # Sanitize path_splat: allow alphanumeric, underscore, hyphen, and forward slash for path
168
+ # Remove leading/trailing slashes and protect against directory traversal
169
+ sane_path = path_splat.gsub(%r{^/+|/+$}, '').gsub(%r{\.{2}/}, '')
170
+ sane_path.gsub!(%r{[^0-9a-zA-Z_\-/]}, '') # Allow alphanumeric, _, -, /
171
+
172
+ if sane_path.empty?
173
+ logger.warn("Attempt to access doc with invalid path: '#{path_splat}'")
174
+ halt 404, slim(:error_404, locals: { title: 'Document Not Found', message: 'Invalid document path.' })
175
+ end
176
+
177
+ # Construct the full file path
178
+ # Ensure it's .md, even if not explicitly in splat, for direct access attempts
179
+ file_path_to_check = sane_path.end_with?('.md') ? sane_path : "#{sane_path}.md"
180
+ full_file_path = File.join(settings.root, 'public', 'docs', file_path_to_check)
181
+ logger.debug("Attempting to access document at: #{full_file_path}")
182
+
183
+ # The render_markdown helper already performs security checks to ensure the file is within 'public/docs'
184
+ # and is a .md file.
185
+
186
+ begin
187
+ markdown_html = render_markdown(full_file_path) # render_markdown expects absolute path
188
+
189
+ if markdown_html.nil?
190
+ logger.warn("Documentation file not found or rendering failed: #{full_file_path}")
191
+ halt 404,
192
+ slim(:error_404,
193
+ locals: { title: 'Document Not Found',
194
+ message: "Document '#{sane_path}' not found or could not be rendered." })
195
+ end
196
+
197
+ instance_variable_set(:@doc_html_content, markdown_html)
198
+
199
+ # Try to extract title from H1 in markdown content
200
+ # This requires reading the file again, or passing content from render_markdown if it returned it
201
+ # For now, let's re-read for title extraction consistency.
202
+ doc_title_for_view = sane_path.split('/').last.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ') # Default title
203
+ if File.exist?(full_file_path)
204
+ markdown_content_for_title = File.read(full_file_path, encoding: 'UTF-8')
205
+ first_h1_match = markdown_content_for_title.match(/^#\s+(.+)$/)
206
+ doc_title_for_view = first_h1_match[1].strip if first_h1_match
207
+ end
208
+ instance_variable_set(:@doc_title, doc_title_for_view)
209
+
210
+ slim :docs_show
211
+ rescue StandardError => e
212
+ logger.error("Error processing document #{sane_path}: #{e.message}")
213
+ logger.error(e.backtrace.join("\n"))
214
+ halt 500, 'Error displaying document.'
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,81 @@
1
+ # File: lib/legate/web/routes/tool_generator_routes.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../generators/tool_generator'
5
+ require_relative '../../generators/runtime_tool_loader'
6
+
7
+ module Legate
8
+ module Web
9
+ # Routes for AI-powered tool code generation. Delegates to
10
+ # Legate::Generators::ToolGenerator so the web path shares the exact
11
+ # generation logic AND the CodeValidator safety check used by the CLI
12
+ # (the route must not reimplement either).
13
+ module ToolGeneratorRoutes
14
+ def self.registered(app)
15
+ # POST /tools/generate - Generate tool class code from natural language
16
+ app.post '/tools/generate' do
17
+ content_type :json
18
+
19
+ begin
20
+ request.body.rewind
21
+ body = JSON.parse(request.body.read)
22
+ rescue JSON::ParserError => e
23
+ halt 400, json(error: "Invalid JSON: #{e.message}")
24
+ end
25
+
26
+ begin
27
+ result = Legate::Generators::ToolGenerator.generate(description: body['description'].to_s.strip)
28
+ logger.info("Successfully generated tool code (name: #{result[:suggested_name]}, type: #{result[:tool_type]})")
29
+ json(result)
30
+ rescue Legate::Generators::ToolGenerator::ApiKeyMissingError => e
31
+ halt 503, json(error: e.message)
32
+ rescue Legate::Generators::ToolGenerator::ApiError => e
33
+ logger.error("Gemini API error during tool generation: #{e.message}")
34
+ halt 503, json(error: 'AI service communication error. Please try again.')
35
+ rescue Legate::Generators::ToolGenerator::GenerationError => e
36
+ # Covers validation failures, empty responses, and unsafe generated
37
+ # code rejected by CodeValidator.
38
+ halt 400, json(error: e.message)
39
+ rescue StandardError => e
40
+ logger.error("Unexpected error during tool generation: #{e.class} - #{e.message}")
41
+ halt 500, json(error: 'Generation failed. Please try again.')
42
+ end
43
+ end
44
+
45
+ # POST /tools/install - Load a generated custom tool into the RUNNING process.
46
+ # SECURITY: executes LLM-generated Ruby. Gated by config + explicit confirm +
47
+ # server-side re-validation (see RuntimeToolLoader). Writes tools/<name>.rb.
48
+ app.post '/tools/install' do
49
+ content_type :json
50
+
51
+ halt 403, json(error: 'Runtime tool loading is disabled in this environment. Use Download instead, place the file in tools/, and restart.') unless Legate::Generators::RuntimeToolLoader.enabled?
52
+
53
+ begin
54
+ request.body.rewind
55
+ body = JSON.parse(request.body.read)
56
+ rescue JSON::ParserError => e
57
+ halt 400, json(error: "Invalid JSON: #{e.message}")
58
+ end
59
+
60
+ # Require an explicit confirmation that the user accepts running this code.
61
+ halt 400, json(error: 'Confirmation required to install a tool.') unless body['confirm'] == true
62
+
63
+ source = body['code'].to_s
64
+ halt 400, json(error: 'No tool code provided.') if source.strip.empty?
65
+
66
+ result = Legate::Generators::RuntimeToolLoader.load_source!(
67
+ source, suggested_name: body['suggested_name'].to_s
68
+ )
69
+
70
+ if result[:ok]
71
+ logger.info("Runtime-loaded custom tool '#{result[:tool_name]}' -> #{result[:path]}")
72
+ json(ok: true, tool_name: result[:tool_name])
73
+ else
74
+ logger.warn("Runtime tool install rejected: #{result[:error]}")
75
+ halt 422, json(error: result[:error])
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,207 @@
1
+ # File: lib/legate/web/routes/tools_ui_routes.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ module Web
6
+ module ToolsUIRoutes
7
+ def self.registered(app)
8
+ # GET /tools - Display available native and MCP tools
9
+ app.get '/tools' do
10
+ logger.info('GET /tools route handler entered (from ToolsUIRoutes)')
11
+
12
+ current_app_instance = self
13
+ definition_store = current_app_instance.instance_variable_get(:@definition_store)
14
+
15
+ # 1. Get Native Tools (already formatted)
16
+ native_tools_metadata = Legate::GlobalToolManager.list_all_tools.map do |tool_meta|
17
+ parameters_array = []
18
+ if tool_meta[:parameters].is_a?(Hash) && !tool_meta[:parameters].empty?
19
+ tool_meta[:parameters].each do |param_name, details|
20
+ parameters_array << {
21
+ name: param_name,
22
+ type: details[:type],
23
+ description: details[:description],
24
+ required: details[:required]
25
+ }
26
+ end
27
+ end
28
+ tool_meta.merge(parameters: parameters_array, source: :native, source_detail: 'Native')
29
+ end
30
+
31
+ # 2. Get MCP Tools
32
+ all_mcp_configs = []
33
+ if definition_store
34
+ begin
35
+ agent_summaries = definition_store.list_definitions
36
+ all_mcp_configs = agent_summaries.flat_map do |summary|
37
+ mcp_json = summary[:mcp_servers_json]
38
+ if mcp_json && !mcp_json.empty? && mcp_json != '[]'
39
+ JSON.parse(mcp_json)
40
+ else
41
+ []
42
+ end
43
+ end.uniq
44
+ rescue JSON::ParserError => e
45
+ logger.error("Error parsing MCP JSON for /tools (from ToolsUIRoutes): #{e.message}")
46
+ rescue Legate::DefinitionStore::StoreError => e
47
+ logger.error("Store error fetching agent definitions for /tools (from ToolsUIRoutes): #{e.message}")
48
+ end
49
+ else
50
+ logger.warn('Definition store not available for MCP tool discovery in /tools (from ToolsUIRoutes)')
51
+ end
52
+
53
+ mcp_tool_fetch_results = fetch_mcp_tools(all_mcp_configs || [])
54
+
55
+ processed_mcp_tools_metadata = []
56
+ mcp_tool_fetch_results.each do |result|
57
+ next unless result[:status] == :success && result[:tools]
58
+
59
+ result[:tools].each do |mcp_tool_schema|
60
+ parameters = []
61
+ begin
62
+ input_schema = mcp_tool_schema[:inputSchema]
63
+ if input_schema&.is_a?(Hash)
64
+ properties = input_schema['properties'] || {}
65
+ required_props = input_schema['required'] || []
66
+ parameters = Legate::Mcp::Util::SchemaConverter.json_to_legate(properties, required_props)
67
+ end
68
+ rescue StandardError => e
69
+ logger.error("Error converting MCP schema for tool '#{mcp_tool_schema[:name]}' in /tools (from ToolsUIRoutes): #{e.message}")
70
+ end
71
+ processed_mcp_tools_metadata << {
72
+ name: mcp_tool_schema[:name].to_sym,
73
+ description: mcp_tool_schema[:description] || '',
74
+ parameters: parameters,
75
+ source: :mcp,
76
+ source_detail: "MCP (#{result[:server]})"
77
+ }
78
+ end
79
+ end
80
+
81
+ # 3. Set @native_tools for the current view requirements
82
+ instance_variable_set(:@native_tools, native_tools_metadata.sort_by { |t| t[:name].to_s })
83
+
84
+ # For future enhancement of tools.slim to show all tools:
85
+ combined_tools_map = {}
86
+ native_tools_metadata.each { |tool| combined_tools_map[tool[:name]] = tool }
87
+ processed_mcp_tools_metadata.each { |tool| combined_tools_map[tool[:name]] ||= tool }
88
+ instance_variable_set(:@all_tools_list, combined_tools_map.values.sort_by { |t| t[:name].to_s })
89
+ instance_variable_set(:@mcp_tool_results_for_view, mcp_tool_fetch_results) # For displaying fetch errors
90
+
91
+ slim :tools
92
+ end
93
+
94
+ # GET /tools/:name - Display tool detail page (native or MCP)
95
+ app.get '/tools/:name' do |name|
96
+ logger.info("GET /tools/#{name} route handler entered (from ToolsUIRoutes)")
97
+ tool_name_sym = name.to_sym
98
+
99
+ current_app_instance = self
100
+ definition_store = current_app_instance.instance_variable_get(:@definition_store)
101
+
102
+ # 1. Try to find in native tools
103
+ native_tool_metadata = Legate::GlobalToolManager.list_all_tools.find { |t| t[:name] == tool_name_sym }
104
+ tool_to_display = nil
105
+
106
+ if native_tool_metadata
107
+ parameters_array = []
108
+ if native_tool_metadata[:parameters].is_a?(Hash) && !native_tool_metadata[:parameters].empty?
109
+ native_tool_metadata[:parameters].each do |param_name, details|
110
+ parameters_array << {
111
+ name: param_name,
112
+ type: details[:type],
113
+ description: details[:description],
114
+ required: details[:required]
115
+ }
116
+ end
117
+ end
118
+ tool_to_display = native_tool_metadata.merge(parameters: parameters_array, source: :native,
119
+ source_detail: 'Native')
120
+ else
121
+ # 2. Not native, try to find in MCP tools
122
+ all_mcp_configs = []
123
+ if definition_store
124
+ begin
125
+ agent_summaries = definition_store.list_definitions
126
+ all_mcp_configs = agent_summaries.flat_map do |summary|
127
+ mcp_json = summary[:mcp_servers_json]
128
+ if mcp_json && !mcp_json.empty? && mcp_json != '[]'
129
+ JSON.parse(mcp_json)
130
+ else
131
+ []
132
+ end
133
+ end.uniq
134
+ rescue JSON::ParserError => e
135
+ logger.error("Error parsing MCP JSON for /tools/#{name} (from ToolsUIRoutes): #{e.message}")
136
+ rescue Legate::DefinitionStore::StoreError => e
137
+ logger.error("Store error fetching agent definitions for /tools/#{name} (from ToolsUIRoutes): #{e.message}")
138
+ end
139
+ else
140
+ logger.warn("Definition store not available for MCP tool discovery in /tools/#{name} (from ToolsUIRoutes)")
141
+ end
142
+
143
+ mcp_tool_fetch_results = fetch_mcp_tools(all_mcp_configs || [])
144
+
145
+ mcp_tool_fetch_results.each do |result|
146
+ next unless result[:status] == :success && result[:tools]
147
+
148
+ tool_data = result[:tools].find { |t| t[:name].to_s == name || t[:name].to_sym == tool_name_sym }
149
+ next unless tool_data
150
+
151
+ parameters = []
152
+ begin
153
+ input_schema = tool_data[:inputSchema]
154
+ if input_schema&.is_a?(Hash)
155
+ properties = input_schema['properties'] || {}
156
+ required_props = input_schema['required'] || []
157
+ parameters = Legate::Mcp::Util::SchemaConverter.json_to_legate(properties, required_props)
158
+ end
159
+ rescue StandardError => e
160
+ logger.error("Error converting MCP schema for tool '#{tool_data[:name]}' in /tools/#{name} (from ToolsUIRoutes): #{e.message}")
161
+ end
162
+ tool_to_display = {
163
+ name: tool_data[:name].to_sym,
164
+ description: tool_data[:description] || '',
165
+ parameters: parameters,
166
+ source: :mcp,
167
+ source_detail: "MCP (#{result[:server]})"
168
+ }
169
+ break
170
+ end
171
+ end
172
+
173
+ if tool_to_display
174
+ instance_variable_set(:@tool, tool_to_display)
175
+ logger.debug("Found tool metadata for '#{name}': #{tool_to_display.inspect}")
176
+ slim :tool_detail
177
+ else
178
+ logger.warn("Tool '#{name}' not found anywhere (from ToolsUIRoutes).")
179
+ status 404
180
+ slim(:error_404,
181
+ locals: { title: 'Tool Not Found', message: "Tool definition for '#{name}' not found." })
182
+ end
183
+ end
184
+
185
+ # GET /tools/:name/download - Download native tool as Ruby file
186
+ app.get '/tools/:name/download' do |name|
187
+ logger.info("Received request to download tool '#{name}' as Ruby file")
188
+ tool_name_sym = name.to_sym
189
+
190
+ # Check if it's a native tool
191
+ tool_class = Legate::GlobalToolManager.find_class(tool_name_sym)
192
+ halt 400, 'Only native tools can be downloaded as Ruby files. MCP tools cannot be exported.' unless tool_class
193
+
194
+ # Generate Ruby code
195
+ require 'legate/tool_code_generator'
196
+ ruby_code = Legate::ToolCodeGenerator.generate(tool_name_sym)
197
+
198
+ halt 404, 'Tool not found or could not generate code.' unless ruby_code
199
+
200
+ content_type 'application/x-ruby'
201
+ attachment "#{name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')}.rb"
202
+ ruby_code
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end