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,412 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'code_validator'
4
+ require_relative '../llm'
5
+
6
+ module Legate
7
+ module Generators
8
+ # AI-powered agent code generator. Uses the configured LLM adapter (Gemini by
9
+ # default) via Legate::LLM.
10
+ class AgentGenerator
11
+ class GenerationError < StandardError; end
12
+ class ApiKeyMissingError < GenerationError; end
13
+ class ApiError < GenerationError; end
14
+
15
+ # Model used for code generation; passed to Legate::LLM.build_adapter.
16
+ GENERATION_MODEL = 'gemini-2.5-pro'
17
+
18
+ # Generate agent definition code from a natural language description
19
+ # @param description [String] Natural language description of the agent to generate
20
+ # @return [Hash] { code: String, suggested_name: String }
21
+ # @raise [ApiKeyMissingError] if GOOGLE_API_KEY is not set
22
+ # @raise [ApiError] if Gemini API fails
23
+ # @raise [GenerationError] for other generation failures
24
+ def self.generate(description:)
25
+ new.generate(description: description)
26
+ end
27
+
28
+ def generate(description:)
29
+ validate_description!(description)
30
+ adapter = Legate::LLM.build_adapter(model: GENERATION_MODEL)
31
+ raise ApiKeyMissingError, 'GOOGLE_API_KEY not configured. AI generation requires a Gemini API key.' unless adapter.available?
32
+
33
+ available_tools = format_available_tools
34
+ system_prompt = build_prompt(available_tools)
35
+ user_prompt = build_user_prompt(description)
36
+
37
+ generated_code = call_llm(adapter, system_prompt, user_prompt)
38
+ clean_code = clean_generated_code(generated_code)
39
+ CodeValidator.validate!(clean_code)
40
+ suggested_name = extract_agent_name(clean_code)
41
+
42
+ { code: clean_code, suggested_name: suggested_name }
43
+ rescue CodeValidator::UnsafeCodeError => e
44
+ raise GenerationError, e.message
45
+ end
46
+
47
+ # Generate a STRUCTURED agent definition (the same fields the "Create Agent"
48
+ # form accepts) from a description. No Ruby is generated or executed — the
49
+ # result is plain data that can be registered live via POST /agents. Tools
50
+ # are filtered to those actually installed (hallucinated ones are dropped).
51
+ # @return [Hash] { name:, description:, instruction:, model:, agent_type:,
52
+ # tools: [String], output_key:, dropped_tools: [String] }
53
+ def self.generate_definition(description:)
54
+ new.generate_definition(description: description)
55
+ end
56
+
57
+ def generate_definition(description:)
58
+ validate_description!(description)
59
+ adapter = Legate::LLM.build_adapter(model: GENERATION_MODEL)
60
+ raise ApiKeyMissingError, 'GOOGLE_API_KEY not configured. AI generation requires a Gemini API key.' unless adapter.available?
61
+
62
+ system_prompt = build_definition_prompt(format_available_tools)
63
+ user_prompt = build_definition_user_prompt(description)
64
+ raw = call_llm(adapter, system_prompt, user_prompt)
65
+ normalize_definition_fields(parse_definition_json(raw), fallback_description: description)
66
+ end
67
+
68
+ private
69
+
70
+ def parse_definition_json(raw)
71
+ cleaned = raw.strip
72
+ .gsub(/\A```json\n?/, '').gsub(/\A```\n?/, '').gsub(/\n?```\z/, '').strip
73
+ # Be forgiving if the model wraps the object in stray prose.
74
+ cleaned = Regexp.last_match(0) if cleaned !~ /\A\{/ && cleaned.match(/\{.*\}/m)
75
+ JSON.parse(cleaned)
76
+ rescue JSON::ParserError => e
77
+ raise GenerationError, "The AI response wasn't valid JSON. Please try regenerating. (#{e.message})"
78
+ end
79
+
80
+ def normalize_definition_fields(fields, fallback_description:)
81
+ raise GenerationError, 'The AI response was not a JSON object. Please try regenerating.' unless fields.is_a?(Hash)
82
+
83
+ valid_tool_names = Legate::GlobalToolManager.list_all_tools.map { |t| t[:name].to_s }
84
+
85
+ name = sanitize_agent_name(fields['name'])
86
+ raise GenerationError, 'The AI did not produce a valid agent name. Please try regenerating.' if name.empty?
87
+
88
+ agent_type = fields['agent_type'].to_s.strip.downcase
89
+ agent_type = 'llm' unless %w[llm sequential parallel loop].include?(agent_type)
90
+
91
+ requested = Array(fields['tools']).map { |t| t.to_s.sub(/\A:/, '').strip }.reject(&:empty?).uniq
92
+ tools = requested & valid_tool_names
93
+ dropped = requested - valid_tool_names
94
+
95
+ description = fields['description'].to_s.strip
96
+ description = fallback_description.to_s.strip if description.empty?
97
+
98
+ model = fields['model'].to_s.strip
99
+ model = Legate::Agent::DEFAULT_MODEL if model.empty?
100
+
101
+ {
102
+ name: name,
103
+ description: description,
104
+ instruction: fields['instruction'].to_s.strip,
105
+ model: model,
106
+ agent_type: agent_type,
107
+ tools: tools,
108
+ output_key: fields['output_key'].to_s.strip,
109
+ dropped_tools: dropped,
110
+ suggested_tools: build_suggested_tools(fields['suggested_tools'], dropped, valid_tool_names)
111
+ }
112
+ end
113
+
114
+ # Tools the agent wants that aren't installed — the model's explicit
115
+ # `suggested_tools` proposals plus any names it wrongly put in `tools`.
116
+ # Filtered to genuinely-missing tools and de-duped by sanitized name.
117
+ # @return [Array<Hash>] [{ name: String, description: String }]
118
+ def build_suggested_tools(explicit, dropped_from_tools, valid_tool_names)
119
+ candidates = Array(explicit).map do |st|
120
+ st.is_a?(Hash) ? { name: st['name'].to_s, description: st['description'].to_s } : { name: st.to_s, description: '' }
121
+ end
122
+ candidates += dropped_from_tools.map { |n| { name: n.to_s, description: '' } }
123
+
124
+ seen = {}
125
+ candidates.each do |st|
126
+ nm = sanitize_agent_name(st[:name])
127
+ next if nm.empty? || valid_tool_names.include?(nm)
128
+
129
+ seen[nm] ||= { name: nm, description: '' }
130
+ desc = st[:description].to_s.strip
131
+ seen[nm][:description] = desc if seen[nm][:description].empty? && !desc.empty?
132
+ end
133
+ seen.values
134
+ end
135
+
136
+ def sanitize_agent_name(raw)
137
+ raw.to_s.strip.sub(/\A:/, '').downcase.gsub(/[^a-z0-9_]+/, '_').gsub(/\A_+|_+\z/, '')
138
+ end
139
+
140
+ def build_definition_user_prompt(description)
141
+ <<~PROMPT
142
+ Create a Legate agent configuration (JSON only) for this description:
143
+
144
+ #{description}
145
+ PROMPT
146
+ end
147
+
148
+ def build_definition_prompt(available_tools)
149
+ <<~PROMPT
150
+ You configure agents for Legate — an AI Agent Framework for Ruby. Given a
151
+ description, output a single JSON object describing the agent. Output ONLY
152
+ the JSON — no markdown fences, no prose.
153
+
154
+ ## JSON schema (all keys required)
155
+ {
156
+ "name": "snake_case_unique_name",
157
+ "description": "one-line summary of what the agent does",
158
+ "instruction": "the system prompt guiding the agent's behavior (may be multi-line)",
159
+ "model": "gemini-3.5-flash",
160
+ "agent_type": "llm",
161
+ "tools": ["echo"],
162
+ "output_key": "",
163
+ "suggested_tools": [
164
+ { "name": "snake_case_tool_name", "description": "what this missing tool would do" }
165
+ ]
166
+ }
167
+
168
+ ## Rules
169
+ - "name": lowercase letters, digits and underscores only; descriptive.
170
+ - "agent_type": one of llm, sequential, parallel, loop. Prefer "llm" unless the description clearly describes a multi-agent workflow.
171
+ - "tools": ONLY names from the Available Tools list below. Never invent tools. Use [] if none fit.
172
+ - "suggested_tools": if the agent would clearly benefit from a capability that NONE of the available tools provide, propose it here with a snake_case name and a one-line description of what it should do. Do NOT put these in "tools". Use [] if every needed capability is already covered.
173
+ - "instruction": clear and detailed; explain when to use each chosen tool.
174
+ - "output_key": optional; use "" when not needed.
175
+
176
+ ## Available Tools
177
+ #{available_tools}
178
+ PROMPT
179
+ end
180
+
181
+ def validate_description!(description)
182
+ raise GenerationError, 'Description is required' if description.nil? || description.strip.empty?
183
+ raise GenerationError, 'Description too long. Maximum 5000 characters.' if description.length > 5000
184
+ end
185
+
186
+ def call_llm(adapter, system_prompt, user_prompt)
187
+ text = begin
188
+ adapter.generate("#{system_prompt}\n\n#{user_prompt}")
189
+ rescue StandardError => e
190
+ raise ApiError, "AI service communication error: #{e.message}"
191
+ end
192
+ raise GenerationError, 'AI service returned empty response. Please try again.' unless text && !text.strip.empty?
193
+
194
+ text
195
+ end
196
+
197
+ def format_available_tools
198
+ Legate::GlobalToolManager.list_all_tools.map do |tool|
199
+ tool_info = "### :#{tool[:name]}\n"
200
+ tool_info += "**Description:** #{tool[:description]}\n"
201
+
202
+ params = tool[:parameters] || {}
203
+ if params.empty?
204
+ tool_info += "**Parameters:** None\n"
205
+ else
206
+ tool_info += "**Parameters:**\n"
207
+ params.each do |param_name, param_options|
208
+ required_str = param_options[:required] ? '(required)' : '(optional)'
209
+ tool_info += " - `#{param_name}` (#{param_options[:type]}) #{required_str}: #{param_options[:description]}\n"
210
+ end
211
+ end
212
+
213
+ tool_info
214
+ end.join("\n")
215
+ end
216
+
217
+ def build_user_prompt(description)
218
+ <<~PROMPT
219
+ Generate a Ruby agent definition based on this description:
220
+
221
+ #{description}
222
+
223
+ Remember to output ONLY the Ruby code, no explanations or markdown formatting.
224
+ PROMPT
225
+ end
226
+
227
+ def clean_generated_code(code)
228
+ clean = code.strip
229
+ clean = clean.gsub(/\A```ruby\n?/, '').gsub(/\A```\n?/, '')
230
+ clean = clean.gsub(/\n?```\z/, '')
231
+ clean.strip
232
+ end
233
+
234
+ def extract_agent_name(code)
235
+ # Try to find a.name :something pattern
236
+ return Regexp.last_match(1) if code =~ /a\.name[(\s]+:(\w+)/
237
+
238
+ 'generated_agent'
239
+ end
240
+
241
+ def build_prompt(available_tools)
242
+ <<~PROMPT
243
+ You are an expert Ruby developer specializing in Legate — AI Agent Framework for Ruby.
244
+ Your task is to generate complete, production-ready Ruby agent definition code based on user descriptions.
245
+
246
+ ## Legate AgentDefinition DSL Reference
247
+
248
+ An agent is defined using the AgentDefinition DSL:
249
+
250
+ ```ruby
251
+ require 'legate'
252
+
253
+ definition = Legate::AgentDefinition.new.define do |a|
254
+ # Required fields
255
+ a.name :agent_name # Symbol, unique identifier
256
+ a.description 'What this agent does' # String, brief description
257
+ a.instruction 'System prompt...' # String, guides agent behavior
258
+
259
+ # Tools - add each tool the agent should use
260
+ a.use_tool :tool_name
261
+
262
+ # Optional: Model configuration
263
+ a.model_name '#{Legate.config.default_model_name}' # LLM model to use
264
+ a.temperature 0.7 # Creativity (0.0-1.0)
265
+ a.fallback_mode :error # :error or :echo
266
+
267
+ # Optional: Output storage
268
+ a.output_key :result_key # Store final result in session state
269
+ end
270
+
271
+ # Register the agent globally
272
+ Legate::GlobalDefinitionRegistry.register(definition)
273
+ ```
274
+
275
+ ## Agent Types
276
+
277
+ ### LLM Agent (default)
278
+ Uses an LLM for planning and tool selection:
279
+ ```ruby
280
+ a.agent_type :llm # This is the default, can be omitted
281
+ a.delegation_targets [:other_agent] # Optional: agents this one can delegate to
282
+ ```
283
+
284
+ ### Sequential Workflow Agent
285
+ Runs sub-agents in order:
286
+ ```ruby
287
+ a.agent_type :sequential
288
+ a.sequential_sub_agent_names [:first_agent, :second_agent, :third_agent]
289
+ ```
290
+
291
+ ### Parallel Workflow Agent
292
+ Runs sub-agents concurrently:
293
+ ```ruby
294
+ a.agent_type :parallel
295
+ a.parallel_sub_agent_names [:agent_a, :agent_b, :agent_c]
296
+ ```
297
+
298
+ ### Loop Workflow Agent
299
+ Runs sub-agents repeatedly until condition is met:
300
+ ```ruby
301
+ a.agent_type :loop
302
+ a.loop_sub_agent_names [:process_agent, :check_agent]
303
+ a.loop_max_iterations 10
304
+ a.loop_condition_state_key :is_complete
305
+ a.loop_condition_expected_value 'true'
306
+ ```
307
+
308
+ ## Webhook Configuration
309
+
310
+ For agents triggered by external HTTP webhooks:
311
+ ```ruby
312
+ a.webhook_enabled true
313
+ a.webhook_validator :hmac_sha256 # Or custom Proc
314
+ a.webhook_secret ENV['WEBHOOK_SECRET'] # Always use ENV vars!
315
+
316
+ # Transform incoming payload to agent input
317
+ a.webhook_transformer ->(payload) do
318
+ data = payload['data'] || payload
319
+ "Process this: \#{data.to_json}"
320
+ end
321
+
322
+ # Extract session ID from payload
323
+ a.webhook_session_extractor ->(payload) do
324
+ id = payload['id'] || payload.dig('resource', 'id') || 'default'
325
+ "webhook_session_\#{id}"
326
+ end
327
+ ```
328
+
329
+ ## Callbacks
330
+
331
+ For custom logic before/after agent and tool execution:
332
+ ```ruby
333
+ a.before_agent_callback do |context|
334
+ context.state_set(:start_time, Time.now.to_f)
335
+ nil # Return nil to continue, or a value to short-circuit
336
+ end
337
+
338
+ a.after_agent_callback do |context, response|
339
+ duration = Time.now.to_f - context.state_get(:start_time)
340
+ puts "Agent completed in \#{duration}s"
341
+ nil # Return nil to use response as-is, or modified response
342
+ end
343
+
344
+ a.before_tool_callback do |tool, args, context|
345
+ puts "Calling \#{tool.name} with \#{args}"
346
+ nil
347
+ end
348
+
349
+ a.after_tool_callback do |tool, args, context, result|
350
+ puts "Tool \#{tool.name} returned: \#{result}"
351
+ nil
352
+ end
353
+ ```
354
+
355
+ ## Available Tools
356
+
357
+ **IMPORTANT: You may ONLY use tools from the list below. Do NOT hallucinate or invent tools that are not listed here.**
358
+
359
+ The following tools are available in this Legate installation:
360
+
361
+ #{available_tools}
362
+
363
+ ## Output Requirements
364
+
365
+ 1. Output ONLY valid Ruby code - no markdown fences, no explanations
366
+ 2. Always start with `require 'legate'`
367
+ 3. Include helpful comments explaining each section
368
+ 4. Use ENV variables for any secrets (never hardcode)
369
+ 5. End with `Legate::GlobalDefinitionRegistry.register(definition)`
370
+ 6. **CRITICAL: Only use tools from the "Available Tools" section above - never invent or assume tools exist**
371
+ 7. If no existing tool matches a requirement, either omit that capability or suggest in a comment that a custom tool would need to be created
372
+ 8. Write clear, detailed instructions that guide the agent's behavior
373
+ 9. In the agent instruction, explain what tools are available and when to use each one
374
+
375
+ ## Example Output
376
+
377
+ ```ruby
378
+ # frozen_string_literal: true
379
+
380
+ require 'legate'
381
+
382
+ # Agent: Customer Support Assistant
383
+ # Handles customer inquiries and provides helpful responses
384
+ definition = Legate::AgentDefinition.new.define do |a|
385
+ a.name :customer_support
386
+ a.description 'Assists customers with questions and issues'
387
+
388
+ a.instruction <<~INSTRUCTION
389
+ You are a friendly and helpful customer support assistant.
390
+ #{' '}
391
+ Guidelines:
392
+ - Be polite and professional at all times
393
+ - Ask clarifying questions when needed
394
+ - Provide accurate information based on available tools
395
+ - Escalate complex issues appropriately
396
+ INSTRUCTION
397
+
398
+ # Tools for customer support
399
+ a.use_tool :echo
400
+
401
+ # Model configuration
402
+ a.model_name '#{Legate.config.default_model_name}'
403
+ a.temperature 0.7
404
+ end
405
+
406
+ Legate::GlobalDefinitionRegistry.register(definition)
407
+ ```
408
+ PROMPT
409
+ end
410
+ end
411
+ end
412
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
4
+
5
+ module Legate
6
+ module Generators
7
+ module CodeValidator
8
+ BLOCKED_IDENTS = %w[system exec eval instance_eval class_eval module_eval popen].freeze
9
+ BLOCKED_CONSTS = %w[Open3].freeze
10
+
11
+ class UnsafeCodeError < StandardError; end
12
+
13
+ module_function
14
+
15
+ def validate!(code)
16
+ validate_syntax!(code)
17
+ validate_no_dangerous_calls!(code)
18
+ end
19
+
20
+ def validate_syntax!(code)
21
+ sexp = Ripper.sexp(code)
22
+ raise UnsafeCodeError, 'Generated code has Ruby syntax errors and cannot be saved.' unless sexp
23
+ end
24
+
25
+ def validate_no_dangerous_calls!(code)
26
+ tokens = Ripper.lex(code)
27
+ dangerous = []
28
+
29
+ tokens.each do |(_, type, token, _)|
30
+ case type
31
+ when :on_backtick
32
+ dangerous << 'backtick command execution'
33
+ when :on_ident
34
+ dangerous << "`#{token}`" if BLOCKED_IDENTS.include?(token)
35
+ when :on_const
36
+ dangerous << "`#{token}`" if BLOCKED_CONSTS.include?(token)
37
+ end
38
+ end
39
+
40
+ return if dangerous.empty?
41
+
42
+ raise UnsafeCodeError,
43
+ "Generated code contains potentially dangerous calls: #{dangerous.uniq.join(', ')}. " \
44
+ 'Review the code manually before saving.'
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ # File: lib/legate/generators/legate/install_generator.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'rails/generators'
5
+ require 'rails/generators/migration'
6
+
7
+ module Legate
8
+ module Generators
9
+ # `rails generate legate:install` — creates the migration for the
10
+ # ActiveRecord session store and an initializer that wires it up.
11
+ class InstallGenerator < ::Rails::Generators::Base
12
+ include ::Rails::Generators::Migration
13
+
14
+ source_root File.expand_path('templates', __dir__)
15
+
16
+ desc 'Creates the Legate session-store migration and initializer.'
17
+
18
+ # Rails requires generators that create migrations to provide the next
19
+ # migration number; mirror ActiveRecord's own implementation.
20
+ def self.next_migration_number(dirname)
21
+ next_num = current_migration_number(dirname) + 1
22
+ ::ActiveRecord::Migration.next_migration_number(next_num)
23
+ end
24
+
25
+ def create_migration_file
26
+ migration_template 'create_legate_tables.rb.tt',
27
+ 'db/migrate/create_legate_tables.rb'
28
+ end
29
+
30
+ def create_initializer_file
31
+ template 'initializer.rb', 'config/initializers/legate.rb'
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Schema for Legate's ActiveRecord session store (durable sessions, events, and
4
+ # scoped state). Mirrors Legate::SessionService::ActiveRecord.create_tables!.
5
+ class CreateLegateTables < ActiveRecord::Migration[7.0]
6
+ def change
7
+ create_table :legate_sessions, id: :string do |t|
8
+ t.string :app_name
9
+ t.string :user_id
10
+ t.text :state
11
+ t.timestamps
12
+ end
13
+
14
+ create_table :legate_events do |t|
15
+ t.string :legate_session_id, null: false
16
+ t.integer :position, null: false, default: 0
17
+ t.string :role
18
+ t.text :content
19
+ t.string :tool_name
20
+ t.text :state_delta
21
+ t.string :event_timestamp
22
+ t.string :event_id
23
+ t.timestamps
24
+ end
25
+ add_index :legate_events, :legate_session_id
26
+
27
+ create_table :legate_scoped_states do |t|
28
+ t.string :scope, null: false
29
+ t.string :state_key, null: false
30
+ t.text :value
31
+ t.timestamps
32
+ end
33
+ add_index :legate_scoped_states, %i[scope state_key], unique: true,
34
+ name: 'index_legate_scoped_states_on_scope_and_key'
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Legate configuration. See https://github.com/<your-org>/legate and the guides
4
+ # under public/docs for details.
5
+ require 'legate/session_service/active_record'
6
+
7
+ Legate.configure do |config|
8
+ # Persist conversations, events, and state in your app's database. Run
9
+ # `rails generate legate:install` then `rails db:migrate` to create the tables.
10
+ config.session_service = Legate::SessionService::ActiveRecord.new
11
+
12
+ # Default model for new agents (override per-agent in the definition).
13
+ # config.default_model_name = 'gemini-3.5-flash'
14
+ end
15
+
16
+ # The gemini-ai gem reads GOOGLE_API_KEY; accept GEMINI_API_KEY as an alias so
17
+ # either env var works. Prefer Rails encrypted credentials in production.
18
+ ENV['GOOGLE_API_KEY'] ||= ENV['GEMINI_API_KEY'] if ENV['GEMINI_API_KEY']
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'code_validator'
5
+
6
+ module Legate
7
+ module Generators
8
+ # Loads an AI-generated custom tool into the RUNNING process.
9
+ #
10
+ # SECURITY: this executes LLM-generated Ruby in-process. Ruby has no true
11
+ # in-process sandbox, and CodeValidator is a denylist (blocks system/exec/eval/
12
+ # popen/Open3) — not a jail. This path is therefore gated three ways:
13
+ # 1. Config: Legate.config.allow_runtime_tool_load (default ON outside prod).
14
+ # 2. The web UI requires an explicit per-tool "this runs code" confirmation.
15
+ # 3. The source is re-validated here, server-side, before loading.
16
+ # All loads are serialized through LOAD_MUTEX and wrapped in a broad rescue so a
17
+ # bad generated tool can never crash the server. The tool is written to tools/
18
+ # so it is auditable in source control and re-loaded on next boot.
19
+ module RuntimeToolLoader
20
+ LOAD_MUTEX = Mutex.new
21
+
22
+ module_function
23
+
24
+ # @return [Boolean] whether runtime tool loading is permitted by config.
25
+ def enabled?
26
+ Legate.config.allow_runtime_tool_load
27
+ end
28
+
29
+ # Validate, persist to tools/<name>.rb, and load the tool into this process.
30
+ # Never raises — always returns a result hash.
31
+ # @param source [String] the generated Ruby tool source.
32
+ # @param suggested_name [String] basis for the file name.
33
+ # @return [Hash] { ok: true, tool_name:, path: } or { ok: false, error: }
34
+ def load_source!(source, suggested_name:)
35
+ return { ok: false, error: 'Runtime tool loading is disabled in this environment.' } unless enabled?
36
+
37
+ CodeValidator.validate!(source)
38
+
39
+ name = sanitize_name(suggested_name)
40
+ return { ok: false, error: 'Could not derive a valid tool file name.' } if name.empty?
41
+
42
+ path = File.join(tools_dir, "#{name}.rb")
43
+ before = Legate::GlobalToolManager.registered_tool_names
44
+
45
+ LOAD_MUTEX.synchronize do
46
+ FileUtils.mkdir_p(tools_dir)
47
+ File.write(path, source)
48
+ # `load` (not `require`) so re-generating the same tool reloads it.
49
+ load(path)
50
+ end
51
+
52
+ added = Legate::GlobalToolManager.registered_tool_names - before
53
+ return { ok: false, error: 'The generated code did not register a tool (missing GlobalToolManager.register_tool call).' } if added.empty?
54
+
55
+ { ok: true, tool_name: added.first.to_s, path: path }
56
+ rescue CodeValidator::UnsafeCodeError => e
57
+ { ok: false, error: e.message }
58
+ rescue Exception => e # rubocop:disable Lint/RescueException
59
+ # Broad on purpose: generated code can raise SyntaxError/NameError/LoadError
60
+ # at file scope. A bad tool must never take down the server.
61
+ Legate.logger.error("RuntimeToolLoader failed for '#{suggested_name}': #{e.class} - #{e.message}")
62
+ { ok: false, error: "#{e.class}: #{e.message}" }
63
+ end
64
+
65
+ # Where generated tools are written. Matches the boot loader's `tools/` glob
66
+ # (TOOL_DIRECTORIES) so the tool is re-loaded on the next server start.
67
+ def tools_dir
68
+ File.join(Dir.pwd, 'tools')
69
+ end
70
+
71
+ def sanitize_name(raw)
72
+ raw.to_s.strip.sub(/\A:/, '').downcase.gsub(/[^a-z0-9_]+/, '_').gsub(/\A_+|_+\z/, '')
73
+ end
74
+ end
75
+ end
76
+ end