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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/bin/legate +13 -0
- data/examples/00_quickstart.rb +51 -0
- data/examples/01_simple_agent.rb +105 -0
- data/examples/02_multi_tool_agent.rb +140 -0
- data/examples/03_custom_tool.rb +93 -0
- data/examples/04_agent_instructions.rb +84 -0
- data/examples/05_state_and_sessions.rb +91 -0
- data/examples/06_callbacks.rb +186 -0
- data/examples/07_async_jobs.rb +112 -0
- data/examples/08_loop_agent.rb +197 -0
- data/examples/09_sequential_workflow.rb +40 -0
- data/examples/10_parallel_workflow.rb +34 -0
- data/examples/11_agent_delegation.rb +24 -0
- data/examples/12_http_client_tool.rb +156 -0
- data/examples/13_authentication.rb +220 -0
- data/examples/14_mcp_client.rb +154 -0
- data/examples/15_mcp_server.rb +79 -0
- data/examples/16_webhooks.rb +91 -0
- data/examples/README_sequential_agents.md +164 -0
- data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
- data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
- data/examples/advanced/auth/excon_middleware.rb +317 -0
- data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
- data/examples/advanced/auth/fiber_auth_example.rb +281 -0
- data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
- data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
- data/examples/advanced/auth/oauth2_auth.rb +419 -0
- data/examples/advanced/auth/oidc_auth.rb +514 -0
- data/examples/advanced/auth/openweather_api.rb +251 -0
- data/examples/advanced/auth/openweather_tool.rb +153 -0
- data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
- data/examples/advanced/auth/service_account.rb +135 -0
- data/examples/advanced/auth/test_with_httpbin.rb +202 -0
- data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
- data/examples/advanced/callback_monitoring.rb +679 -0
- data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
- data/examples/advanced/mas/loop_workflow.rb +28 -0
- data/examples/advanced/mas/mock_planner.rb +77 -0
- data/examples/advanced/mas/proper_delegation_example.rb +276 -0
- data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
- data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
- data/examples/advanced/mcp/mcp_server_async.rb +76 -0
- data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
- data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
- data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
- data/examples/advanced/random_calculator.rb +104 -0
- data/examples/advanced/sleep_agent.rb +153 -0
- data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
- data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
- data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
- data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
- data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
- data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
- data/examples/tools/oauth2_example.rb +136 -0
- data/examples/tools/sleepy_tool.rb +42 -0
- data/lib/legate/activity_log.rb +71 -0
- data/lib/legate/agent.rb +959 -0
- data/lib/legate/agent_code_generator.rb +185 -0
- data/lib/legate/agent_definition.rb +812 -0
- data/lib/legate/agentic/decision.rb +49 -0
- data/lib/legate/agentic/loop.rb +134 -0
- data/lib/legate/agentic.rb +5 -0
- data/lib/legate/agents/loop_agent.rb +248 -0
- data/lib/legate/agents/parallel_agent.rb +163 -0
- data/lib/legate/agents/sequential_agent.rb +190 -0
- data/lib/legate/agents.rb +14 -0
- data/lib/legate/auth/config.rb +148 -0
- data/lib/legate/auth/coordinator.rb +218 -0
- data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
- data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
- data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
- data/lib/legate/auth/credential.rb +157 -0
- data/lib/legate/auth/encryption.rb +108 -0
- data/lib/legate/auth/error.rb +94 -0
- data/lib/legate/auth/exchanged_credential.rb +180 -0
- data/lib/legate/auth/excon_middleware.rb +285 -0
- data/lib/legate/auth/http_client_utils.rb +364 -0
- data/lib/legate/auth/manager.rb +531 -0
- data/lib/legate/auth/manager_store.rb +394 -0
- data/lib/legate/auth/middleware_factory.rb +290 -0
- data/lib/legate/auth/runner.rb +279 -0
- data/lib/legate/auth/scheme.rb +125 -0
- data/lib/legate/auth/schemes/api_key.rb +212 -0
- data/lib/legate/auth/schemes/google_service_account.rb +108 -0
- data/lib/legate/auth/schemes/http_bearer.rb +98 -0
- data/lib/legate/auth/schemes/oauth2.rb +396 -0
- data/lib/legate/auth/schemes/openid_connect.rb +346 -0
- data/lib/legate/auth/schemes/service_account.rb +388 -0
- data/lib/legate/auth/schemes.rb +40 -0
- data/lib/legate/auth/token_manager.rb +362 -0
- data/lib/legate/auth/token_store.rb +86 -0
- data/lib/legate/auth/tool_context_extension.rb +97 -0
- data/lib/legate/auth/tool_integration.rb +188 -0
- data/lib/legate/auth/url_guard.rb +81 -0
- data/lib/legate/auth.rb +453 -0
- data/lib/legate/callbacks/callback_context.rb +71 -0
- data/lib/legate/cli/agent_commands.rb +950 -0
- data/lib/legate/cli/auth_commands.rb +520 -0
- data/lib/legate/cli/base_command.rb +24 -0
- data/lib/legate/cli/deployment_commands.rb +934 -0
- data/lib/legate/cli/output_helper.rb +108 -0
- data/lib/legate/cli/session_commands.rb +138 -0
- data/lib/legate/cli/skaffold_commands.rb +223 -0
- data/lib/legate/cli/tool_commands.rb +261 -0
- data/lib/legate/cli/web_commands.rb +182 -0
- data/lib/legate/cli.rb +40 -0
- data/lib/legate/configuration/webhooks.rb +113 -0
- data/lib/legate/configuration.rb +39 -0
- data/lib/legate/definition_store.rb +23 -0
- data/lib/legate/errors.rb +118 -0
- data/lib/legate/event.rb +161 -0
- data/lib/legate/gemini_ai_beta_patch.rb +39 -0
- data/lib/legate/generators/agent_generator.rb +412 -0
- data/lib/legate/generators/code_validator.rb +48 -0
- data/lib/legate/generators/legate/install_generator.rb +35 -0
- data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
- data/lib/legate/generators/legate/templates/initializer.rb +18 -0
- data/lib/legate/generators/runtime_tool_loader.rb +76 -0
- data/lib/legate/generators/tool_generator.rb +408 -0
- data/lib/legate/generators.rb +11 -0
- data/lib/legate/global_definition_registry.rb +506 -0
- data/lib/legate/global_tool_manager.rb +135 -0
- data/lib/legate/llm/adapter.rb +69 -0
- data/lib/legate/llm/gemini.rb +172 -0
- data/lib/legate/llm/ollama.rb +80 -0
- data/lib/legate/llm.rb +34 -0
- data/lib/legate/mcp/client.rb +320 -0
- data/lib/legate/mcp/connection/sse.rb +292 -0
- data/lib/legate/mcp/connection/stdio.rb +273 -0
- data/lib/legate/mcp/connection_manager.rb +103 -0
- data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
- data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
- data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
- data/lib/legate/mcp/tool_wrapper.rb +138 -0
- data/lib/legate/mcp/util/schema_converter.rb +134 -0
- data/lib/legate/mcp.rb +23 -0
- data/lib/legate/plan_executor.rb +375 -0
- data/lib/legate/planner.rb +839 -0
- data/lib/legate/rails/railtie.rb +43 -0
- data/lib/legate/rails.rb +9 -0
- data/lib/legate/redaction.rb +32 -0
- data/lib/legate/session.rb +299 -0
- data/lib/legate/session_service/active_record.rb +300 -0
- data/lib/legate/session_service/base.rb +68 -0
- data/lib/legate/session_service/event_broadcast.rb +74 -0
- data/lib/legate/session_service/in_memory.rb +188 -0
- data/lib/legate/tool/metadata_dsl.rb +122 -0
- data/lib/legate/tool.rb +276 -0
- data/lib/legate/tool_code_generator.rb +103 -0
- data/lib/legate/tool_context.rb +350 -0
- data/lib/legate/tool_loader.rb +39 -0
- data/lib/legate/tool_registry.rb +73 -0
- data/lib/legate/tool_result.rb +61 -0
- data/lib/legate/tools/agent_tool.rb +187 -0
- data/lib/legate/tools/base/http_client.rb +319 -0
- data/lib/legate/tools/base/safe_url.rb +56 -0
- data/lib/legate/tools/base_async_job_tool.rb +91 -0
- data/lib/legate/tools/calculator.rb +89 -0
- data/lib/legate/tools/cat_facts.rb +81 -0
- data/lib/legate/tools/check_job_status_tool.rb +48 -0
- data/lib/legate/tools/current_time_tool.rb +64 -0
- data/lib/legate/tools/echo.rb +43 -0
- data/lib/legate/tools/http_request_tool.rb +105 -0
- data/lib/legate/tools/random_number_tool.rb +64 -0
- data/lib/legate/tools/read_webpage_tool.rb +92 -0
- data/lib/legate/tools/sleepy_tool.rb +74 -0
- data/lib/legate/tools/webhook_tool.rb +146 -0
- data/lib/legate/version.rb +5 -0
- data/lib/legate/web/app.rb +984 -0
- data/lib/legate/web/public/css/main.css +4980 -0
- data/lib/legate/web/public/images/favicon-256.png +0 -0
- data/lib/legate/web/public/images/favicon-32.png +0 -0
- data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
- data/lib/legate/web/public/images/legate-logo-light.png +0 -0
- data/lib/legate/web/public/js/legate.js +616 -0
- data/lib/legate/web/public/styles/main.scss +4402 -0
- data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
- data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
- data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
- data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
- data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
- data/lib/legate/web/routes/api_routes.rb +56 -0
- data/lib/legate/web/routes/authentication_routes.rb +1541 -0
- data/lib/legate/web/routes/core_routes.rb +111 -0
- data/lib/legate/web/routes/documentation_routes.rb +220 -0
- data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
- data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
- data/lib/legate/web/sass_compiler.rb +73 -0
- data/lib/legate/web/views/_active_session_info.slim +25 -0
- data/lib/legate/web/views/_activity_list.slim +55 -0
- data/lib/legate/web/views/_agent_card.slim +56 -0
- data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
- data/lib/legate/web/views/_agent_status_controls.slim +71 -0
- data/lib/legate/web/views/_agent_tool_table.slim +74 -0
- data/lib/legate/web/views/_chat_message.slim +95 -0
- data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
- data/lib/legate/web/views/_display_agent_description.slim +11 -0
- data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
- data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
- data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
- data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
- data/lib/legate/web/views/_display_agent_model.slim +17 -0
- data/lib/legate/web/views/_display_agent_name.slim +42 -0
- data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
- data/lib/legate/web/views/_display_agent_type.slim +65 -0
- data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
- data/lib/legate/web/views/_edit_agent_description.slim +16 -0
- data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
- data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
- data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
- data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
- data/lib/legate/web/views/_edit_agent_model.slim +23 -0
- data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
- data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
- data/lib/legate/web/views/_edit_agent_type.slim +67 -0
- data/lib/legate/web/views/_session_error.slim +4 -0
- data/lib/legate/web/views/_skeleton.slim +69 -0
- data/lib/legate/web/views/_tool_card.slim +9 -0
- data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
- data/lib/legate/web/views/agent.slim +436 -0
- data/lib/legate/web/views/agent_auth.slim +562 -0
- data/lib/legate/web/views/agents.slim +369 -0
- data/lib/legate/web/views/auth.slim +112 -0
- data/lib/legate/web/views/auth_credential_detail.slim +327 -0
- data/lib/legate/web/views/auth_credentials.slim +261 -0
- data/lib/legate/web/views/auth_debug.slim +94 -0
- data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
- data/lib/legate/web/views/auth_mapping_new.slim +123 -0
- data/lib/legate/web/views/auth_mappings.slim +120 -0
- data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
- data/lib/legate/web/views/auth_schemes.slim +259 -0
- data/lib/legate/web/views/auth_test.slim +418 -0
- data/lib/legate/web/views/chat.slim +192 -0
- data/lib/legate/web/views/docs_index.slim +105 -0
- data/lib/legate/web/views/docs_show.slim +105 -0
- data/lib/legate/web/views/error_404.slim +5 -0
- data/lib/legate/web/views/index.slim +148 -0
- data/lib/legate/web/views/layout.slim +144 -0
- data/lib/legate/web/views/tool_detail.slim +87 -0
- data/lib/legate/web/views/tools.slim +50 -0
- data/lib/legate/web/webhook_listener.rb +367 -0
- data/lib/legate/web.rb +9 -0
- data/lib/legate.rb +220 -0
- data/public/docs/advanced/callbacks.md +828 -0
- data/public/docs/advanced/mcp_schema_conversion.md +59 -0
- data/public/docs/authentication/api_reference/config.md +210 -0
- data/public/docs/authentication/api_reference/credential.md +246 -0
- data/public/docs/authentication/api_reference/encryption.md +218 -0
- data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
- data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
- data/public/docs/authentication/api_reference/index.md +30 -0
- data/public/docs/authentication/api_reference/scheme.md +250 -0
- data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
- data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
- data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
- data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
- data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
- data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
- data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
- data/public/docs/authentication/api_reference/token_manager.md +221 -0
- data/public/docs/authentication/api_reference/token_store.md +146 -0
- data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
- data/public/docs/authentication/guides/api_key.md +190 -0
- data/public/docs/authentication/guides/bearer.md +172 -0
- data/public/docs/authentication/guides/configuration.md +255 -0
- data/public/docs/authentication/guides/custom_flow.md +523 -0
- data/public/docs/authentication/guides/index.md +24 -0
- data/public/docs/authentication/guides/migration.md +435 -0
- data/public/docs/authentication/guides/oauth2.md +252 -0
- data/public/docs/authentication/guides/oidc.md +241 -0
- data/public/docs/authentication/guides/overview.md +155 -0
- data/public/docs/authentication/guides/secure_storage.md +301 -0
- data/public/docs/authentication/guides/service_account.md +228 -0
- data/public/docs/authentication/guides/token_lifecycle.md +295 -0
- data/public/docs/authentication/guides/web_ui_integration.md +504 -0
- data/public/docs/authentication/index.md +58 -0
- data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
- data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
- data/public/docs/authentication/troubleshooting/index.md +11 -0
- data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
- data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
- data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
- data/public/docs/cli/legate_cli_usage.md +363 -0
- data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
- data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
- data/public/docs/core_concepts/legate_configuration.md +116 -0
- data/public/docs/core_concepts/legate_definition_store.md +102 -0
- data/public/docs/core_concepts/legate_planner.md +94 -0
- data/public/docs/core_concepts/legate_session_service.md +104 -0
- data/public/docs/error_handling/legate_error_handling.md +122 -0
- data/public/docs/examples.md +199 -0
- data/public/docs/getting_started.md +111 -0
- data/public/docs/guides/agentic_agents.md +137 -0
- data/public/docs/guides/ai_code_generators.md +437 -0
- data/public/docs/guides/auto_loading.md +326 -0
- data/public/docs/guides/configuring_agent_webhooks.md +219 -0
- data/public/docs/guides/http_client_usage.md +264 -0
- data/public/docs/guides/llm_providers.md +137 -0
- data/public/docs/guides/mcp_client_integration.md +232 -0
- data/public/docs/guides/mcp_server_exposure.md +206 -0
- data/public/docs/guides/rails_integration.md +128 -0
- data/public/docs/guides/sending_outbound_webhooks.md +227 -0
- data/public/docs/guides/streaming.md +112 -0
- data/public/docs/guides/webhooks.md +288 -0
- data/public/docs/introduction.md +51 -0
- data/public/docs/multi_agent_systems/advanced_features.md +57 -0
- data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
- data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
- data/public/docs/multi_agent_systems/state_management.md +47 -0
- data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
- data/public/docs/tools/legate_built_in_tools.md +332 -0
- data/public/docs/tools/legate_tools_and_registry.md +263 -0
- data/public/docs/web_ui/legate_web_ui.md +137 -0
- 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
|