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,89 @@
|
|
|
1
|
+
# File: lib/legate/tools/calculator.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Removed require 'logger' - Use Legate.logger
|
|
5
|
+
require_relative '../tool' # Ensure base class is loaded
|
|
6
|
+
|
|
7
|
+
module Legate
|
|
8
|
+
module Tools
|
|
9
|
+
# A simple calculator tool supporting basic arithmetic operations.
|
|
10
|
+
class Calculator < Tool
|
|
11
|
+
# --- New DSL Metadata ---
|
|
12
|
+
# Name :calculator will be inferred
|
|
13
|
+
tool_description 'Calculates the result of an arithmetic operation. Requires two numbers (operand1, operand2) and the operation name (operation: "add", "subtract", "multiply", "divide", or symbols +, -, *, /).'
|
|
14
|
+
|
|
15
|
+
parameter :operand1,
|
|
16
|
+
type: :numeric,
|
|
17
|
+
description: 'The first number for the calculation.',
|
|
18
|
+
required: true
|
|
19
|
+
|
|
20
|
+
parameter :operand2,
|
|
21
|
+
type: :numeric,
|
|
22
|
+
description: 'The second number for the calculation.',
|
|
23
|
+
required: true
|
|
24
|
+
|
|
25
|
+
parameter :operation,
|
|
26
|
+
type: :string,
|
|
27
|
+
description: 'The operation to perform (e.g., "add", "subtract", "multiply", "divide", "+", "-", "*", "/").',
|
|
28
|
+
required: true
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# @param params [Hash] Contains operand1, operand2, operation.
|
|
33
|
+
# @param _context [Legate::ToolContext, nil] The execution context (unused here).
|
|
34
|
+
def perform_execution(params, _context)
|
|
35
|
+
op1_str = params.fetch('operand1') { params.fetch(:operand1, nil) }
|
|
36
|
+
op2_str = params.fetch('operand2') { params.fetch(:operand2, nil) }
|
|
37
|
+
operation = params.fetch('operation') { params.fetch(:operation, nil) }&.to_s&.downcase
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
# Validate inputs first
|
|
41
|
+
begin
|
|
42
|
+
op1 = Float(op1_str)
|
|
43
|
+
op2 = Float(op2_str)
|
|
44
|
+
rescue ArgumentError, TypeError
|
|
45
|
+
err_msg = "Invalid numeric input provided for operands. Op1: '#{op1_str}', Op2: '#{op2_str}'"
|
|
46
|
+
Legate.logger.error("Calculator Tool Argument Error: #{err_msg}")
|
|
47
|
+
raise Legate::ToolArgumentError, err_msg
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
valid_ops = %w[add subtract multiply divide + - * /]
|
|
51
|
+
unless valid_ops.include?(operation)
|
|
52
|
+
err_msg = "Unsupported operation: '#{operation}'. Use add, subtract, multiply, or divide (or +, -, *, /)."
|
|
53
|
+
Legate.logger.warn("Calculator Tool Argument Warning: #{err_msg}")
|
|
54
|
+
raise Legate::ToolArgumentError, err_msg
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if %w[divide /].include?(operation) && op2.zero?
|
|
58
|
+
err_msg = 'Division by zero is not allowed.'
|
|
59
|
+
Legate.logger.error("Calculator Tool Argument Error: #{err_msg}")
|
|
60
|
+
raise Legate::ToolArgumentError, err_msg
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Perform calculation
|
|
64
|
+
result_val = case operation
|
|
65
|
+
when 'add', '+' then op1 + op2
|
|
66
|
+
when 'subtract', '-' then op1 - op2
|
|
67
|
+
when 'multiply', '*' then op1 * op2
|
|
68
|
+
when 'divide', '/' then op1 / op2 # Zero already checked
|
|
69
|
+
# No else needed due to validation above
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
Legate.logger.info("Calculator Tool: #{op1} #{operation} #{op2} = #{result_val}")
|
|
73
|
+
{ status: :success, result: result_val }
|
|
74
|
+
|
|
75
|
+
# Catch potential unexpected errors during execution logic itself
|
|
76
|
+
rescue Legate::ToolArgumentError => e # Re-raise argument errors
|
|
77
|
+
raise e
|
|
78
|
+
rescue Legate::ToolError => e # Catch specific Legate tool errors if they occur somehow
|
|
79
|
+
Legate.logger.error("Calculator Tool Legate::ToolError: #{e.message}")
|
|
80
|
+
raise e # Re-raise to be handled by the agent
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
# Wrap unexpected errors in a ToolError
|
|
83
|
+
Legate.logger.error("Calculator Tool: Unexpected internal error during calculation: #{e.class} - #{e.message}")
|
|
84
|
+
raise Legate::ToolError, "Calculation failed due to an unexpected internal error: #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
end # End perform_execution
|
|
87
|
+
end # End Calculator class
|
|
88
|
+
end # End Tools module
|
|
89
|
+
end # End Legate module
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# File: lib/legate/tools/cat_facts.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Removed Faraday and JSON requires, handled by HttpClient
|
|
5
|
+
require_relative '../tool'
|
|
6
|
+
require_relative 'base/http_client' # Include the base module
|
|
7
|
+
|
|
8
|
+
module Legate
|
|
9
|
+
module Tools
|
|
10
|
+
# Tool to fetch a random cat fact from an online API.
|
|
11
|
+
class CatFacts < Legate::Tool
|
|
12
|
+
include Legate::Tools::Base::HttpClient # Include the mixin
|
|
13
|
+
|
|
14
|
+
# --- New DSL Metadata ---
|
|
15
|
+
# Name :cat_facts will be inferred
|
|
16
|
+
tool_description 'Fetches a random cat fact from an online API.'
|
|
17
|
+
# No parameters needed, so no `parameter` calls.
|
|
18
|
+
# --- End New DSL Metadata ---
|
|
19
|
+
|
|
20
|
+
# The base URL for the Cat Fact API.
|
|
21
|
+
CAT_FACT_BASE_URL = 'https://catfact.ninja'
|
|
22
|
+
|
|
23
|
+
# Initializes the tool instance.
|
|
24
|
+
# Sets up the HTTP client using the base module.
|
|
25
|
+
def initialize(**options)
|
|
26
|
+
super(**options)
|
|
27
|
+
# Use the base module to set up the client
|
|
28
|
+
# It handles initialization errors and logging internally.
|
|
29
|
+
setup_http_client(base_url: CAT_FACT_BASE_URL)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# The main execution method required by the Legate::Tool base class.
|
|
35
|
+
# It delegates the actual work to the fetch_cat_fact helper method.
|
|
36
|
+
def perform_execution(_params, _context)
|
|
37
|
+
fetch_cat_fact
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Helper method to perform the HTTP request using the HttpClient module
|
|
41
|
+
# and handle responses/errors.
|
|
42
|
+
# Returns the standardized result hash.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash] A hash with :status (:success or :error) and :result/:error_message.
|
|
45
|
+
# @raise [Legate::ToolError] Propagates errors from http_get, parse_json_response, or validation.
|
|
46
|
+
def fetch_cat_fact
|
|
47
|
+
Legate.logger.info('Fetching cat fact using HttpClient...')
|
|
48
|
+
|
|
49
|
+
# Perform the GET request using the base module helper
|
|
50
|
+
# Network/HTTP/Timeout errors are automatically handled and raised as Legate::ToolError
|
|
51
|
+
response = http_get('/fact')
|
|
52
|
+
|
|
53
|
+
# Parse the JSON response body directly
|
|
54
|
+
begin
|
|
55
|
+
data = JSON.parse(response.body)
|
|
56
|
+
rescue JSON::ParserError => e
|
|
57
|
+
raise Legate::ToolError.new("Failed to parse JSON response from Cat Fact API: #{e.message}", cause: e)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
fact = data['fact'] # Extract the 'fact' field
|
|
61
|
+
|
|
62
|
+
# Check if a valid fact was received
|
|
63
|
+
if fact && !fact.empty?
|
|
64
|
+
Legate.logger.info('Cat fact fetched successfully.')
|
|
65
|
+
{ status: :success, result: fact }
|
|
66
|
+
else
|
|
67
|
+
# Raise an error if the expected field is missing or empty
|
|
68
|
+
err_msg = "Cat fact API response did not contain a valid 'fact' field."
|
|
69
|
+
Legate.logger.warn(err_msg)
|
|
70
|
+
raise Legate::ToolError, err_msg
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# No need for extensive rescue blocks here anymore.
|
|
74
|
+
# Legate::ToolError from http_get, parse_json_response, or the validation
|
|
75
|
+
# will propagate up and be handled by the Legate runtime.
|
|
76
|
+
# StandardError might still occur in unexpected places, but the base
|
|
77
|
+
# HttpClient tries to catch most common issues.
|
|
78
|
+
end # end fetch_cat_fact
|
|
79
|
+
end # End CatFacts class
|
|
80
|
+
end # End Tools module
|
|
81
|
+
end # End Legate module
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# File: lib/legate/tools/check_job_status_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module Legate
|
|
9
|
+
module Tools
|
|
10
|
+
# A built-in tool to check the status and retrieve results for a background job
|
|
11
|
+
# initiated by a Legate async tool.
|
|
12
|
+
class CheckJobStatusTool < Legate::Tool
|
|
13
|
+
# --- New DSL Metadata ---
|
|
14
|
+
# Name :check_job_status_tool will be inferred
|
|
15
|
+
self.explicit_tool_name = :check_job_status # Keep original name
|
|
16
|
+
|
|
17
|
+
tool_description 'Checks the status and retrieves the result of a previously started background job using its ID.'
|
|
18
|
+
|
|
19
|
+
parameter :job_id,
|
|
20
|
+
type: :string,
|
|
21
|
+
description: 'The ID of the job to check status for',
|
|
22
|
+
required: true
|
|
23
|
+
# --- End New DSL Metadata ---
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @param params [Hash] Must contain :job_id.
|
|
28
|
+
# @param _context [Legate::ToolContext, nil] The execution context (unused here).
|
|
29
|
+
# @return [Hash] { status: :success/:error, ... }
|
|
30
|
+
def perform_execution(params, _context)
|
|
31
|
+
job_id = params[:job_id]
|
|
32
|
+
return { status: :error, error_message: 'Missing job_id parameter.' } unless job_id && !job_id.strip.empty?
|
|
33
|
+
|
|
34
|
+
result = Legate::Tools::BaseAsyncJobTool.job_results[job_id]
|
|
35
|
+
|
|
36
|
+
if result.nil?
|
|
37
|
+
{ status: :success, job_id: job_id, job_status: 'unknown', message: 'Job ID not found.' }
|
|
38
|
+
else
|
|
39
|
+
job_status = result['status']
|
|
40
|
+
response = { status: :success, job_id: job_id, job_status: job_status }
|
|
41
|
+
response[:result] = result['result'] if result['result']
|
|
42
|
+
response[:error_message] = result['error_message'] if result['error_message']
|
|
43
|
+
response
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# File: lib/legate/tools/current_time_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module Legate
|
|
8
|
+
module Tools
|
|
9
|
+
# Returns the current date and time.
|
|
10
|
+
#
|
|
11
|
+
# Language models don't know the current time, so this is a common building
|
|
12
|
+
# block for scheduling, "how long ago", and freshness checks. Returns UTC by
|
|
13
|
+
# default; accepts "UTC", "local", or a fixed UTC offset (e.g. "+09:00").
|
|
14
|
+
# Named IANA zones (e.g. "America/New_York") are intentionally not supported
|
|
15
|
+
# to avoid a timezone-database dependency and process-global TZ mutation.
|
|
16
|
+
class CurrentTime < Legate::Tool
|
|
17
|
+
OFFSET_PATTERN = /\A[+-]\d{2}:?\d{2}\z/
|
|
18
|
+
|
|
19
|
+
tool_name # inferred: :current_time
|
|
20
|
+
tool_description 'Returns the current date and time (ISO 8601, epoch, and an optional custom format). ' \
|
|
21
|
+
'Accepts a timezone of "UTC" (default), "local", or a fixed UTC offset like "+09:00".'
|
|
22
|
+
|
|
23
|
+
parameter :timezone, type: :string, required: false,
|
|
24
|
+
description: 'Timezone: "UTC" (default), "local", or a fixed offset such as "+05:30" or "-0800".'
|
|
25
|
+
parameter :format, type: :string, required: false,
|
|
26
|
+
description: 'Optional strftime format (e.g. "%A, %B %-d, %Y"). Defaults to ISO 8601.'
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def perform_execution(params, _context)
|
|
31
|
+
now = Time.now.utc
|
|
32
|
+
tz = params[:timezone].to_s.strip
|
|
33
|
+
base = localize(now, tz)
|
|
34
|
+
return { status: :error, error_message: unsupported_tz_message(tz) } unless base
|
|
35
|
+
|
|
36
|
+
fmt = params[:format].to_s
|
|
37
|
+
{
|
|
38
|
+
status: :success,
|
|
39
|
+
result: {
|
|
40
|
+
iso8601: base.iso8601,
|
|
41
|
+
formatted: fmt.empty? ? base.iso8601 : base.strftime(fmt),
|
|
42
|
+
epoch: now.to_i,
|
|
43
|
+
timezone: tz.empty? ? 'UTC' : tz
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
rescue ArgumentError => e
|
|
47
|
+
{ status: :error, error_message: "Invalid format or timezone: #{e.message}" }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def localize(utc_now, zone)
|
|
51
|
+
case zone.downcase
|
|
52
|
+
when '', 'utc', 'z' then utc_now
|
|
53
|
+
when 'local' then utc_now.getlocal
|
|
54
|
+
else
|
|
55
|
+
zone.match?(OFFSET_PATTERN) ? utc_now.getlocal(zone) : nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unsupported_tz_message(zone)
|
|
60
|
+
"Unsupported timezone '#{zone}'. Use 'UTC', 'local', or a fixed offset like '+09:00'."
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# File: lib/legate/tools/echo.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
|
|
6
|
+
module Legate
|
|
7
|
+
module Tools
|
|
8
|
+
class Echo < Tool
|
|
9
|
+
# --- New DSL Metadata ---
|
|
10
|
+
# Name :echo will be inferred
|
|
11
|
+
tool_description 'Echoes back the provided message.'
|
|
12
|
+
|
|
13
|
+
parameter :message,
|
|
14
|
+
type: :string,
|
|
15
|
+
description: 'The message to echo',
|
|
16
|
+
required: true
|
|
17
|
+
# --- End New DSL Metadata ---
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# @param params [Hash] Contains :message.
|
|
22
|
+
# @param _context [Legate::ToolContext, nil] The execution context (unused here).
|
|
23
|
+
def perform_execution(params, _context)
|
|
24
|
+
# Fetch validated parameter
|
|
25
|
+
message = params.fetch('message') { params.fetch(:message, nil) }
|
|
26
|
+
|
|
27
|
+
# This check is belts-and-suspenders; validation should catch missing required params.
|
|
28
|
+
unless message
|
|
29
|
+
err_msg = 'Internal Error: Message parameter missing in perform_execution for Echo tool after validation.'
|
|
30
|
+
Legate.logger.error(err_msg)
|
|
31
|
+
raise Legate::ToolError, err_msg
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Simple success case
|
|
35
|
+
{ status: :success, result: message }
|
|
36
|
+
rescue StandardError => e # Catch any truly unexpected errors during fetch/processing
|
|
37
|
+
Legate.logger.error("Echo Tool: Unexpected error: #{e.class} - #{e.message}")
|
|
38
|
+
# Wrap unexpected errors in a ToolError
|
|
39
|
+
raise Legate::ToolError, "Unexpected error in Echo tool: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
end # End Echo class
|
|
42
|
+
end # End Tools module
|
|
43
|
+
end # End Legate module
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# File: lib/legate/tools/http_request_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
require_relative 'base/http_client'
|
|
6
|
+
require_relative 'base/safe_url'
|
|
7
|
+
|
|
8
|
+
module Legate
|
|
9
|
+
module Tools
|
|
10
|
+
# General-purpose HTTP client tool.
|
|
11
|
+
#
|
|
12
|
+
# Makes a request to a URL and returns the status code, response headers, and
|
|
13
|
+
# body. It is SSRF-safe (private/loopback/link-local hosts are blocked and the
|
|
14
|
+
# connection is pinned to the validated IP) and auth-aware (configured auth
|
|
15
|
+
# URL-mappings are applied automatically; pass `headers` for manual auth).
|
|
16
|
+
#
|
|
17
|
+
# A non-2xx response is returned as a normal result (with its status_code) so
|
|
18
|
+
# an agent can inspect it; only network/SSRF/timeout failures are errors.
|
|
19
|
+
class HttpRequest < Legate::Tool
|
|
20
|
+
include Legate::Tools::Base::HttpClient
|
|
21
|
+
|
|
22
|
+
# Cap the returned body so a large download can't blow up the context/LLM.
|
|
23
|
+
MAX_BODY_BYTES = 1_000_000
|
|
24
|
+
ALLOWED_METHODS = %w[GET POST PUT PATCH DELETE HEAD].freeze
|
|
25
|
+
|
|
26
|
+
tool_name # inferred: :http_request
|
|
27
|
+
tool_description 'Makes an HTTP request to a URL and returns the status code, headers, and body. ' \
|
|
28
|
+
'Supports GET (default), POST, PUT, PATCH, DELETE, and HEAD. Blocks private and ' \
|
|
29
|
+
'loopback addresses (SSRF-safe) and applies configured authentication for matching URLs.'
|
|
30
|
+
|
|
31
|
+
parameter :url, type: :string, required: true,
|
|
32
|
+
description: 'The full URL to request (must be http or https).'
|
|
33
|
+
parameter :method, type: :string, required: false,
|
|
34
|
+
description: 'HTTP method: GET (default), POST, PUT, PATCH, DELETE, or HEAD.'
|
|
35
|
+
parameter :headers, type: :hash, required: false,
|
|
36
|
+
description: 'Optional request headers.'
|
|
37
|
+
parameter :body, type: %i[hash string], required: false,
|
|
38
|
+
description: 'Optional request body. A Hash is JSON-encoded with Content-Type: application/json.'
|
|
39
|
+
parameter :query, type: :hash, required: false,
|
|
40
|
+
description: 'Optional query-string parameters.'
|
|
41
|
+
|
|
42
|
+
def initialize(**options)
|
|
43
|
+
super(**options)
|
|
44
|
+
# Targets are passed absolute per-request; the base URL is only a required placeholder.
|
|
45
|
+
setup_http_client(base_url: 'https://placeholder.invalid')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def perform_execution(params, context)
|
|
51
|
+
url = params.fetch(:url)
|
|
52
|
+
method = (params[:method] || 'GET').to_s.upcase
|
|
53
|
+
return { status: :error, error_message: "Unsupported HTTP method: #{method}" } unless ALLOWED_METHODS.include?(method)
|
|
54
|
+
|
|
55
|
+
uri, pinned_ip = Legate::Tools::Base::SafeUrl.resolve!(url)
|
|
56
|
+
headers = apply_auth(context, method, url, stringify_headers(params[:headers] || {}))
|
|
57
|
+
|
|
58
|
+
response = make_request(
|
|
59
|
+
method.downcase.to_sym, url,
|
|
60
|
+
body: params[:body],
|
|
61
|
+
query: params[:query] || {},
|
|
62
|
+
headers: headers,
|
|
63
|
+
options: { resolved_ip: pinned_ip, original_host: uri.host }
|
|
64
|
+
)
|
|
65
|
+
{ status: :success, result: build_result(url, response) }
|
|
66
|
+
rescue Legate::ToolHttpError => e
|
|
67
|
+
# A non-2xx response still completed; surface its details rather than erroring.
|
|
68
|
+
return { status: :success, result: build_result(url, e.response) } if e.response
|
|
69
|
+
|
|
70
|
+
{ status: :error, error_message: e.message }
|
|
71
|
+
rescue Legate::ToolError => e
|
|
72
|
+
{ status: :error, error_message: e.message }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Let the execution context apply any configured auth (URL mappings). It is a
|
|
76
|
+
# no-op when no auth is configured or the context doesn't support it.
|
|
77
|
+
def apply_auth(context, method, url, headers)
|
|
78
|
+
return headers unless context.respond_to?(:handle_request_auth)
|
|
79
|
+
|
|
80
|
+
request = context.handle_request_auth({ method: method.downcase.to_sym, url: url, headers: headers })
|
|
81
|
+
request.is_a?(Hash) && request[:headers] ? request[:headers] : headers
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
Legate.logger.warn("HttpRequest: auth application failed, sending unauthenticated: #{e.message}")
|
|
84
|
+
headers
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_result(url, response)
|
|
88
|
+
body = response.body.to_s
|
|
89
|
+
truncated = body.bytesize > MAX_BODY_BYTES
|
|
90
|
+
body = body.byteslice(0, MAX_BODY_BYTES) if truncated
|
|
91
|
+
{
|
|
92
|
+
url: url,
|
|
93
|
+
status_code: response.status,
|
|
94
|
+
headers: response.headers,
|
|
95
|
+
body: body,
|
|
96
|
+
truncated: truncated
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def stringify_headers(hash)
|
|
101
|
+
hash.transform_keys(&:to_s)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# File: lib/legate/tools/random_number_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
|
|
6
|
+
module Legate
|
|
7
|
+
module Tools
|
|
8
|
+
class RandomNumberTool < Legate::Tool
|
|
9
|
+
# --- New DSL Metadata ---
|
|
10
|
+
# Name will be inferred as :random_number_tool
|
|
11
|
+
self.explicit_tool_name = :random_number # Keep original name
|
|
12
|
+
|
|
13
|
+
tool_description 'Generates a random integer between a minimum and maximum value (inclusive). Defaults to 1-100.'
|
|
14
|
+
|
|
15
|
+
parameter :min,
|
|
16
|
+
type: :integer,
|
|
17
|
+
description: 'The minimum value for the random number (inclusive).',
|
|
18
|
+
required: false
|
|
19
|
+
|
|
20
|
+
parameter :max,
|
|
21
|
+
type: :integer,
|
|
22
|
+
description: 'The maximum value for the random number (inclusive).',
|
|
23
|
+
required: false
|
|
24
|
+
# --- End New DSL Metadata ---
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# @param params [Hash] Contains min and max.
|
|
29
|
+
# @param _context [Legate::ToolContext, nil] The execution context (unused here).
|
|
30
|
+
def perform_execution(params, _context)
|
|
31
|
+
# Fetch parameters safely, providing defaults
|
|
32
|
+
min_val_str = params.fetch('min') { params.fetch(:min, '1') }
|
|
33
|
+
max_val_str = params.fetch('max') { params.fetch(:max, '100') }
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
min_val = Integer(min_val_str)
|
|
37
|
+
max_val = Integer(max_val_str)
|
|
38
|
+
rescue ArgumentError, TypeError
|
|
39
|
+
err_msg = "Invalid integer input provided for min or max. Min: '#{min_val_str}', Max: '#{max_val_str}'"
|
|
40
|
+
Legate.logger.error("RandomNumberTool Argument Error: #{err_msg}")
|
|
41
|
+
raise Legate::ToolArgumentError, err_msg
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check logical constraint
|
|
45
|
+
if min_val > max_val
|
|
46
|
+
err_msg = "Min value (#{min_val}) cannot be greater than Max value (#{max_val})."
|
|
47
|
+
Legate.logger.error("RandomNumberTool Argument Error: #{err_msg}")
|
|
48
|
+
raise Legate::ToolArgumentError, err_msg
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Perform the core logic
|
|
52
|
+
random_num = rand(min_val..max_val)
|
|
53
|
+
Legate.logger.info("RandomNumberTool generated: #{random_num} (Range: #{min_val}-#{max_val})")
|
|
54
|
+
{ status: :success, result: random_num }
|
|
55
|
+
rescue Legate::ToolArgumentError => e # Re-raise specific argument errors
|
|
56
|
+
raise e
|
|
57
|
+
rescue StandardError => e # Catch unexpected errors during the process
|
|
58
|
+
Legate.logger.error("RandomNumberTool: Unexpected error: #{e.class} - #{e.message}")
|
|
59
|
+
# Wrap unexpected errors in a ToolError
|
|
60
|
+
raise Legate::ToolError, "Unexpected error in RandomNumber tool: #{e.message}"
|
|
61
|
+
end
|
|
62
|
+
end # End RandomNumberTool class
|
|
63
|
+
end # End Tools module
|
|
64
|
+
end # End Legate module
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# File: lib/legate/tools/read_webpage_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
require_relative 'base/http_client'
|
|
6
|
+
require_relative 'base/safe_url'
|
|
7
|
+
|
|
8
|
+
module Legate
|
|
9
|
+
module Tools
|
|
10
|
+
# Fetches a web page and returns its readable text content with markup removed.
|
|
11
|
+
#
|
|
12
|
+
# This is the backbone of research/RAG agents: give it a URL and it returns
|
|
13
|
+
# the page title and plain text (script/style stripped, entities decoded,
|
|
14
|
+
# whitespace collapsed), capped to a sane size. SSRF-safe via {Base::SafeUrl}.
|
|
15
|
+
class ReadWebpage < Legate::Tool
|
|
16
|
+
include Legate::Tools::Base::HttpClient
|
|
17
|
+
|
|
18
|
+
DEFAULT_MAX_CHARS = 20_000
|
|
19
|
+
HARD_MAX_CHARS = 200_000
|
|
20
|
+
ENTITIES = { '&' => '&', '<' => '<', '>' => '>', '"' => '"',
|
|
21
|
+
''' => "'", ''' => "'", ' ' => ' ' }.freeze
|
|
22
|
+
|
|
23
|
+
tool_name # inferred: :read_webpage
|
|
24
|
+
tool_description 'Fetches a web page and returns its readable text content (HTML markup removed) and title. ' \
|
|
25
|
+
'Use this to read articles or documentation. Blocks private/loopback addresses (SSRF-safe).'
|
|
26
|
+
|
|
27
|
+
parameter :url, type: :string, required: true,
|
|
28
|
+
description: 'The URL of the page to read (http or https).'
|
|
29
|
+
parameter :max_chars, type: :integer, required: false,
|
|
30
|
+
description: "Maximum characters of text to return (default #{DEFAULT_MAX_CHARS})."
|
|
31
|
+
|
|
32
|
+
def initialize(**options)
|
|
33
|
+
super(**options)
|
|
34
|
+
setup_http_client(base_url: 'https://placeholder.invalid')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def perform_execution(params, _context)
|
|
40
|
+
url = params.fetch(:url)
|
|
41
|
+
limit = (params[:max_chars] || DEFAULT_MAX_CHARS).to_i.clamp(1, HARD_MAX_CHARS)
|
|
42
|
+
|
|
43
|
+
uri, pinned_ip = Legate::Tools::Base::SafeUrl.resolve!(url)
|
|
44
|
+
response = make_request(
|
|
45
|
+
:get, url,
|
|
46
|
+
headers: { 'Accept' => 'text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8' },
|
|
47
|
+
options: { resolved_ip: pinned_ip, original_host: uri.host }
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
html = response.body.to_s
|
|
51
|
+
text = html_to_text(html)
|
|
52
|
+
truncated = text.length > limit
|
|
53
|
+
{
|
|
54
|
+
status: :success,
|
|
55
|
+
result: {
|
|
56
|
+
url: url,
|
|
57
|
+
title: extract_title(html),
|
|
58
|
+
text: truncated ? text[0, limit] : text,
|
|
59
|
+
truncated: truncated
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
rescue Legate::ToolError => e
|
|
63
|
+
{ status: :error, error_message: e.message }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def extract_title(html)
|
|
67
|
+
match = html.match(%r{<title[^>]*>(.*?)</title>}im)
|
|
68
|
+
match ? decode_entities(match[1].strip) : nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Best-effort HTML → plain text without a parser dependency: drop
|
|
72
|
+
# script/style/comments, turn block boundaries into newlines, strip the
|
|
73
|
+
# remaining tags, decode common entities, and collapse whitespace.
|
|
74
|
+
def html_to_text(html)
|
|
75
|
+
text = html.dup
|
|
76
|
+
text.gsub!(%r{<head[^>]*>.*?</head>}im, ' ')
|
|
77
|
+
text.gsub!(%r{<(script|style)[^>]*>.*?</\1>}im, ' ')
|
|
78
|
+
text.gsub!(/<!--.*?-->/m, ' ')
|
|
79
|
+
text.gsub!(%r{</?(p|div|br|li|tr|h[1-6]|section|article|header|footer)[^>]*>}i, "\n")
|
|
80
|
+
text.gsub!(/<[^>]+>/, ' ')
|
|
81
|
+
text = decode_entities(text)
|
|
82
|
+
text.gsub(/[ \t]+/, ' ').gsub(/ *\n */, "\n").gsub(/\n{3,}/, "\n\n").strip
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def decode_entities(str)
|
|
86
|
+
str = str.gsub(/&#(\d+);/) { [Regexp.last_match(1).to_i].pack('U') }
|
|
87
|
+
ENTITIES.each { |entity, char| str = str.gsub(entity, char) }
|
|
88
|
+
str
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# File: lib/legate/tools/sleepy_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'base_async_job_tool'
|
|
5
|
+
|
|
6
|
+
module Legate
|
|
7
|
+
module Tools
|
|
8
|
+
# An example Legate tool that starts a background job that sleeps for a specified duration.
|
|
9
|
+
# This tool demonstrates how to implement a BaseAsyncJobTool with a background worker.
|
|
10
|
+
class SleepyTool < BaseAsyncJobTool
|
|
11
|
+
# --- New DSL Metadata ---
|
|
12
|
+
# Name will be inferred as :sleepy_tool
|
|
13
|
+
self.explicit_tool_name = :start_sleepy_job # Keep original name
|
|
14
|
+
|
|
15
|
+
tool_description 'Starts a background job that sleeps for a specified duration and then returns a message.'
|
|
16
|
+
|
|
17
|
+
parameter :duration,
|
|
18
|
+
type: :integer,
|
|
19
|
+
description: 'How many seconds the job should sleep.',
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
parameter :message,
|
|
23
|
+
type: :string,
|
|
24
|
+
description: 'A message to include in the final result.',
|
|
25
|
+
required: true
|
|
26
|
+
# --- End New DSL Metadata ---
|
|
27
|
+
|
|
28
|
+
# Return the worker class to use.
|
|
29
|
+
def worker_class
|
|
30
|
+
SleepyWorker
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Prepare the arguments for the worker's perform method.
|
|
34
|
+
# Arguments must be JSON-serializable.
|
|
35
|
+
def prepare_job_arguments(params, _context)
|
|
36
|
+
duration = params[:duration].to_i
|
|
37
|
+
message = params[:message].to_s
|
|
38
|
+
[duration, message] # Must match SleepyWorker#perform signature (after jid)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The worker that performs the actual sleep operation.
|
|
43
|
+
class SleepyWorker
|
|
44
|
+
# @param jid [String] The Job ID (passed as first argument by BaseAsyncJobTool).
|
|
45
|
+
# @param duration [Integer] How long to sleep in seconds.
|
|
46
|
+
# @param message [String] The message to return upon completion.
|
|
47
|
+
def perform(jid, duration, message)
|
|
48
|
+
# --- Store initial pending status --- #
|
|
49
|
+
begin
|
|
50
|
+
Legate::Tools::BaseAsyncJobTool.store_job_pending(jid)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
# Log error but try to continue if possible
|
|
53
|
+
Legate.logger.error("[SleepyWorker JID: #{jid}] Failed to store initial pending status: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
# --- End store initial pending status --- #
|
|
56
|
+
|
|
57
|
+
Legate.logger.info("[SleepyWorker JID: #{jid}] Starting job. Sleeping for #{duration} seconds...")
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
sleep duration.to_i
|
|
61
|
+
result_message = "Slept for #{duration} seconds. Your message: #{message}"
|
|
62
|
+
Legate.logger.info("[SleepyWorker JID: #{jid}] Job finished. Storing result.")
|
|
63
|
+
# Store the successful result using the helper from BaseAsyncJobTool
|
|
64
|
+
Legate::Tools::BaseAsyncJobTool.store_job_result(jid, result_message)
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
error_message = "Job failed after starting sleep: #{e.message}"
|
|
67
|
+
Legate.logger.error("[SleepyWorker JID: #{jid}] Job failed! Storing error. Error: #{error_message}")
|
|
68
|
+
# Store the error using the helper
|
|
69
|
+
Legate::Tools::BaseAsyncJobTool.store_job_error(jid, error_message, e.class.name)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|