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,206 @@
|
|
|
1
|
+
# Exposing Legate Components via MCP
|
|
2
|
+
|
|
3
|
+
This guide explains how to make your `Legate::Tool`s (and experimentally, `Legate::Agent`s) available to external MCP clients. This is achieved by using provided adapters with the [`fast-mcp`](https://github.com/yjacquin/fast-mcp) Ruby gem, which handles the MCP server implementation.
|
|
4
|
+
|
|
5
|
+
**Prerequisite**: You must have the `fast-mcp` gem included in your project's Gemfile and installed (`bundle add fast_mcp`).
|
|
6
|
+
|
|
7
|
+
## Architecture Overview: Legate as MCP Service Provider
|
|
8
|
+
|
|
9
|
+
The following diagram illustrates how Legate components are wrapped and exposed via `fast-mcp`:
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph LR
|
|
13
|
+
subgraph Your Ruby Application / Script
|
|
14
|
+
LegateTool["Your Legate::Tool Class"] -- Wrapped by --> Adapter["Legate::Mcp::Server::LegateToolAdapter"]
|
|
15
|
+
LegateAgent["Your Legate::Agent Definition/Instance"] -- Wrapped by --> AgentAdapter["Legate::Mcp::Server::LegateAgentAdapter"]
|
|
16
|
+
Adapter -- Registers with --> FastMcpServer["fast-mcp Server Instance"]
|
|
17
|
+
AgentAdapter -- Registers with --> FastMcpServer
|
|
18
|
+
FastMcpServer -- Manages --> Transport["fast-mcp Transport (STDIO/Rack)"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
subgraph External MCP Client
|
|
22
|
+
MCP_Client["External MCP Client"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Transport -- JSON-RPC --> MCP_Client
|
|
26
|
+
|
|
27
|
+
style LegateTool fill:#ccf,stroke:#333,stroke-width:2px
|
|
28
|
+
style LegateAgent fill:#ccf,stroke:#333,stroke-width:2px
|
|
29
|
+
style Adapter fill:#cff,stroke:#333,stroke-width:2px
|
|
30
|
+
style AgentAdapter fill:#cff,stroke:#333,stroke-width:2px
|
|
31
|
+
style FastMcpServer fill:#fcf,stroke:#333,stroke-width:2px
|
|
32
|
+
style MCP_Client fill:#f9f,stroke:#333,stroke-width:2px
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Key Components:**
|
|
36
|
+
|
|
37
|
+
* **`Legate::Tool` / `Legate::Agent`**: Your existing Legate components.
|
|
38
|
+
* **`Legate::Mcp::Server::LegateToolAdapter`**: Wraps an `Legate::Tool` class to make it compatible with `fast-mcp`.
|
|
39
|
+
* **`Legate::Mcp::Server::LegateAgentAdapter` / `LegateDirectAgentAdapter`**: Wraps an `Legate::Agent` (either a registry definition or a direct instance) to expose it as a single MCP tool.
|
|
40
|
+
* **`fast-mcp Server Instance`**: The MCP server provided by the `fast-mcp` gem.
|
|
41
|
+
* **`fast-mcp Transport`**: How the `fast-mcp` server communicates (e.g., STDIO, Rack middleware for HTTP/SSE).
|
|
42
|
+
|
|
43
|
+
## 1. Exposing Legate Tools
|
|
44
|
+
|
|
45
|
+
Use the `Legate::Mcp::Server::LegateToolAdapter.wrap` method to create a `fast-mcp` compatible tool class from your existing `Legate::Tool` subclass.
|
|
46
|
+
|
|
47
|
+
### 1.1. Wrapping a Tool
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require 'legate'
|
|
51
|
+
require 'fast_mcp'
|
|
52
|
+
require 'legate/mcp/server/legate_tool_adapter'
|
|
53
|
+
require 'legate/tools/calculator' # Example Legate tool
|
|
54
|
+
|
|
55
|
+
# Configure Legate logger if needed
|
|
56
|
+
# Legate.configure { |c| c.log_level = Logger::INFO }
|
|
57
|
+
|
|
58
|
+
# 1. Wrap your Legate::Tool class
|
|
59
|
+
AdaptedCalculatorTool = Legate::Mcp::Server::LegateToolAdapter.wrap(Legate::Tools::Calculator)
|
|
60
|
+
|
|
61
|
+
# 2. Create and configure the fast-mcp server
|
|
62
|
+
mcp_server = FastMcp::Server.new(
|
|
63
|
+
name: 'legate-tool-server',
|
|
64
|
+
version: Legate::VERSION,
|
|
65
|
+
logger: Legate.logger # Optional: Use Legate's logger
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 3. Register the adapted tool with the fast-mcp server
|
|
69
|
+
mcp_server.register_tool(AdaptedCalculatorTool)
|
|
70
|
+
|
|
71
|
+
# 4. Start the server (e.g., using STDIO transport)
|
|
72
|
+
Legate.logger.info("Starting Legate MCP Tool Server via STDIO...")
|
|
73
|
+
mcp_server.start # This will block and listen on STDIN/STDOUT
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 1.2. How it Works (Tool Adapter)
|
|
77
|
+
|
|
78
|
+
* `LegateToolAdapter.wrap(YourLegateToolClass)`:
|
|
79
|
+
* Retrieves metadata (name, description, parameters) from `YourLegateToolClass`.
|
|
80
|
+
* Uses `Legate::Mcp::Util::SchemaConverter.legate_to_dry_schema` to convert Legate parameter definitions into a `Dry::Schema` block for `fast-mcp` argument validation. (See [Schema Conversion Details](../advanced/mcp_schema_conversion))
|
|
81
|
+
* Dynamically creates a new class that inherits from `FastMcp::Tool`.
|
|
82
|
+
* The `call` method of this new class will:
|
|
83
|
+
* Instantiate `YourLegateToolClass`.
|
|
84
|
+
* Create a dummy `Legate::ToolContext` (as there's no Legate session in this server context).
|
|
85
|
+
* Execute `your_legate_tool_instance.execute(params, dummy_context)`.
|
|
86
|
+
* Translate the Legate result hash (`{status: :success, result: ...}` or `{status: :error, ...}`) into a format `fast-mcp` expects (either direct result or raises an error).
|
|
87
|
+
|
|
88
|
+
### 1.3. Handling Asynchronous Legate Tools
|
|
89
|
+
|
|
90
|
+
If your Legate tool is asynchronous (returns `{status: :pending, job_id: ...}`), the `LegateToolAdapter` will return a corresponding MCP-friendly pending response (e.g., `{'status': 'pending', 'job_id': '...'}`).
|
|
91
|
+
|
|
92
|
+
To allow clients to check the status of these jobs, you **must** also wrap and expose the `Legate::Tools::CheckJobStatusTool`:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
require 'legate/tools/base_async_job_tool' # If defining your own async tool
|
|
96
|
+
require 'legate/tools/sleepy_tool' # Example Legate async tool
|
|
97
|
+
require 'legate/tools/check_job_status_tool'
|
|
98
|
+
|
|
99
|
+
# ... (fast_mcp server setup) ...
|
|
100
|
+
|
|
101
|
+
AdaptedSleepyTool = Legate::Mcp::Server::LegateToolAdapter.wrap(Legate::Tools::SleepyTool)
|
|
102
|
+
AdaptedCheckJobStatusTool = Legate::Mcp::Server::LegateToolAdapter.wrap(Legate::Tools::CheckJobStatusTool)
|
|
103
|
+
|
|
104
|
+
mcp_server.register_tool(AdaptedSleepyTool)
|
|
105
|
+
mcp_server.register_tool(AdaptedCheckJobStatusTool) # Crucial for async tools!
|
|
106
|
+
|
|
107
|
+
# ... (start server) ...
|
|
108
|
+
```
|
|
109
|
+
Clients will then call your async tool, get a `job_id`, and use the exposed `check_job_status` tool to poll for completion.
|
|
110
|
+
|
|
111
|
+
## 2. Exposing Legate Agents (Experimental)
|
|
112
|
+
|
|
113
|
+
You can wrap an entire Legate Agent and expose its `run_task` functionality as a single MCP tool. This is useful for providing a simple, prompt-based interface to your agent for external MCP clients.
|
|
114
|
+
|
|
115
|
+
Two adapters are available:
|
|
116
|
+
|
|
117
|
+
* **`Legate::Mcp::Server::LegateAgentAdapter`**: For agents defined and stored in the **`GlobalDefinitionRegistry`** (e.g., created via `legate agent save`).
|
|
118
|
+
* **`Legate::Mcp::Server::LegateDirectAgentAdapter`**: For `Legate::Agent` instances created directly in your Ruby code.
|
|
119
|
+
|
|
120
|
+
### 2.1. Using `LegateAgentAdapter` (Registry Definition)
|
|
121
|
+
|
|
122
|
+
**Prerequisites:**
|
|
123
|
+
* Your agent definition must exist in the `GlobalDefinitionRegistry`.
|
|
124
|
+
* An `Legate::SessionService` instance is needed by the adapter for temporary session management.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
require 'legate/mcp/server/legate_agent_adapter'
|
|
128
|
+
require 'legate/session_service/in_memory'
|
|
129
|
+
|
|
130
|
+
AGENT_NAME_IN_REGISTRY = 'my_chat_agent' # The name of your agent in the registry
|
|
131
|
+
session_service = Legate::SessionService::InMemory.new
|
|
132
|
+
|
|
133
|
+
# 1. Wrap the agent definition from the registry
|
|
134
|
+
AdaptedRegistryAgent = Legate::Mcp::Server::LegateAgentAdapter.wrap(AGENT_NAME_IN_REGISTRY, session_service)
|
|
135
|
+
|
|
136
|
+
# 2. Setup fast-mcp server and register AdaptedRegistryAgent
|
|
137
|
+
# ... (as shown in Tool Adapter example) ...
|
|
138
|
+
mcp_server.register_tool(AdaptedRegistryAgent)
|
|
139
|
+
# ... (start server) ...
|
|
140
|
+
```
|
|
141
|
+
This will expose a tool named something like `run_agent_my_chat_agent` that accepts a `prompt` argument.
|
|
142
|
+
|
|
143
|
+
### 2.2. Using `LegateDirectAgentAdapter` (Ruby Instance)
|
|
144
|
+
|
|
145
|
+
**Prerequisites:**
|
|
146
|
+
* An `Legate::Agent` instance created in your Ruby code.
|
|
147
|
+
* An `Legate::SessionService` instance.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
require 'legate/mcp/server/legate_direct_agent_adapter'
|
|
151
|
+
require 'legate/session_service/in_memory'
|
|
152
|
+
require 'legate/tools/echo' # Example tool for the agent
|
|
153
|
+
|
|
154
|
+
# 1. Create your Legate Agent instance from a definition.
|
|
155
|
+
# Tools are selected on the definition via use_tool; ensure the tool
|
|
156
|
+
# class is registered globally so the agent can find it.
|
|
157
|
+
Legate::GlobalToolManager.register_tool(Legate::Tools::Echo)
|
|
158
|
+
|
|
159
|
+
agent_definition = Legate::AgentDefinition.new.define do |a|
|
|
160
|
+
a.name :my_code_defined_agent
|
|
161
|
+
a.instruction 'Echo the user input.'
|
|
162
|
+
a.use_tool :echo
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
my_agent_instance = Legate::Agent.new(definition: agent_definition)
|
|
166
|
+
# Do NOT call my_agent_instance.start() here if only using for MCP wrapping.
|
|
167
|
+
|
|
168
|
+
session_service = Legate::SessionService::InMemory.new
|
|
169
|
+
|
|
170
|
+
# 2. Wrap the agent *instance*
|
|
171
|
+
AdaptedDirectAgent = Legate::Mcp::Server::LegateDirectAgentAdapter.wrap(my_agent_instance, session_service)
|
|
172
|
+
|
|
173
|
+
# 3. Setup fast-mcp server and register AdaptedDirectAgent
|
|
174
|
+
# ... (as shown in Tool Adapter example) ...
|
|
175
|
+
mcp_server.register_tool(AdaptedDirectAgent)
|
|
176
|
+
# ... (start server) ...
|
|
177
|
+
```
|
|
178
|
+
This will expose a tool named `run_agent_my_code_defined_agent`.
|
|
179
|
+
|
|
180
|
+
### 2.3. Agent Adapter Limitations (Current Version)
|
|
181
|
+
|
|
182
|
+
* **Stateless Execution**: Each call to the wrapped agent tool via MCP is treated as a new, isolated interaction. A temporary Legate session is created for the `run_task` call and then deleted. There is no persistent conversation history between MCP calls to the agent tool.
|
|
183
|
+
* **Tool Availability**: For the `LegateAgentAdapter` (registry-based), any tools specified in the agent's definition must be available in the global `Legate::ToolRegistry` of the Ruby process running the MCP server.
|
|
184
|
+
|
|
185
|
+
## 3. Server Setup with `fast-mcp`
|
|
186
|
+
|
|
187
|
+
`fast-mcp` offers different ways to run the MCP server:
|
|
188
|
+
|
|
189
|
+
* **STDIO Server (`FastMcp::Server.new` or `FastMcp::Server::Stdio.new`)**:
|
|
190
|
+
* Listens for JSON-RPC messages on standard input and writes responses to standard output.
|
|
191
|
+
* Ideal for local development, integration with tools that manage child processes (like `mcp-inspector`), or simple standalone tool/agent servers.
|
|
192
|
+
* Start by calling `mcp_server.start`.
|
|
193
|
+
* See `examples/15_mcp_server.rb`.
|
|
194
|
+
* **Rack Middleware (`FastMcp.rack_middleware`)**:
|
|
195
|
+
* Integrates the MCP server into any Rack-based web application (e.g., Sinatra, Rails).
|
|
196
|
+
* Adds MCP endpoints (typically `/mcp/messages` for POST and `/mcp/sse` for Server-Sent Events) to your existing app.
|
|
197
|
+
* Allows clients to connect via HTTP/SSE.
|
|
198
|
+
* See `examples/advanced/mcp/mcp_server_rack.rb`.
|
|
199
|
+
|
|
200
|
+
Refer to the [`fast-mcp` documentation](https://github.com/yjacquin/fast-mcp) for more details on server setup, authentication, and other advanced features.
|
|
201
|
+
|
|
202
|
+
## 4. Security Considerations
|
|
203
|
+
|
|
204
|
+
* When exposing tools and agents via MCP, you are making their functionality available to any client that can connect to your `fast-mcp` server.
|
|
205
|
+
* If using the Rack middleware for an HTTP-accessible server, consider appropriate network security (firewalls, private networks) and authentication mechanisms if needed. `fast-mcp` provides options for token-based authentication (`FastMcp.authenticated_rack_middleware`) and origin checks.
|
|
206
|
+
* Be mindful of the capabilities of the Legate tools and agents you expose. Avoid exposing components that could lead to unintended actions or data access if called by unauthorized clients.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Rails integration
|
|
2
|
+
|
|
3
|
+
Legate runs in any Ruby program, but it ships first-class glue for Rails: a
|
|
4
|
+
durable ActiveRecord session store, a Railtie, and an install generator. None of
|
|
5
|
+
it is loaded unless you ask for it — `require 'legate'` never touches Rails or
|
|
6
|
+
ActiveRecord.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Add the gem and require the Rails integration:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# Gemfile
|
|
14
|
+
gem 'legate', require: 'legate/rails'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then run the generator and migrate:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bin/rails generate legate:install
|
|
21
|
+
bin/rails db:migrate
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That creates two files:
|
|
25
|
+
|
|
26
|
+
- **`db/migrate/…_create_legate_tables.rb`** — the schema for the session store
|
|
27
|
+
(`legate_sessions`, `legate_events`, `legate_scoped_states`).
|
|
28
|
+
- **`config/initializers/legate.rb`** — points Legate at the ActiveRecord store
|
|
29
|
+
and maps `GEMINI_API_KEY` → `GOOGLE_API_KEY` for the planner.
|
|
30
|
+
|
|
31
|
+
The initializer wires the store explicitly, so you stay in control:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'legate/session_service/active_record'
|
|
35
|
+
|
|
36
|
+
Legate.configure do |config|
|
|
37
|
+
config.session_service = Legate::SessionService::ActiveRecord.new
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
(Prefer Rails encrypted credentials for the API key in production rather than a
|
|
42
|
+
plain env var.)
|
|
43
|
+
|
|
44
|
+
## Durable sessions
|
|
45
|
+
|
|
46
|
+
With the ActiveRecord store configured, every conversation — its events and its
|
|
47
|
+
state — is persisted to your database and survives restarts. The store behaves
|
|
48
|
+
exactly like the in-memory one within a single run (the agent sees one live
|
|
49
|
+
`Session` object), but writes through on every change, so another process or a
|
|
50
|
+
later request re-hydrates the committed history:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
service = Legate.config.session_service # the AR store
|
|
54
|
+
session = service.create_session(app_name: 'support', user_id: current_user.id)
|
|
55
|
+
|
|
56
|
+
agent.run_task(session_id: session.id, user_input: params[:message], session_service: service)
|
|
57
|
+
|
|
58
|
+
# …later, another request / process:
|
|
59
|
+
restored = service.get_session(session_id: session.id)
|
|
60
|
+
restored.events # the full persisted history
|
|
61
|
+
restored.get_state(:some_key)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`persistent?` returns `true`, and scoped state (`user:` / `app:` / `temp:` keys)
|
|
65
|
+
is persisted too.
|
|
66
|
+
|
|
67
|
+
## Running agents in the background (ActiveJob)
|
|
68
|
+
|
|
69
|
+
Agent runs can take many seconds — do them off the request thread. Because the
|
|
70
|
+
store is durable, the controller enqueues a job and reads the result back from
|
|
71
|
+
the persisted session when it's done (poll, or push via Turbo/ActionCable):
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class LegateRunJob < ApplicationJob
|
|
75
|
+
queue_as :default
|
|
76
|
+
|
|
77
|
+
def perform(agent_name:, session_id:, message:)
|
|
78
|
+
agent = MyAgents.build(agent_name) # your factory: definition -> started Agent
|
|
79
|
+
agent.run_task(
|
|
80
|
+
session_id: session_id,
|
|
81
|
+
user_input: message,
|
|
82
|
+
session_service: Legate.config.session_service,
|
|
83
|
+
)
|
|
84
|
+
# The final answer + full history are now persisted on the session.
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# app/controllers/messages_controller.rb
|
|
91
|
+
session = Legate.config.session_service.create_session(app_name: 'support', user_id: current_user.id)
|
|
92
|
+
LegateRunJob.perform_later(agent_name: 'support', session_id: session.id, message: params[:message])
|
|
93
|
+
render json: { session_id: session.id }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Pair this with [event streaming](streaming): pass an `on_event:` callback to
|
|
97
|
+
`run_task` inside the job to broadcast progress over ActionCable as each tool
|
|
98
|
+
runs.
|
|
99
|
+
|
|
100
|
+
## Without Rails
|
|
101
|
+
|
|
102
|
+
The store is plain ActiveRecord — you can use it in any Ruby program. Establish a
|
|
103
|
+
connection and create the tables yourself:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
require 'active_record'
|
|
107
|
+
require 'legate/session_service/active_record'
|
|
108
|
+
|
|
109
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'legate.db')
|
|
110
|
+
Legate::SessionService::ActiveRecord.create_tables! # idempotent
|
|
111
|
+
|
|
112
|
+
Legate.configure { |c| c.session_service = Legate::SessionService::ActiveRecord.new }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The host application owns the connection and pool; Legate doesn't manage it.
|
|
116
|
+
|
|
117
|
+
## Notes
|
|
118
|
+
|
|
119
|
+
- Event content and state are stored as JSON. Resumed (historical) event content
|
|
120
|
+
comes back with string keys — the live run uses in-memory objects with their
|
|
121
|
+
original keys, so this only affects how you read prior turns.
|
|
122
|
+
- The schema is portable across SQLite, PostgreSQL, and MySQL (JSON stored as
|
|
123
|
+
text); the generated migration matches `ActiveRecord.create_tables!`.
|
|
124
|
+
|
|
125
|
+
## Related
|
|
126
|
+
|
|
127
|
+
- [Streaming agent events](streaming) — progress over ActionCable from a job.
|
|
128
|
+
- [LLM Providers](llm_providers) — configuring the model in the initializer.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Sending Outbound Webhooks from Legate Agents
|
|
2
|
+
|
|
3
|
+
The Legate Agents often need to notify external systems upon completing a task or when a specific event occurs. This can be achieved by sending outbound HTTP requests, commonly known as webhooks.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview: Sending Outbound Webhooks
|
|
6
|
+
|
|
7
|
+
The following diagram illustrates the two primary ways an Legate Agent can send outbound webhooks:
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
graph TD
|
|
11
|
+
subgraph Legate Agent Process
|
|
12
|
+
Agent[Legate::Agent]
|
|
13
|
+
Planner[Legate::Planner]
|
|
14
|
+
WebhookTool[Legate::Tools::WebhookTool]
|
|
15
|
+
CustomTool[Your Custom Tool with HttpClient]
|
|
16
|
+
HttpClient[Legate::Tools::Base::HttpClient]
|
|
17
|
+
|
|
18
|
+
Agent -- Task Prompt --> Planner
|
|
19
|
+
Planner -- Selects Tool --> WebhookTool
|
|
20
|
+
Planner -- Selects Tool --> CustomTool
|
|
21
|
+
CustomTool -- Uses --> HttpClient
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
subgraph External Systems
|
|
25
|
+
ExternalService1[External Service A]
|
|
26
|
+
ExternalService2[External Service B]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
WebhookTool -- HTTP POST --> ExternalService1
|
|
30
|
+
HttpClient -- HTTP Request (GET/HEAD/POST/PUT etc.) --> ExternalService2
|
|
31
|
+
|
|
32
|
+
style Agent fill:#ccf,stroke:#333,stroke-width:2px
|
|
33
|
+
style Planner fill:#ccf,stroke:#333,stroke-width:2px
|
|
34
|
+
style WebhookTool fill:#cff,stroke:#333,stroke-width:2px
|
|
35
|
+
style CustomTool fill:#cff,stroke:#333,stroke-width:2px
|
|
36
|
+
style HttpClient fill:#cff,stroke:#333,stroke-width:2px
|
|
37
|
+
style ExternalService1 fill:#f9f,stroke:#333,stroke-width:2px
|
|
38
|
+
style ExternalService2 fill:#f9f,stroke:#333,stroke-width:2px
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key Scenarios Depicted:**
|
|
42
|
+
|
|
43
|
+
1. **Using `WebhookTool`:** The agent, guided by the planner, uses the built-in `WebhookTool` to send a (typically POST) request to an external service.
|
|
44
|
+
2. **Using a Custom Tool with `HttpClient`:** The agent, guided by the planner, invokes a custom-developed tool. This custom tool then utilizes the `Legate::Tools::Base::HttpClient` module to make more complex or varied HTTP requests to another (or the same) external service.
|
|
45
|
+
|
|
46
|
+
There are two primary ways to send outbound webhooks from within the Legate framework:
|
|
47
|
+
|
|
48
|
+
1. **Using the built-in `WebhookTool`:** A convenient tool specifically designed for sending standard HTTP POST requests with JSON payloads, including optional HMAC-SHA256 signing. This is the recommended approach for common webhook integrations.
|
|
49
|
+
2. **Using the `HttpClient` module within a Custom Tool:** For more complex scenarios requiring different HTTP methods (GET, PUT, etc.), custom headers, non-JSON bodies, or more intricate logic before/after the request, you can create a custom tool that utilizes the `Legate::Tools::Base::HttpClient` mixin.
|
|
50
|
+
|
|
51
|
+
## 1. Using the `WebhookTool`
|
|
52
|
+
|
|
53
|
+
The `Legate::Tools::WebhookTool` provides a simple interface for sending POST requests.
|
|
54
|
+
|
|
55
|
+
### Adding the Tool to your Agent
|
|
56
|
+
|
|
57
|
+
Ensure the tool is available to your agent by including it in the agent definition:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# my_notifying_agent.rb
|
|
61
|
+
Legate::Agent.define do |a|
|
|
62
|
+
a.name :my_notifying_agent
|
|
63
|
+
a.description "Performs a task and notifies an external system."
|
|
64
|
+
# ... other config ...
|
|
65
|
+
|
|
66
|
+
a.use_tool :webhook_tool # Make the tool available
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Instructing the Agent
|
|
71
|
+
|
|
72
|
+
You instruct the agent to use the tool within its task prompt, providing the necessary parameters.
|
|
73
|
+
|
|
74
|
+
**`WebhookTool` Parameters:**
|
|
75
|
+
|
|
76
|
+
* `url` (String, required): The target URL to send the POST request to.
|
|
77
|
+
* `payload` (Hash | String, required): The data to send.
|
|
78
|
+
* If a `Hash` is provided, it will be automatically encoded as JSON, and the `Content-Type` header will be set to `application/json; charset=utf-8`.
|
|
79
|
+
* If a `String` is provided, it will be sent as the raw request body. You might need to specify a `Content-Type` via the `headers` parameter if the receiving system requires it.
|
|
80
|
+
* `secret` (String, optional): If provided, the tool will calculate an HMAC-SHA256 signature of the request body using this secret. The signature will be added to the request headers as `X-Hub-Signature-256: sha256=<calculated_signature>`. **Note:** Passing secrets directly in agent prompts can have security implications. Consider if a custom tool with configured secrets is more appropriate for sensitive integrations.
|
|
81
|
+
* `headers` (Hash, optional): Additional custom headers to include in the request (e.g., `{'Authorization': 'Bearer ...'}`). These are merged with default headers like `User-Agent` and the automatically added `Content-Type` (for Hash payloads) or `X-Hub-Signature-256` (if `secret` is used).
|
|
82
|
+
|
|
83
|
+
**Example Agent Prompt:**
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
"Analyze the provided data file. Once the analysis is complete and saved, use the `webhook_tool` to notify the monitoring system. Send a POST request with the following parameters:
|
|
87
|
+
- url: 'https://monitor.example.com/api/v1/updates'
|
|
88
|
+
- payload: A JSON object like {'task_id': '12345', 'status': 'completed', 'result_summary': 'Analysis finished successfully.'}
|
|
89
|
+
- secret: 'my-monitoring-api-secret'
|
|
90
|
+
- headers: {'X-Source-System': 'Legate-Agent'}"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Result
|
|
94
|
+
|
|
95
|
+
The `webhook_tool` will return a result indicating success or failure:
|
|
96
|
+
|
|
97
|
+
* **Success:** `{ status: :success, result: { response_status: <Integer>, response_body: <String> } }`
|
|
98
|
+
* **Failure:** Raises an `Legate::ToolError` (or a subclass like `Legate::ToolHttpError`, `Legate::ToolTimeoutError`) which the agent should handle or report.
|
|
99
|
+
|
|
100
|
+
## 2. Using `HttpClient` in a Custom Tool
|
|
101
|
+
|
|
102
|
+
For more control over the HTTP request (e.g., using methods other than POST, sending non-JSON data, complex authentication headers, custom retry logic), create a dedicated custom tool that includes the `Legate::Tools::Base::HttpClient` module.
|
|
103
|
+
|
|
104
|
+
**Important:** The agent itself *cannot* directly call `http_get`, `http_post`, etc. It must invoke a *custom tool* that encapsulates this logic.
|
|
105
|
+
|
|
106
|
+
### Creating the Custom Tool
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# lib/my_app/tools/notification_tool.rb
|
|
110
|
+
require 'legate/tool'
|
|
111
|
+
require 'legate/tools/base/http_client'
|
|
112
|
+
require 'json'
|
|
113
|
+
require 'openssl' # If manual signing is needed
|
|
114
|
+
|
|
115
|
+
module MyApp
|
|
116
|
+
module Tools
|
|
117
|
+
# The tool name :notification_tool is inferred from the class name.
|
|
118
|
+
# (To override, use `self.explicit_tool_name = :some_name`.)
|
|
119
|
+
class NotificationTool < Legate::Tool
|
|
120
|
+
include Legate::Tools::Base::HttpClient
|
|
121
|
+
|
|
122
|
+
tool_description "Sends notifications to a specific internal system."
|
|
123
|
+
|
|
124
|
+
parameter :endpoint, type: :string, required: true, description: "The API endpoint path (e.g., '/events')."
|
|
125
|
+
parameter :event_data, type: :hash, required: true, description: "Data for the notification payload."
|
|
126
|
+
# NOTE: `default:` is not applied by the framework; defaults are handled in
|
|
127
|
+
# perform_execution below (e.g. `params[:http_method] || 'POST'`).
|
|
128
|
+
parameter :http_method, type: :string, required: false, description: "HTTP method (POST, PUT, etc.). Defaults to POST."
|
|
129
|
+
|
|
130
|
+
def initialize(**options)
|
|
131
|
+
super(**options)
|
|
132
|
+
# Setup HttpClient - read base URL/secret from ENV or config
|
|
133
|
+
# IMPORTANT: Avoid hardcoding secrets!
|
|
134
|
+
@api_base_url = ENV.fetch('NOTIFICATION_API_URL', 'https://internal.example.com/api')
|
|
135
|
+
@api_secret = ENV['NOTIFICATION_API_SECRET'] # Optional secret
|
|
136
|
+
|
|
137
|
+
setup_http_client(
|
|
138
|
+
base_url: @api_base_url,
|
|
139
|
+
headers: { 'Accept' => 'application/json' },
|
|
140
|
+
options: { read_timeout: 10 }
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def perform_execution(params, context)
|
|
147
|
+
endpoint = params[:endpoint]
|
|
148
|
+
event_data = params[:event_data]
|
|
149
|
+
# Handle the default in code; the `default:` parameter option is not applied.
|
|
150
|
+
http_method = (params[:http_method] || 'POST').downcase.to_sym
|
|
151
|
+
|
|
152
|
+
request_headers = {}
|
|
153
|
+
request_body = JSON.generate(event_data) rescue event_data.to_s # Ensure JSON encoding
|
|
154
|
+
|
|
155
|
+
# Example: Manual HMAC Signing (if not using WebhookTool's secret param)
|
|
156
|
+
if @api_secret
|
|
157
|
+
signature = OpenSSL::HMAC.hexdigest('sha256', @api_secret, request_body)
|
|
158
|
+
request_headers['X-Internal-Signature-256'] = "sha256=#{signature}"
|
|
159
|
+
end
|
|
160
|
+
# Always set Content-Type for JSON body when using HttpClient directly
|
|
161
|
+
request_headers['Content-Type'] = 'application/json; charset=utf-8'
|
|
162
|
+
|
|
163
|
+
begin
|
|
164
|
+
response = make_request(
|
|
165
|
+
http_method,
|
|
166
|
+
endpoint, # Path relative to base_url
|
|
167
|
+
body: request_body,
|
|
168
|
+
headers: request_headers
|
|
169
|
+
)
|
|
170
|
+
Legate.logger.info "NotificationTool: Successfully sent #{http_method.upcase} to #{endpoint}. Status: #{response.status}"
|
|
171
|
+
# Return a success indicator
|
|
172
|
+
{ status: :success, response_code: response.status }
|
|
173
|
+
rescue Legate::ToolHttpError => e
|
|
174
|
+
Legate.logger.error "NotificationTool: HTTP Error sending to #{endpoint}. Status: #{e.response&.status}. Body: #{e.response&.body}"
|
|
175
|
+
# Re-raise or return structured error
|
|
176
|
+
raise Legate::ToolError, "Failed to send notification (HTTP #{e.response&.status})"
|
|
177
|
+
rescue Legate::ToolError => e # Includes timeouts, network errors etc.
|
|
178
|
+
Legate.logger.error "NotificationTool: Error sending to #{endpoint}: #{e.message}"
|
|
179
|
+
# Re-raise or return structured error
|
|
180
|
+
raise Legate::ToolError, "Failed to send notification: #{e.message}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
*(See [`http_client_usage`](./http_client_usage) for full details on using the `HttpClient` module).*
|
|
189
|
+
|
|
190
|
+
### Adding the Custom Tool to your Agent
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# my_complex_agent.rb
|
|
194
|
+
# require 'my_app/tools/notification_tool' # Make sure tool class is loaded
|
|
195
|
+
|
|
196
|
+
Legate::Agent.define do |a|
|
|
197
|
+
a.name :my_complex_agent
|
|
198
|
+
# ...
|
|
199
|
+
|
|
200
|
+
a.use_tool :notification_tool # Use the custom tool
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Instructing the Agent (to use the Custom Tool)
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
"After processing the order (ID: #{order_id}), use the `notification_tool` to inform the fulfillment system. Use the endpoint '/order_updates' and send the following event_data as JSON: {'orderId': '#{order_id}', 'status': 'processed', 'items': [...]}. Use the default POST method."
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Choosing the Right Approach
|
|
211
|
+
|
|
212
|
+
* Use **`WebhookTool`** if:
|
|
213
|
+
* You need to send a simple HTTP POST request.
|
|
214
|
+
* Your payload is typically JSON (Hash).
|
|
215
|
+
* You need standard HMAC-SHA256 signing (`X-Hub-Signature-256`).
|
|
216
|
+
* You are comfortable with the agent prompt potentially containing the signing secret (use with caution).
|
|
217
|
+
* Use **`HttpClient` in a Custom Tool** if:
|
|
218
|
+
* You need to use other HTTP methods (GET, HEAD, PUT, DELETE, etc.).
|
|
219
|
+
* You need to send non-JSON request bodies (e.g., XML, form data) and manage `Content-Type` manually.
|
|
220
|
+
* You require complex custom headers or authentication schemes beyond simple HMAC.
|
|
221
|
+
* You need to perform logic before or after the HTTP request within the tool itself.
|
|
222
|
+
* You want to manage API secrets within the tool's configuration (e.g., via ENV variables) rather than passing them via agent parameters.
|
|
223
|
+
|
|
224
|
+
## Security Considerations
|
|
225
|
+
|
|
226
|
+
* **Secrets Management:** Avoid hardcoding secrets. Use environment variables or a dedicated configuration management system to provide secrets (like API keys or webhook signing keys) to your custom tools or potentially to the `WebhookTool`'s `secret` parameter (though configuring them in the tool is generally safer than passing via LLM prompt).
|
|
227
|
+
* **URL Validation:** Ensure the URLs being targeted by your webhooks are intended and trusted. Be cautious if URLs are dynamically generated based on user input.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Streaming agent events
|
|
2
|
+
|
|
3
|
+
An agent task can take many seconds — it may call several tools before it
|
|
4
|
+
answers. Rather than block until the final result, you can **stream the agent's
|
|
5
|
+
lifecycle events as they happen**: the user message, each tool request, each tool
|
|
6
|
+
result, and the final answer. This is how you build a responsive "thinking…
|
|
7
|
+
calling search… answering" experience instead of a frozen spinner.
|
|
8
|
+
|
|
9
|
+
## `on_event` — the streaming callback
|
|
10
|
+
|
|
11
|
+
`Agent#run_task` accepts an optional `on_event:` proc. It's called with each
|
|
12
|
+
`Legate::Event` the moment it's appended to the session, while the task runs:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
agent.run_task(
|
|
16
|
+
session_id: session.id,
|
|
17
|
+
user_input: 'Find the population of France and double it',
|
|
18
|
+
session_service: session_service,
|
|
19
|
+
on_event: ->(event) do
|
|
20
|
+
case event.role
|
|
21
|
+
when :user then puts "▶ #{event.content}"
|
|
22
|
+
when :tool_request then puts " → calling #{event.tool_name}"
|
|
23
|
+
when :tool_result then puts " ← #{event.content[:result]}"
|
|
24
|
+
when :agent then puts "✓ #{event.content[:result]}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The events you'll see, in order:
|
|
31
|
+
|
|
32
|
+
| `event.role` | When | `event.content` |
|
|
33
|
+
|----------------|------|-----------------|
|
|
34
|
+
| `:user` | task starts | the user input string |
|
|
35
|
+
| `:tool_request`| before each tool runs | the params; `event.tool_name` is the tool |
|
|
36
|
+
| `:tool_result` | after each tool runs | `{ status:, result: / error_message: }` |
|
|
37
|
+
| `:agent` | task finishes | the final result hash |
|
|
38
|
+
|
|
39
|
+
`on_event` is **purely additive**: the method still returns the final `Event`, and
|
|
40
|
+
omitting `on_event` leaves behavior exactly as before. It works for both the
|
|
41
|
+
default plan-then-execute strategy and the [agentic `:react` loop](agentic_agents)
|
|
42
|
+
— every event flows through the same session funnel.
|
|
43
|
+
|
|
44
|
+
### How it works
|
|
45
|
+
|
|
46
|
+
Every event an agent produces is persisted through `SessionService#append_event`.
|
|
47
|
+
The service broadcasts each appended event to subscribers of that session
|
|
48
|
+
(`EventBroadcast`), and `run_task` subscribes your `on_event` for the duration of
|
|
49
|
+
the run, tearing the subscription down afterward. Delivery is synchronous and
|
|
50
|
+
in order on the running thread, so a streaming HTTP response can write each frame
|
|
51
|
+
as it arrives.
|
|
52
|
+
|
|
53
|
+
## Server-Sent Events over HTTP
|
|
54
|
+
|
|
55
|
+
The web app exposes the stream as SSE:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
POST /agents/:name/stream (form field: message=…, plus the CSRF token)
|
|
59
|
+
Content-Type: text/event-stream
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each appended event is sent as an `event: message` frame; the run ends with an
|
|
63
|
+
`event: done` frame carrying the final result (or `event: error`):
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
event: message
|
|
67
|
+
data: {"role":"user","content":"double the population of France",…}
|
|
68
|
+
|
|
69
|
+
event: message
|
|
70
|
+
data: {"role":"tool_request","tool_name":"search","content":{…},…}
|
|
71
|
+
|
|
72
|
+
event: message
|
|
73
|
+
data: {"role":"tool_result","content":{"status":"success","result":"67 million"},…}
|
|
74
|
+
|
|
75
|
+
event: done
|
|
76
|
+
data: {"role":"agent","content":{"status":"success","result":"134 million"},…}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The frame payloads are `Event#to_h` (JSON, ISO-8601 timestamps). Because the
|
|
80
|
+
endpoint is CSRF-protected it can't be consumed by a bare `EventSource` (which is
|
|
81
|
+
GET-only and can't send the token); use `fetch` with a streaming reader:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
const res = await fetch(`/agents/${name}/stream`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'X-CSRF-Token': csrfToken, 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
87
|
+
body: new URLSearchParams({ message }),
|
|
88
|
+
});
|
|
89
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
90
|
+
for (;;) {
|
|
91
|
+
const { value, done } = await reader.read();
|
|
92
|
+
if (done) break;
|
|
93
|
+
// parse SSE frames out of `value`…
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each request runs as a fresh one-shot session.
|
|
98
|
+
|
|
99
|
+
## Scope: steps, not tokens
|
|
100
|
+
|
|
101
|
+
Streaming here is **step/event** level — you get each tool call and result as it
|
|
102
|
+
happens. Legate agents always plan and call tools rather than emitting free-form
|
|
103
|
+
model prose, and in the agentic loop the final answer arrives whole inside one
|
|
104
|
+
decision, so there's no separate token stream to forward. Token-level streaming
|
|
105
|
+
is a possible future addition (an adapter `supports_streaming?` capability); it
|
|
106
|
+
isn't needed to solve the "no feedback during a long run" problem, which event
|
|
107
|
+
streaming already does.
|
|
108
|
+
|
|
109
|
+
## Related
|
|
110
|
+
|
|
111
|
+
- [Agentic Agents](agentic_agents) — multi-step runs where streaming shines.
|
|
112
|
+
- [Legate Planner](../core_concepts/legate_planner) — what produces the steps.
|