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,984 @@
|
|
|
1
|
+
# File: lib/legate/web/app.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This file defines the main Sinatra application for the Legate Web UI.
|
|
5
|
+
# It handles agent definition management (via GlobalDefinitionRegistry), runtime management (in-memory),
|
|
6
|
+
# user interactions (chat, direct execution), tool discovery (native and MCP),
|
|
7
|
+
# and provides a dynamic web interface using HTMX.
|
|
8
|
+
|
|
9
|
+
# STDOUT.sync = true # Uncomment for immediate output flushing if needed
|
|
10
|
+
# --- Core Web Framework Dependencies ---
|
|
11
|
+
require 'openssl' # session-secret key derivation
|
|
12
|
+
require 'sinatra/base'
|
|
13
|
+
require 'sinatra/json'
|
|
14
|
+
require 'sinatra/custom_logger' # For using helpers Sinatra::CustomLogger
|
|
15
|
+
require 'sinatra/reloader'
|
|
16
|
+
require 'slim' # Templating engine
|
|
17
|
+
require 'json'
|
|
18
|
+
require_relative 'sass_compiler' # For compiling Sass/SCSS to CSS
|
|
19
|
+
require 'rack/utils' # For escape_html
|
|
20
|
+
require 'securerandom' # For session secret generation
|
|
21
|
+
require_relative '../mcp/util/schema_converter' # For converting MCP tool schemas
|
|
22
|
+
require_relative '../llm' # LLM provider adapters (example task generation, etc.)
|
|
23
|
+
|
|
24
|
+
# --- Load Legate Components ---
|
|
25
|
+
# Order matters: Load core concepts before components that depend on them.
|
|
26
|
+
require_relative '../event' # Core event structure used by sessions
|
|
27
|
+
require_relative '../session' # Session structure for conversation history
|
|
28
|
+
require_relative '../tool_context' # Context object passed to tools during execution
|
|
29
|
+
require_relative '../agent' # Core Agent class (defines DEFAULT_MODEL)
|
|
30
|
+
require_relative '../tool' # Base Tool class
|
|
31
|
+
require_relative '../tool_registry' # Manages tools within an agent instance
|
|
32
|
+
require_relative '../session_service/in_memory' # Default in-memory session storage
|
|
33
|
+
require_relative '../global_tool_manager' # Discovers and manages native tools available to the application
|
|
34
|
+
# --- Load Authentication System ---
|
|
35
|
+
require_relative '../auth/manager' # Authentication manager for handling authentication schemes and credentials
|
|
36
|
+
require_relative '../auth/manager_store' # Persistence for authentication configuration
|
|
37
|
+
# Explicitly require built-in native tools so GlobalToolManager can find them
|
|
38
|
+
require_relative '../tools/echo'
|
|
39
|
+
require_relative '../tools/calculator'
|
|
40
|
+
require_relative '../tools/cat_facts'
|
|
41
|
+
require_relative '../tools/random_number_tool'
|
|
42
|
+
require_relative '../tools/agent_tool' # Tool that allows an agent to call another agent
|
|
43
|
+
require_relative '../activity_log' # Activity logging for dashboard
|
|
44
|
+
require_relative '../tools/base_async_job_tool' # Base class for tools that run asynchronously
|
|
45
|
+
require_relative '../tools/check_job_status_tool' # Tool to check the status of async jobs
|
|
46
|
+
require_relative '../tools/sleepy_tool' # Example async tool
|
|
47
|
+
# --- Require GlobalDefinitionRegistry (replaces Redis-based DefinitionStore) ---
|
|
48
|
+
require_relative '../global_definition_registry'
|
|
49
|
+
|
|
50
|
+
# --- Route Modules ---
|
|
51
|
+
require_relative 'routes/core_routes'
|
|
52
|
+
require_relative 'routes/api_routes'
|
|
53
|
+
require_relative 'routes/tools_ui_routes'
|
|
54
|
+
require_relative 'routes/agent_runtime_routes'
|
|
55
|
+
require_relative 'routes/agent_definition_routes'
|
|
56
|
+
require_relative 'routes/agent_interaction_routes'
|
|
57
|
+
require_relative 'routes/documentation_routes'
|
|
58
|
+
require_relative 'routes/authentication_routes'
|
|
59
|
+
require_relative 'routes/agent_authentication_routes'
|
|
60
|
+
require_relative 'routes/agent_generator_routes'
|
|
61
|
+
require_relative 'routes/tool_generator_routes'
|
|
62
|
+
|
|
63
|
+
# Load dotenv for development environment variables
|
|
64
|
+
begin; require 'dotenv/load'; rescue LoadError; end if ENV['RACK_ENV'] == 'development' || Sinatra::Base.development?
|
|
65
|
+
|
|
66
|
+
module Legate
|
|
67
|
+
module Web
|
|
68
|
+
# Sinatra application providing a web UI for managing and interacting with Legate Agents.
|
|
69
|
+
# Uses GlobalDefinitionRegistry for agent definitions and an in-memory hash for running agent instances.
|
|
70
|
+
# Leverages HTMX for dynamic UI updates and communicates with external tools via MCP.
|
|
71
|
+
class App < Sinatra::Base
|
|
72
|
+
helpers Sinatra::CustomLogger # Integrate Sinatra logging with the central Legate logger
|
|
73
|
+
|
|
74
|
+
# Development-specific configurations
|
|
75
|
+
configure :development do
|
|
76
|
+
register Sinatra::Reloader # Enable automatic code reloading
|
|
77
|
+
# Optional: Increase logging level specifically for development web server
|
|
78
|
+
# Legate.logger.level = Logger::DEBUG if Legate.logger
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# General configurations for all environments
|
|
82
|
+
configure do
|
|
83
|
+
set :logger, Legate.logger # Use the central Legate logger
|
|
84
|
+
# Sinatra 4 / rack-protection 4 enable Host authorization by default,
|
|
85
|
+
# which 403s any request whose Host header isn't explicitly permitted.
|
|
86
|
+
# This dev tool is reached via localhost, a LAN IP, or whatever host the
|
|
87
|
+
# operator binds, so permit all hosts (the prior Sinatra 3 posture). The
|
|
88
|
+
# web UI is not meant to face untrusted networks — see the security model.
|
|
89
|
+
set :host_authorization, { permitted_hosts: [] }
|
|
90
|
+
# Session cookie for storing the per-browser user id and CSRF token.
|
|
91
|
+
# httponly + SameSite blunt cookie theft/CSRF everywhere; Secure is only
|
|
92
|
+
# set in production (the dev UI is plain HTTP on localhost, where a Secure
|
|
93
|
+
# cookie would never be sent). Production deployments must terminate TLS.
|
|
94
|
+
set :sessions, httponly: true, same_site: :lax, secure: production?
|
|
95
|
+
# Derive a stable 64-hex-char key from whatever SESSION_SECRET is set.
|
|
96
|
+
# rack-protection's encrypted-cookie store 500s on a short/odd-length
|
|
97
|
+
# secret (it slices a fixed-length AES key off the raw string); hashing
|
|
98
|
+
# makes ANY value work, deterministically across restarts and Puma
|
|
99
|
+
# workers. SecureRandom default is per-process (dev only).
|
|
100
|
+
raw_session_secret = ENV['SESSION_SECRET'] || SecureRandom.hex(64)
|
|
101
|
+
set :session_secret, OpenSSL::Digest::SHA256.hexdigest(raw_session_secret)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
configure :production do
|
|
105
|
+
unless ENV['SESSION_SECRET']
|
|
106
|
+
raise 'SESSION_SECRET must be set in production. It secures the session ' \
|
|
107
|
+
'and CSRF cookies, and a per-process random fallback breaks both ' \
|
|
108
|
+
'across restarts and Puma workers. Refusing to start.'
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
CSRF_SAFE_METHODS = %w[GET HEAD OPTIONS].freeze
|
|
113
|
+
|
|
114
|
+
before do
|
|
115
|
+
session[:web_user_id] ||= SecureRandom.uuid
|
|
116
|
+
session[:csrf] ||= SecureRandom.hex(32)
|
|
117
|
+
|
|
118
|
+
next if CSRF_SAFE_METHODS.include?(request.request_method)
|
|
119
|
+
|
|
120
|
+
provided = params['authenticity_token'] || request.env['HTTP_X_CSRF_TOKEN']
|
|
121
|
+
halt 403, { 'Content-Type' => 'text/plain' }, 'Forbidden: Invalid CSRF token' unless provided && Rack::Utils.secure_compare(provided, session[:csrf])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# --- Sinatra Settings ---
|
|
125
|
+
set :root, File.expand_path('../../..', __dir__) # Project root directory
|
|
126
|
+
set :views, File.expand_path('views', __dir__) # Views directory for Slim templates
|
|
127
|
+
set :public_folder, File.expand_path('public', __dir__) # Directory for static assets (CSS, JS, images)
|
|
128
|
+
set :slim, pretty: true # Configure Slim for readable HTML output
|
|
129
|
+
|
|
130
|
+
# --- Constants ---
|
|
131
|
+
# List of available Gemini models selectable in the UI.
|
|
132
|
+
# Now includes beta models since we're using v1beta API endpoint
|
|
133
|
+
AVAILABLE_MODELS = [
|
|
134
|
+
'gemini-3.5-flash',
|
|
135
|
+
'gemini-2.5-flash',
|
|
136
|
+
'gemini-2.5-pro',
|
|
137
|
+
# Preview / Experimental
|
|
138
|
+
'gemini-3-pro-preview'
|
|
139
|
+
].freeze
|
|
140
|
+
|
|
141
|
+
# --- Register Route Modules ---
|
|
142
|
+
register Legate::Web::CoreRoutes
|
|
143
|
+
register Legate::Web::ApiRoutes
|
|
144
|
+
register Legate::Web::ToolsUIRoutes
|
|
145
|
+
# Generator routes must be registered BEFORE definition routes to avoid :name matching "generate"
|
|
146
|
+
register Legate::Web::AgentGeneratorRoutes
|
|
147
|
+
register Legate::Web::ToolGeneratorRoutes
|
|
148
|
+
register Legate::Web::AgentRuntimeRoutes
|
|
149
|
+
register Legate::Web::AgentDefinitionRoutes
|
|
150
|
+
register Legate::Web::AgentInteractionRoutes
|
|
151
|
+
register Legate::Web::DocumentationRoutes
|
|
152
|
+
register Legate::Web::AuthenticationRoutes
|
|
153
|
+
register Legate::Web::AgentAuthenticationRoutes
|
|
154
|
+
|
|
155
|
+
# --- Instance Variables ---
|
|
156
|
+
# Initializes application state, including connections and services.
|
|
157
|
+
def initialize
|
|
158
|
+
super
|
|
159
|
+
@logger = Legate.logger # Ensure logger is set early
|
|
160
|
+
# In-memory map of active/running Legate::Agent instances. Keyed by the
|
|
161
|
+
# agent's STRING name (the `/agents/:name/...` route-param form). Note that
|
|
162
|
+
# definition hashes store `:name` as a Symbol, so any running-state lookup
|
|
163
|
+
# that starts from a definition must `.to_s` the name before `@agents.key?`.
|
|
164
|
+
@agents = Concurrent::Map.new
|
|
165
|
+
# Service responsible for managing chat sessions (stores conversation history).
|
|
166
|
+
# Uses the global Legate.config.session_service for consistency with CLI.
|
|
167
|
+
@session_service = Legate.config.session_service
|
|
168
|
+
# Use GlobalDefinitionRegistry as the definition store (in-memory, no Redis)
|
|
169
|
+
@definition_store = Legate::GlobalDefinitionRegistry
|
|
170
|
+
@logger.info('Agent Definition Store initialized (using GlobalDefinitionRegistry).')
|
|
171
|
+
|
|
172
|
+
# Initialize Auth Manager Store (in-memory, no Redis)
|
|
173
|
+
initialize_auth_manager_store
|
|
174
|
+
# --- END MODIFICATION ---
|
|
175
|
+
|
|
176
|
+
# Compile SASS/SCSS files in public/styles to CSS in public/css on application startup.
|
|
177
|
+
# In production, we assume this was done during the build process.
|
|
178
|
+
SassCompiler.compile_all if ENV['RACK_ENV'] == 'development' || Sinatra::Base.development?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# --- Sinatra Helpers ---
|
|
182
|
+
# Utility methods accessible within route handlers and Slim templates.
|
|
183
|
+
helpers do
|
|
184
|
+
def csrf_token
|
|
185
|
+
session[:csrf]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Format time as relative time ago string
|
|
189
|
+
# @param time [Time] The time to format
|
|
190
|
+
# @return [String] Relative time string (e.g., "2 minutes ago")
|
|
191
|
+
def time_ago_in_words(time)
|
|
192
|
+
return 'just now' unless time
|
|
193
|
+
|
|
194
|
+
seconds = (Time.now.utc - time.utc).to_i
|
|
195
|
+
|
|
196
|
+
case seconds
|
|
197
|
+
when 0..59
|
|
198
|
+
'just now'
|
|
199
|
+
when 60..119
|
|
200
|
+
'1 minute ago'
|
|
201
|
+
when 120..3599
|
|
202
|
+
"#{seconds / 60} minutes ago"
|
|
203
|
+
when 3600..7199
|
|
204
|
+
'1 hour ago'
|
|
205
|
+
when 7200..86_399
|
|
206
|
+
"#{seconds / 3600} hours ago"
|
|
207
|
+
when 86_400..172_799
|
|
208
|
+
'yesterday'
|
|
209
|
+
else
|
|
210
|
+
"#{seconds / 86_400} days ago"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Fetches tool lists from one or more MCP (Multi-Capability Protocol) servers.
|
|
215
|
+
# Connects to each server defined in the mcp_configs array, lists its tools,
|
|
216
|
+
# and handles connection errors or timeouts.
|
|
217
|
+
# @param mcp_configs [Array<Hash>] Array of hashes, each defining an MCP server connection (e.g., {type: :stdio, command: "...", name: "..."}, {type: :tcp, url: "...", name: "..."}).
|
|
218
|
+
# @param timeout_seconds [Integer] Connection/fetch timeout per server.
|
|
219
|
+
# @return [Array<Hash>] An array of result hashes, one for each config.
|
|
220
|
+
# Success: { status: :success, server: String, config: Hash, tools: Array<Hash> }
|
|
221
|
+
# Error: { status: :error, server: String, config: Hash, message: String }
|
|
222
|
+
def fetch_mcp_tools(mcp_configs, timeout_seconds = 5)
|
|
223
|
+
# Ensure necessary Legate::Mcp classes are loaded (might be redundant if loaded globally, but safe)
|
|
224
|
+
require_relative '../mcp/client'
|
|
225
|
+
# MCP errors are defined in legate/errors.rb (loaded by lib/legate.rb)
|
|
226
|
+
require 'timeout'
|
|
227
|
+
|
|
228
|
+
aggregated_results = [] # Store results for each server config
|
|
229
|
+
return aggregated_results unless mcp_configs.is_a?(Array)
|
|
230
|
+
|
|
231
|
+
mcp_configs.each_with_index do |config, index|
|
|
232
|
+
server_label = config['name'] || config['command'] || config['url'] || "Server #{index + 1}" # Need string keys here
|
|
233
|
+
begin
|
|
234
|
+
logger.info("Attempting to fetch tools from MCP server: #{server_label}")
|
|
235
|
+
result = Timeout.timeout(timeout_seconds) do
|
|
236
|
+
client = nil
|
|
237
|
+
fetched_tools = []
|
|
238
|
+
begin
|
|
239
|
+
# Transform keys to symbols for the client
|
|
240
|
+
symbolized_config = config.transform_keys(&:to_sym)
|
|
241
|
+
# --- NEW: Explicitly convert string 'stdio' value to symbol :stdio ---
|
|
242
|
+
symbolized_config[:type] = :stdio if symbolized_config[:type] == 'stdio'
|
|
243
|
+
# Pass the modified hash with symbol keys and potentially symbolized type value
|
|
244
|
+
client = Legate::Mcp::Client.new(symbolized_config)
|
|
245
|
+
# Connect implicitly calls list_tools during handshake in current implementation
|
|
246
|
+
# If connect succeeds, tools should be available via client instance if needed
|
|
247
|
+
client.connect # Performs handshake
|
|
248
|
+
fetched_tools = client.list_tools # Explicitly list tools
|
|
249
|
+
logger.info("Successfully fetched #{fetched_tools.count} tools from #{server_label}.")
|
|
250
|
+
# Add server label/config for context in results
|
|
251
|
+
aggregated_results << { status: :success, server: server_label, config: config, tools: fetched_tools }
|
|
252
|
+
rescue Legate::Mcp::ConnectionError, Legate::Mcp::ProtocolError => e
|
|
253
|
+
logger.error("MCP Error fetching tools from #{server_label}: #{e.message}")
|
|
254
|
+
aggregated_results << { status: :error, server: server_label, config: config,
|
|
255
|
+
message: "MCP Connection/Protocol Error: #{e.message}" }
|
|
256
|
+
rescue StandardError => e
|
|
257
|
+
logger.error("Unexpected Error fetching tools from #{server_label}: #{e.class} - #{e.message}")
|
|
258
|
+
logger.error(e.backtrace.first(5).join("\n")) # Log backtrace for unexpected errors
|
|
259
|
+
aggregated_results << { status: :error, server: server_label, config: config,
|
|
260
|
+
message: "Internal Error: #{e.message}" }
|
|
261
|
+
ensure
|
|
262
|
+
# Ensure disconnect is always attempted if client was created
|
|
263
|
+
client&.disconnect
|
|
264
|
+
end
|
|
265
|
+
end # Timeout block
|
|
266
|
+
rescue Timeout::Error
|
|
267
|
+
logger.error("Timeout (#{timeout_seconds}s) fetching tools from MCP server: #{server_label}")
|
|
268
|
+
aggregated_results << { status: :error, server: server_label, config: config,
|
|
269
|
+
message: "Timeout after #{timeout_seconds} seconds" }
|
|
270
|
+
end # Begin/rescue Timeout
|
|
271
|
+
end # each_with_index
|
|
272
|
+
|
|
273
|
+
aggregated_results
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Builds the lookup map of all tools available to an agent (native +
|
|
277
|
+
# MCP), keyed by tool-name Symbol, used by the agent-definition routes to
|
|
278
|
+
# resolve a definition's configured tools for display. Callers pass their
|
|
279
|
+
# own native-tool metadata (the routes differ in how they shape it) and
|
|
280
|
+
# do their own configured-tool selection / check_job_status handling.
|
|
281
|
+
# @param mcp_servers_json [String, nil] the definition's MCP servers JSON
|
|
282
|
+
# @param native_tools_metadata [Array<Hash>] caller-shaped native tool metadata
|
|
283
|
+
# @param log_context [String] label for any MCP-JSON parse error log
|
|
284
|
+
# @return [Hash] { map: {Symbol => tool_hash}, mcp_results: Array<Hash> }
|
|
285
|
+
def resolve_available_tools(mcp_servers_json, native_tools_metadata, log_context: 'agent tools')
|
|
286
|
+
mcp_configs_list = []
|
|
287
|
+
begin
|
|
288
|
+
mcp_configs_list = JSON.parse(mcp_servers_json) if mcp_servers_json && !mcp_servers_json.empty? && mcp_servers_json != '[]'
|
|
289
|
+
rescue JSON::ParserError => e
|
|
290
|
+
logger.error("Invalid MCP JSON (#{log_context}): #{e.message}")
|
|
291
|
+
end
|
|
292
|
+
mcp_results = fetch_mcp_tools(mcp_configs_list)
|
|
293
|
+
|
|
294
|
+
fetched_mcp_tools_metadata = []
|
|
295
|
+
mcp_results.each do |result|
|
|
296
|
+
next unless result[:status] == :success && result[:tools]
|
|
297
|
+
|
|
298
|
+
result[:tools].each do |mcp_tool_schema|
|
|
299
|
+
parameters = Legate::Mcp::Util::SchemaConverter.json_to_legate(
|
|
300
|
+
mcp_tool_schema.dig(:inputSchema, 'properties') || {},
|
|
301
|
+
mcp_tool_schema.dig(:inputSchema, 'required') || []
|
|
302
|
+
)
|
|
303
|
+
fetched_mcp_tools_metadata << {
|
|
304
|
+
name: mcp_tool_schema[:name].to_sym,
|
|
305
|
+
description: mcp_tool_schema[:description] || '',
|
|
306
|
+
parameters: parameters, source: :mcp, source_detail: "MCP (#{result[:server]})"
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
map = (native_tools_metadata + fetched_mcp_tools_metadata).each_with_object({}) do |tool, acc|
|
|
312
|
+
acc[tool[:name]] ||= tool
|
|
313
|
+
end
|
|
314
|
+
{ map: map, mcp_results: mcp_results }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Helper for Agent Start/Stop button fragments (used in table view)
|
|
318
|
+
def agent_status_fragments(agent_data_or_obj)
|
|
319
|
+
agent_name = agent_data_or_obj.is_a?(Hash) ? agent_data_or_obj[:name] : agent_data_or_obj.name
|
|
320
|
+
safe_agent_id = agent_name.to_s.gsub(/[^a-zA-Z0-9_-]/, '-') # Sanitize for use in HTML IDs/CSS selectors
|
|
321
|
+
is_running = agent_data_or_obj.is_a?(Hash) ? agent_data_or_obj[:running] : agent_data_or_obj.running?
|
|
322
|
+
|
|
323
|
+
status_content_id = "agent-status-content-#{safe_agent_id}"
|
|
324
|
+
start_action_id = "agent-start-action-#{safe_agent_id}"
|
|
325
|
+
stop_action_id = "agent-stop-action-#{safe_agent_id}"
|
|
326
|
+
dropdown_id = "agent-actions-dropdown-#{safe_agent_id}"
|
|
327
|
+
|
|
328
|
+
status_html = <<~HTML
|
|
329
|
+
<span id="#{status_content_id}" hx-swap-oob="outerHTML">
|
|
330
|
+
<span class="tag is-medium #{is_running ? 'is-success' : 'is-danger'}">
|
|
331
|
+
<span class="icon is-small"><i class="fas #{is_running ? 'fa-check-circle' : 'fa-stop-circle'}"></i></span>
|
|
332
|
+
<span class="ml-1">#{is_running ? 'Running' : 'Stopped'}</span>
|
|
333
|
+
</span>
|
|
334
|
+
</span>
|
|
335
|
+
HTML
|
|
336
|
+
start_action_html = <<~HTML
|
|
337
|
+
<a class="dropdown-item #{is_running ? 'is-disabled' : ''}" href="#"#{' '}
|
|
338
|
+
id="#{start_action_id}"#{' '}
|
|
339
|
+
hx-post="/agents/#{agent_name}/start"#{' '}
|
|
340
|
+
hx-indicator="##{dropdown_id} .dropdown-trigger button"
|
|
341
|
+
hx-swap-oob="outerHTML"#{' '}
|
|
342
|
+
onclick="if(this.classList.contains('is-disabled')) event.preventDefault();">
|
|
343
|
+
<span class="icon has-text-success"><i class="fas fa-play"></i></span>
|
|
344
|
+
<span>Start</span>
|
|
345
|
+
</a>
|
|
346
|
+
HTML
|
|
347
|
+
stop_action_html = <<~HTML
|
|
348
|
+
<a class="dropdown-item #{!is_running ? 'is-disabled' : ''}" href="#"#{' '}
|
|
349
|
+
id="#{stop_action_id}"#{' '}
|
|
350
|
+
hx-post="/agents/#{agent_name}/stop"#{' '}
|
|
351
|
+
hx-indicator="##{dropdown_id} .dropdown-trigger button"
|
|
352
|
+
hx-swap-oob="outerHTML"#{' '}
|
|
353
|
+
onclick="if(this.classList.contains('is-disabled')) event.preventDefault();">
|
|
354
|
+
<span class="icon has-text-danger"><i class="fas fa-stop"></i></span>
|
|
355
|
+
<span>Stop</span>
|
|
356
|
+
</a>
|
|
357
|
+
HTML
|
|
358
|
+
status_html.strip + start_action_html.strip + stop_action_html.strip
|
|
359
|
+
end # end agent_status_fragments
|
|
360
|
+
|
|
361
|
+
# Helper for formatting tool/agent execution results into HTML
|
|
362
|
+
def format_execution_result_html(result_data)
|
|
363
|
+
html_parts = []
|
|
364
|
+
notification_class = 'is-info' # Default
|
|
365
|
+
overall_status = :unknown # Default
|
|
366
|
+
|
|
367
|
+
# --- Determine overall status ---
|
|
368
|
+
# Handle Legate::Event first
|
|
369
|
+
if result_data.is_a?(Legate::Event)
|
|
370
|
+
result_data = result_data.content # Extract content hash
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Now work with the hash
|
|
374
|
+
if result_data.is_a?(Hash) && result_data.key?(:status)
|
|
375
|
+
overall_status = result_data[:status]
|
|
376
|
+
elsif result_data.is_a?(Array) && result_data.all? { |h| h.is_a?(Hash) && h.key?(:status) }
|
|
377
|
+
# Multi-step array - determine overall status
|
|
378
|
+
overall_status = if result_data.any? { |h| h[:status] == :error }
|
|
379
|
+
:error
|
|
380
|
+
elsif result_data.any? { |h| h[:status] == :pending }
|
|
381
|
+
:pending
|
|
382
|
+
elsif result_data.empty? # Empty plan result
|
|
383
|
+
:warning # Or treat as error?
|
|
384
|
+
else # All success
|
|
385
|
+
:success
|
|
386
|
+
end
|
|
387
|
+
else # Unexpected format, treat as error
|
|
388
|
+
overall_status = :error
|
|
389
|
+
# Wrap the unexpected data into a standard error hash for consistent handling below
|
|
390
|
+
result_data = { status: :error, error_message: "Unexpected result format: #{result_data.inspect}" }
|
|
391
|
+
end
|
|
392
|
+
# --- End determine overall status ---
|
|
393
|
+
|
|
394
|
+
# Set notification class based on status
|
|
395
|
+
notification_class = case overall_status
|
|
396
|
+
when :success then 'is-success'
|
|
397
|
+
when :error then 'is-danger'
|
|
398
|
+
when :pending then 'is-warning' # Use warning for pending
|
|
399
|
+
else 'is-info' # includes :unknown, :warning (empty plan)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# --- Generate HTML content ---
|
|
403
|
+
if result_data.is_a?(Array) # Multi-step result array
|
|
404
|
+
html_parts << '<p><strong>Multi-step Result:</strong></p><ol>'
|
|
405
|
+
result_data.each_with_index do |step_hash, index|
|
|
406
|
+
html_parts << '<li>'
|
|
407
|
+
if step_hash.is_a?(Hash) # Ensure it's a hash before checking status
|
|
408
|
+
case step_hash[:status]
|
|
409
|
+
when :success
|
|
410
|
+
step_result_content = step_hash[:result]
|
|
411
|
+
# Handle potential nested result from AgentTool for display
|
|
412
|
+
if step_result_content.is_a?(Hash) && step_result_content.key?(:status)
|
|
413
|
+
html_parts << "<strong>Step #{index + 1} (Success - Delegated):</strong>"
|
|
414
|
+
html_parts << "<blockquote style='margin-left: 1em; border-left: 3px solid #dbdbdb; padding-left: 1em;'>"
|
|
415
|
+
html_parts << format_execution_result_html(step_result_content) # Recursive call
|
|
416
|
+
html_parts << '</blockquote>'
|
|
417
|
+
else
|
|
418
|
+
# Format as JSON if it's a hash or array, otherwise use to_s
|
|
419
|
+
formatted_step_content = if step_result_content.is_a?(Hash) || step_result_content.is_a?(Array)
|
|
420
|
+
JSON.pretty_generate(step_result_content)
|
|
421
|
+
else
|
|
422
|
+
step_result_content.to_s
|
|
423
|
+
end
|
|
424
|
+
html_parts << "<strong>Step #{index + 1} (Success):</strong> <pre>#{Rack::Utils.escape_html(formatted_step_content)}</pre>"
|
|
425
|
+
end
|
|
426
|
+
when :pending # <-- ADDED Pending Case for Multi-step
|
|
427
|
+
html_parts << "<strong>Step #{index + 1} (Pending):</strong>"
|
|
428
|
+
html_parts << "<pre>Job ID: #{Rack::Utils.escape_html(step_hash[:job_id].to_s)}" # Changed workflow_id to job_id
|
|
429
|
+
html_parts << "\nMessage: #{Rack::Utils.escape_html(step_hash[:message].to_s)}" if step_hash[:message]
|
|
430
|
+
html_parts << '</pre>'
|
|
431
|
+
when :error
|
|
432
|
+
html_parts << "<strong>Step #{index + 1} (Error):</strong> <pre class='has-text-danger'>#{Rack::Utils.escape_html(step_hash[:error_message].to_s)}</pre>"
|
|
433
|
+
else # Unknown status
|
|
434
|
+
html_parts << "<strong>Step #{index + 1} (Unknown Status):</strong> <pre>#{Rack::Utils.escape_html(step_hash.inspect)}</pre>"
|
|
435
|
+
end
|
|
436
|
+
else
|
|
437
|
+
# Handle case where an element in the array isn't a hash
|
|
438
|
+
html_parts << "<strong>Step #{index + 1} (Invalid format):</strong> <pre>#{Rack::Utils.escape_html(step_hash.inspect)}</pre>"
|
|
439
|
+
end
|
|
440
|
+
html_parts << '</li>'
|
|
441
|
+
end
|
|
442
|
+
html_parts << '</ol>'
|
|
443
|
+
|
|
444
|
+
elsif result_data.is_a?(Hash) # Single result/error/pending hash
|
|
445
|
+
case result_data[:status]
|
|
446
|
+
when :success
|
|
447
|
+
result_content = result_data[:result]
|
|
448
|
+
# Handle potential nested result from AgentTool
|
|
449
|
+
if result_content.is_a?(Hash) && result_content.key?(:status)
|
|
450
|
+
html_parts << '<p><strong>Result (from delegated agent):</strong></p>'
|
|
451
|
+
html_parts << "<blockquote style='margin-left: 1em; border-left: 3px solid #dbdbdb; padding-left: 1em;'>"
|
|
452
|
+
html_parts << format_execution_result_html(result_content) # Recursive call
|
|
453
|
+
html_parts << '</blockquote>'
|
|
454
|
+
else
|
|
455
|
+
# Format as JSON if it's a hash or array, otherwise use to_s
|
|
456
|
+
formatted_content = if result_content.is_a?(Hash) || result_content.is_a?(Array)
|
|
457
|
+
JSON.pretty_generate(result_content)
|
|
458
|
+
else
|
|
459
|
+
result_content.to_s
|
|
460
|
+
end
|
|
461
|
+
html_parts << "<p><strong>Result:</strong></p><pre>#{Rack::Utils.escape_html(formatted_content)}</pre>"
|
|
462
|
+
end
|
|
463
|
+
when :pending # <-- ADDED Pending Case for Single Step
|
|
464
|
+
html_parts << '<p><strong>Status: Pending</strong></p>'
|
|
465
|
+
html_parts << "<pre>Job ID: #{Rack::Utils.escape_html(result_data[:job_id].to_s)}" # Changed workflow_id to job_id
|
|
466
|
+
html_parts << "\nMessage: #{Rack::Utils.escape_html(result_data[:message].to_s)}" if result_data[:message]
|
|
467
|
+
html_parts << "\n(Use tool 'check_job_status' with this ID to get the final result)</pre>"
|
|
468
|
+
when :error
|
|
469
|
+
html_parts << "<p><strong>Error:</strong></p><pre class='has-text-danger'>#{Rack::Utils.escape_html(result_data[:error_message].to_s)}</pre>"
|
|
470
|
+
else # Unknown status within hash
|
|
471
|
+
html_parts << "<p><strong>Result (Unknown Status):</strong></p><pre>#{Rack::Utils.escape_html(result_data.inspect)}</pre>"
|
|
472
|
+
end
|
|
473
|
+
end # End if result_data.is_a?(Hash)
|
|
474
|
+
# --- End Generate HTML ---
|
|
475
|
+
|
|
476
|
+
# Return final HTML structure
|
|
477
|
+
"<div class='notification #{notification_class} mt-4'>#{html_parts.join}</div>"
|
|
478
|
+
end # end format_execution_result_html
|
|
479
|
+
|
|
480
|
+
def process_agent_response(agent_result)
|
|
481
|
+
response_data = {
|
|
482
|
+
msg_class: 'is-warning',
|
|
483
|
+
display_content: '',
|
|
484
|
+
raw_json_content: '',
|
|
485
|
+
event_id: SecureRandom.hex(4)
|
|
486
|
+
}
|
|
487
|
+
case agent_result
|
|
488
|
+
when Legate::Event
|
|
489
|
+
response_data[:event_id] = agent_result.event_id || response_data[:event_id]
|
|
490
|
+
if agent_result.role == :agent
|
|
491
|
+
content = agent_result.content
|
|
492
|
+
response_data[:raw_json_content] = content.inspect
|
|
493
|
+
if content.is_a?(Hash)
|
|
494
|
+
case content[:status]
|
|
495
|
+
when :success
|
|
496
|
+
response_data[:msg_class] = 'is-success'
|
|
497
|
+
response_data[:display_content] = content[:result].to_s
|
|
498
|
+
when :error
|
|
499
|
+
response_data[:msg_class] = 'is-danger'
|
|
500
|
+
original_error = content[:error_message] || 'Agent error (no message)'
|
|
501
|
+
response_data[:display_content] = if original_error == 'I cannot fulfill this request with the available tools (empty plan).'
|
|
502
|
+
"Sorry, I couldn't determine how to handle that request with the tools I have available."
|
|
503
|
+
else
|
|
504
|
+
original_error
|
|
505
|
+
end
|
|
506
|
+
when :pending
|
|
507
|
+
response_data[:msg_class] = 'is-warning'
|
|
508
|
+
response_data[:display_content] = "Task pending... Job ID: #{content[:job_id]}" # Changed workflow_id to job_id
|
|
509
|
+
response_data[:display_content] << " - #{content[:message]}" if content[:message]
|
|
510
|
+
else
|
|
511
|
+
response_data[:display_content] = "Agent response has unknown status: #{content[:status]}"
|
|
512
|
+
end
|
|
513
|
+
else
|
|
514
|
+
response_data[:display_content] = "Agent event content format unexpected: #{content.inspect}"
|
|
515
|
+
end
|
|
516
|
+
elsif agent_result.role == :tool_request
|
|
517
|
+
response_data[:msg_class] = 'is-info is-light'
|
|
518
|
+
content = agent_result.content
|
|
519
|
+
response_data[:raw_json_content] = content.inspect
|
|
520
|
+
if content.is_a?(Hash) && content[:tool_name]
|
|
521
|
+
tool_name = content[:tool_name]
|
|
522
|
+
params_preview = content[:params] && !content[:params].empty? ? ' with parameters' : ' (no parameters)'
|
|
523
|
+
response_data[:display_content] = "Tool Request: #{tool_name}#{params_preview}"
|
|
524
|
+
else
|
|
525
|
+
response_data[:display_content] = "Tool Request: #{content.inspect}"
|
|
526
|
+
end
|
|
527
|
+
elsif agent_result.role == :tool_result
|
|
528
|
+
content = agent_result.content
|
|
529
|
+
response_data[:raw_json_content] = content.inspect
|
|
530
|
+
if content.is_a?(Hash)
|
|
531
|
+
if content[:status] == :error || content[:error]
|
|
532
|
+
response_data[:msg_class] = 'is-danger is-light'
|
|
533
|
+
response_data[:display_content] =
|
|
534
|
+
"Tool Error: #{content[:error] || content[:error_message] || 'Unknown error'}"
|
|
535
|
+
else
|
|
536
|
+
response_data[:msg_class] = 'is-success is-light'
|
|
537
|
+
if content[:result]
|
|
538
|
+
result_str = content[:result].is_a?(String) ? content[:result] : content[:result].inspect
|
|
539
|
+
response_data[:display_content] = "Tool Result: #{result_str}"
|
|
540
|
+
else
|
|
541
|
+
response_data[:display_content] = "Tool Result: #{content.inspect}"
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
else
|
|
545
|
+
response_data[:display_content] = "Tool Result: #{content.inspect}"
|
|
546
|
+
response_data[:msg_class] = 'is-success is-light'
|
|
547
|
+
end
|
|
548
|
+
else
|
|
549
|
+
response_data[:display_content] = "Received event with unknown role: #{agent_result.role}"
|
|
550
|
+
response_data[:raw_json_content] = agent_result.inspect
|
|
551
|
+
end
|
|
552
|
+
when Hash
|
|
553
|
+
response_data[:raw_json_content] = agent_result.inspect
|
|
554
|
+
if agent_result[:status] == :error
|
|
555
|
+
response_data[:msg_class] = 'is-danger'
|
|
556
|
+
response_data[:display_content] = agent_result[:error_message] || 'An unspecified error occurred.'
|
|
557
|
+
else
|
|
558
|
+
response_data[:display_content] = "Unexpected hash format from server: #{agent_result.inspect}"
|
|
559
|
+
end
|
|
560
|
+
else
|
|
561
|
+
response_data[:raw_json_content] = agent_result.inspect
|
|
562
|
+
response_data[:display_content] = "Unexpected response type from server: #{agent_result.class}"
|
|
563
|
+
end
|
|
564
|
+
response_data
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def format_historical_agent_content(content)
|
|
568
|
+
display_content = ''
|
|
569
|
+
if content.is_a?(Hash) && content.key?(:status)
|
|
570
|
+
case content[:status]
|
|
571
|
+
when :success
|
|
572
|
+
display_content = content[:result]
|
|
573
|
+
when :error
|
|
574
|
+
original_error = content[:error_message] || 'Agent error (no message)'
|
|
575
|
+
display_content = if original_error == 'I cannot fulfill this request with the available tools (empty plan).'
|
|
576
|
+
"Sorry, I couldn't determine how to handle that request with the tools I have available."
|
|
577
|
+
else
|
|
578
|
+
original_error
|
|
579
|
+
end
|
|
580
|
+
when :pending
|
|
581
|
+
display_content = "Task pending... Job ID: #{content[:job_id]}" # Changed workflow_id to job_id
|
|
582
|
+
display_content << " - #{content[:message]}" if content[:message]
|
|
583
|
+
else
|
|
584
|
+
display_content = "Agent response (unknown status): #{content.inspect}"
|
|
585
|
+
end
|
|
586
|
+
elsif content.is_a?(Hash) && content.key?(:tool_name)
|
|
587
|
+
display_content = "Tool request: #{content[:tool_name]}"
|
|
588
|
+
display_content += ' with parameters' if content[:params] && !content[:params].empty?
|
|
589
|
+
elsif content.is_a?(Hash) && (content.key?(:result) || content.key?(:error))
|
|
590
|
+
if content[:error]
|
|
591
|
+
display_content = "Tool error: #{content[:error]}"
|
|
592
|
+
elsif content[:result]
|
|
593
|
+
result_str = content[:result].is_a?(String) ? content[:result] : content[:result].inspect
|
|
594
|
+
display_content = "Tool result: #{result_str}"
|
|
595
|
+
else
|
|
596
|
+
display_content = "Tool response: #{content.inspect}"
|
|
597
|
+
end
|
|
598
|
+
elsif content.is_a?(Array)
|
|
599
|
+
display_content = "Agent response (array): #{content.inspect}"
|
|
600
|
+
else
|
|
601
|
+
display_content = content.to_s
|
|
602
|
+
end
|
|
603
|
+
display_content.to_s
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def summarize_session(session_object)
|
|
607
|
+
return 'Invalid session object' unless session_object.is_a?(Legate::Session)
|
|
608
|
+
|
|
609
|
+
created_at_formatted = session_object.created_at.strftime('%b %d, %Y %H:%M')
|
|
610
|
+
updated_at_formatted = session_object.updated_at.strftime('%b %d, %Y %H:%M')
|
|
611
|
+
event_count = session_object.events&.count || 0
|
|
612
|
+
messages_text = event_count == 1 ? 'message' : 'messages'
|
|
613
|
+
preview_text = 'Session started'
|
|
614
|
+
if event_count.zero?
|
|
615
|
+
preview_text = "Empty session (created #{created_at_formatted})"
|
|
616
|
+
else
|
|
617
|
+
first_user_text_event = session_object.events.find do |event|
|
|
618
|
+
event.role == :user && event.content.is_a?(String) && !event.content.strip.empty?
|
|
619
|
+
end
|
|
620
|
+
if first_user_text_event
|
|
621
|
+
words = first_user_text_event.content.strip.split(/\s+/)
|
|
622
|
+
preview = words.take(10).join(' ')
|
|
623
|
+
preview_text = "#{preview}#{words.size > 10 ? '...' : ''}"
|
|
624
|
+
elsif session_object.events.any? { |e| e.role == :user }
|
|
625
|
+
preview_text = 'Contains non-text user messages'
|
|
626
|
+
else
|
|
627
|
+
preview_text = 'Agent-initiated session'
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
"Chat from #{created_at_formatted} (Last active: #{updated_at_formatted}) (#{event_count} #{messages_text}): #{preview_text}"
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def pretty_json(object)
|
|
634
|
+
JSON.pretty_generate(object)
|
|
635
|
+
rescue StandardError => e
|
|
636
|
+
object.inspect
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# --- START: MERMAID HELPERS (Corrected for delegate_task rich result) ---
|
|
640
|
+
def generate_mermaid_sequence_diagram(final_agent_event_content, original_user_input)
|
|
641
|
+
return '' unless final_agent_event_content.is_a?(Hash)
|
|
642
|
+
|
|
643
|
+
mermaid_def = ['sequenceDiagram']
|
|
644
|
+
participants = Set.new
|
|
645
|
+
# Initial call: current_agent_name is just "Agent"
|
|
646
|
+
collect_participants_recursive(final_agent_event_content, participants, 'Agent')
|
|
647
|
+
|
|
648
|
+
participants.each { |p| mermaid_def << " participant #{p}" }
|
|
649
|
+
|
|
650
|
+
mermaid_def << " User->>Agent: #{escape_mermaid_label(original_user_input)}"
|
|
651
|
+
# Initial call: current_agent_is "Agent", final_recipient_is "User"
|
|
652
|
+
append_plan_to_mermaid_recursive(final_agent_event_content, 'Agent', 'User', mermaid_def)
|
|
653
|
+
|
|
654
|
+
mermaid_def.join("\n")
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def collect_participants_recursive(event_content, participants_set, current_agent_alias = 'Agent')
|
|
658
|
+
participants_set.add('User')
|
|
659
|
+
participants_set.add(current_agent_alias)
|
|
660
|
+
|
|
661
|
+
plan_details = event_content[:plan_details]
|
|
662
|
+
return unless plan_details.is_a?(Array)
|
|
663
|
+
|
|
664
|
+
plan_details.each do |step_in_plan|
|
|
665
|
+
tool_name_str = step_in_plan[:tool_name]&.to_s
|
|
666
|
+
participants_set.add("Tool(#{tool_name_str})") if tool_name_str && !tool_name_str.empty?
|
|
667
|
+
|
|
668
|
+
next unless step_in_plan[:tool_name]&.to_sym == :delegate_task &&
|
|
669
|
+
step_in_plan == plan_details.last &&
|
|
670
|
+
event_content.dig(:result, :status) == :success &&
|
|
671
|
+
event_content.dig(:result, :result).is_a?(Hash) &&
|
|
672
|
+
event_content.dig(:result, :result, :plan_details)
|
|
673
|
+
|
|
674
|
+
delegated_agent_full_content = event_content.dig(:result, :result)
|
|
675
|
+
target_agent_name_param = step_in_plan.dig(:params,
|
|
676
|
+
:target_agent_name) || step_in_plan.dig(:params,
|
|
677
|
+
'target_agent_name')
|
|
678
|
+
delegated_agent_actual_name = delegated_agent_full_content.dig(:name)&.to_s || target_agent_name_param || 'DelegatedAgent'
|
|
679
|
+
delegated_agent_participant_alias = "Agent(#{delegated_agent_actual_name})"
|
|
680
|
+
|
|
681
|
+
participants_set.add(delegated_agent_participant_alias)
|
|
682
|
+
collect_participants_recursive(delegated_agent_full_content, participants_set,
|
|
683
|
+
delegated_agent_participant_alias)
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def append_plan_to_mermaid_recursive(event_content, current_agent_participant_name, final_recipient_name,
|
|
688
|
+
mermaid_def_array)
|
|
689
|
+
plan_details = event_content[:plan_details]
|
|
690
|
+
return unless plan_details.is_a?(Array)
|
|
691
|
+
|
|
692
|
+
plan_details.each_with_index do |step_in_plan, index|
|
|
693
|
+
tool_name_str = step_in_plan[:tool_name]&.to_s || 'UnknownTool'
|
|
694
|
+
tool_participant = "Tool(#{tool_name_str})"
|
|
695
|
+
|
|
696
|
+
params_summary = summarize_for_mermaid(step_in_plan[:params]) # Removed max_length override
|
|
697
|
+
mermaid_def_array << " #{current_agent_participant_name}->>#{tool_participant}: Call #{tool_name_str} with #{params_summary}"
|
|
698
|
+
|
|
699
|
+
original_tool_output_for_this_step = step_in_plan[:result]
|
|
700
|
+
|
|
701
|
+
if step_in_plan == plan_details.last &&
|
|
702
|
+
step_in_plan[:tool_name]&.to_sym == :delegate_task &&
|
|
703
|
+
event_content.dig(:result, :status) == :success &&
|
|
704
|
+
event_content.dig(:result, :result).is_a?(Hash) &&
|
|
705
|
+
event_content.dig(:result, :result, :plan_details)
|
|
706
|
+
|
|
707
|
+
original_tool_output_for_this_step = event_content[:result][:result]
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
if original_tool_output_for_this_step.is_a?(Hash) &&
|
|
711
|
+
original_tool_output_for_this_step.key?(:plan_details) &&
|
|
712
|
+
step_in_plan[:tool_name]&.to_sym == :delegate_task
|
|
713
|
+
|
|
714
|
+
delegated_agent_content = original_tool_output_for_this_step
|
|
715
|
+
target_agent_name_param = step_in_plan.dig(:params,
|
|
716
|
+
:target_agent_name) || step_in_plan.dig(:params,
|
|
717
|
+
'target_agent_name')
|
|
718
|
+
effective_delegated_name = delegated_agent_content[:name]&.to_s || target_agent_name_param&.to_s || 'DelegatedAgent'
|
|
719
|
+
delegated_agent_participant = "Agent(#{effective_delegated_name})"
|
|
720
|
+
task_for_delegated = summarize_for_mermaid(step_in_plan.dig(:params,
|
|
721
|
+
:task) || step_in_plan.dig(:params, 'task'))
|
|
722
|
+
mermaid_def_array << " #{tool_participant}->>#{delegated_agent_participant}: Run task: #{task_for_delegated || 'Delegated Task'}"
|
|
723
|
+
append_plan_to_mermaid_recursive(delegated_agent_content, delegated_agent_participant, tool_participant,
|
|
724
|
+
mermaid_def_array)
|
|
725
|
+
delegated_outcome_summary = if delegated_agent_content[:status] == :success
|
|
726
|
+
"Delegated success: #{summarize_for_mermaid(delegated_agent_content[:result])}"
|
|
727
|
+
elsif delegated_agent_content[:status] == :error
|
|
728
|
+
"Delegated error: #{summarize_for_mermaid(delegated_agent_content[:error_message])}"
|
|
729
|
+
else
|
|
730
|
+
"Delegated status: #{delegated_agent_content[:status]}"
|
|
731
|
+
end
|
|
732
|
+
mermaid_def_array << " #{tool_participant}-->>#{current_agent_participant_name}: #{delegated_outcome_summary}"
|
|
733
|
+
elsif original_tool_output_for_this_step.is_a?(Hash)
|
|
734
|
+
status = original_tool_output_for_this_step[:status]&.to_s || 'unknown'
|
|
735
|
+
case status.to_sym
|
|
736
|
+
when :success
|
|
737
|
+
result_value = original_tool_output_for_this_step[:result]
|
|
738
|
+
if result_value.is_a?(String) && result_value == '[Complex Result Structure]'
|
|
739
|
+
actual_result = event_content[:result][:result]
|
|
740
|
+
mermaid_def_array << if actual_result.is_a?(String)
|
|
741
|
+
" #{tool_participant}-->>#{current_agent_participant_name}: Result: \"#{actual_result}\""
|
|
742
|
+
else
|
|
743
|
+
" #{tool_participant}-->>#{current_agent_participant_name}: Result: [Complex Result Structure]"
|
|
744
|
+
end
|
|
745
|
+
else
|
|
746
|
+
result_summary = summarize_for_mermaid(original_tool_output_for_this_step[:result])
|
|
747
|
+
mermaid_def_array << " #{tool_participant}-->>#{current_agent_participant_name}: Result: #{result_summary}"
|
|
748
|
+
end
|
|
749
|
+
when :error
|
|
750
|
+
error_summary = summarize_for_mermaid(original_tool_output_for_this_step[:error_message] || 'Unknown Error')
|
|
751
|
+
mermaid_def_array << " #{tool_participant}-->>#{current_agent_participant_name}: Error: #{error_summary}"
|
|
752
|
+
when :pending
|
|
753
|
+
job_id_summary = summarize_for_mermaid(original_tool_output_for_this_step[:job_id] || 'N/A') # Changed from workflow_id
|
|
754
|
+
message_summary = original_tool_output_for_this_step[:message] ? " (#{summarize_for_mermaid(original_tool_output_for_this_step[:message])})" : ''
|
|
755
|
+
mermaid_def_array << " #{tool_participant}-->>#{current_agent_participant_name}: Pending (Job ID: #{job_id_summary})#{message_summary}"
|
|
756
|
+
else
|
|
757
|
+
mermaid_def_array << " #{tool_participant}-->>#{current_agent_participant_name}: Result (Status: #{status}): #{summarize_for_mermaid(original_tool_output_for_this_step)}"
|
|
758
|
+
end
|
|
759
|
+
else
|
|
760
|
+
mermaid_def_array << " #{tool_participant}-->>#{current_agent_participant_name}: Malformed Result: #{summarize_for_mermaid(original_tool_output_for_this_step)}"
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
final_response_summary = ''
|
|
765
|
+
if event_content[:status] == :success
|
|
766
|
+
core_result = if event_content[:result].is_a?(Hash) && event_content[:result][:status] == :success && event_content[:result].key?(:result)
|
|
767
|
+
event_content[:result][:result]
|
|
768
|
+
else
|
|
769
|
+
event_content[:result]
|
|
770
|
+
end
|
|
771
|
+
if core_result.is_a?(String) && core_result == '[Complex Result Structure]'
|
|
772
|
+
actual_result = event_content[:result][:result]
|
|
773
|
+
final_response_summary = if actual_result.is_a?(String)
|
|
774
|
+
"Final Result: \"#{actual_result}\""
|
|
775
|
+
else
|
|
776
|
+
'Final Result: [Complex Result Structure]'
|
|
777
|
+
end
|
|
778
|
+
else
|
|
779
|
+
final_response_summary = "Final Result: #{summarize_for_mermaid(core_result)}"
|
|
780
|
+
end
|
|
781
|
+
elsif event_content[:status] == :error
|
|
782
|
+
final_response_summary = "Final Error: #{summarize_for_mermaid(event_content[:error_message])}"
|
|
783
|
+
elsif event_content[:status] == :pending
|
|
784
|
+
job_id_summary = summarize_for_mermaid(event_content[:job_id]) # Changed from workflow_id
|
|
785
|
+
message_summary = event_content[:message] ? " - #{summarize_for_mermaid(event_content[:message])}" : ''
|
|
786
|
+
final_response_summary = "Task Pending: Job ID #{job_id_summary}#{message_summary}"
|
|
787
|
+
else
|
|
788
|
+
final_response_summary = "Final Response (Status: #{event_content[:status]}): #{summarize_for_mermaid(event_content)}"
|
|
789
|
+
end
|
|
790
|
+
mermaid_def_array << " #{current_agent_participant_name}-->>#{final_recipient_name}: #{final_response_summary}"
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def summarize_for_mermaid(data, max_length = 700)
|
|
794
|
+
return 'nil' if data.nil?
|
|
795
|
+
|
|
796
|
+
raw_summary_str = ''
|
|
797
|
+
if data.is_a?(Hash)
|
|
798
|
+
if data.key?(:result) && data[:result].is_a?(Hash) && data[:result].key?(:content)
|
|
799
|
+
content_str = data[:result][:content].to_s
|
|
800
|
+
content_preview = content_str.length > 50 ? "#{content_str[0..50]}..." : content_str
|
|
801
|
+
raw_summary_str = "{status: #{data[:status]}, result: {content: \"#{content_preview}\"}}"
|
|
802
|
+
else
|
|
803
|
+
items = data.map do |k, v_raw|
|
|
804
|
+
v_str = if v_raw.is_a?(Hash)
|
|
805
|
+
"{#{v_raw.keys.take(3).join(', ')}#{v_raw.keys.size > 3 ? ', ...' : ''}}"
|
|
806
|
+
elsif v_raw.is_a?(String) && v_raw.length <= 30 && !v_raw.match?(/[:;()`"'\n\\]/)
|
|
807
|
+
v_raw
|
|
808
|
+
elsif v_raw.is_a?(Array) && v_raw.size <= 3
|
|
809
|
+
v_raw.inspect
|
|
810
|
+
elsif v_raw.is_a?(Array)
|
|
811
|
+
"[#{v_raw.size} items]"
|
|
812
|
+
else
|
|
813
|
+
v_raw.inspect
|
|
814
|
+
end
|
|
815
|
+
"#{k}: #{v_str}"
|
|
816
|
+
end
|
|
817
|
+
raw_summary_str = "{#{items.join(', ')}}"
|
|
818
|
+
end
|
|
819
|
+
elsif data.is_a?(Array)
|
|
820
|
+
if data.size <= 5
|
|
821
|
+
items_str = data.map do |item|
|
|
822
|
+
if item.is_a?(Hash)
|
|
823
|
+
"{#{item.keys.take(2).join(', ')}#{item.keys.size > 2 ? ', ...' : ''}}"
|
|
824
|
+
else
|
|
825
|
+
item.inspect
|
|
826
|
+
end
|
|
827
|
+
end.join(', ')
|
|
828
|
+
raw_summary_str = "[#{items_str}]"
|
|
829
|
+
else
|
|
830
|
+
items_str = data.take(3).map do |item|
|
|
831
|
+
if item.is_a?(Hash)
|
|
832
|
+
"{#{item.keys.take(2).join(', ')}#{item.keys.size > 2 ? ', ...' : ''}}"
|
|
833
|
+
else
|
|
834
|
+
item.inspect
|
|
835
|
+
end
|
|
836
|
+
end.join(', ')
|
|
837
|
+
raw_summary_str = "[#{items_str}, ... (#{data.size} total items)]"
|
|
838
|
+
end
|
|
839
|
+
else
|
|
840
|
+
raw_summary_str = data.to_s
|
|
841
|
+
end
|
|
842
|
+
escaped_summary = escape_mermaid_label(raw_summary_str)
|
|
843
|
+
if escaped_summary.length > max_length
|
|
844
|
+
escaped_summary[0...(max_length - 3)] + '...'
|
|
845
|
+
else
|
|
846
|
+
escaped_summary
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# MODIFIED: Simplified escape_mermaid_label
|
|
851
|
+
def escape_mermaid_label(text)
|
|
852
|
+
return '' if text.nil?
|
|
853
|
+
|
|
854
|
+
s = text.to_s
|
|
855
|
+
s = s.gsub(/#/, '#hash;') # Escape # to prevent it being a comment/directive
|
|
856
|
+
s = s.gsub(/"/, '#quot;') # For quoted strings within messages; Mermaid prefers this over "
|
|
857
|
+
s = s.gsub(/;/, '#semi;') # Semicolons can end Mermaid statements
|
|
858
|
+
|
|
859
|
+
# Replace newlines with <br> for explicit line breaks in Mermaid labels
|
|
860
|
+
s = s.gsub(/\n/, '<br>')
|
|
861
|
+
|
|
862
|
+
# Escape sequences that might be misinterpreted as Mermaid diagram arrows/lines
|
|
863
|
+
s = s.gsub(/->>/, '->>')
|
|
864
|
+
s = s.gsub(/-->>/, '-->>')
|
|
865
|
+
s = s.gsub(/->/, '->')
|
|
866
|
+
s.gsub(/--/, '- -') # also to prevent '--' being parsed as start of solid line in some contexts
|
|
867
|
+
|
|
868
|
+
# Parentheses and backticks are often fine in message text, remove aggressive escaping for them for now.
|
|
869
|
+
# Colons are fine.
|
|
870
|
+
end
|
|
871
|
+
# --- END MERMAID HELPERS ---
|
|
872
|
+
end # end helpers
|
|
873
|
+
|
|
874
|
+
# --- Private Helper Methods ---
|
|
875
|
+
private
|
|
876
|
+
|
|
877
|
+
# Initialize the authentication manager store (in-memory)
|
|
878
|
+
def initialize_auth_manager_store
|
|
879
|
+
# Create the in-memory store for authentication configuration
|
|
880
|
+
auth_store = Legate::Auth::ManagerStore::InMemoryStore.new
|
|
881
|
+
|
|
882
|
+
# Set the store on the singleton Auth::Manager
|
|
883
|
+
auth_manager = Legate::Auth::Manager.instance
|
|
884
|
+
auth_manager.set_store(auth_store, load_immediately: false)
|
|
885
|
+
|
|
886
|
+
@logger.info('Authentication Manager Store initialized (in-memory).')
|
|
887
|
+
rescue StandardError => e
|
|
888
|
+
@logger.error("Failed to initialize Auth Manager Store: #{e.class} - #{e.message}")
|
|
889
|
+
@logger.debug(e.backtrace.first(3).join("\n"))
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
def _stop_agent(name)
|
|
893
|
+
agent = @agents[name]
|
|
894
|
+
if agent
|
|
895
|
+
logger.info("Stopping agent '#{name}'...")
|
|
896
|
+
begin
|
|
897
|
+
agent.stop
|
|
898
|
+
@agents.delete(name)
|
|
899
|
+
logger.info("Agent '#{name}' stopped.")
|
|
900
|
+
Legate::ActivityLog.safe_log(:agent_stopped, { name: name })
|
|
901
|
+
true
|
|
902
|
+
rescue StandardError => e
|
|
903
|
+
logger.error("Error stopping agent '#{name}': #{e.message}")
|
|
904
|
+
false
|
|
905
|
+
end
|
|
906
|
+
else
|
|
907
|
+
logger.warn("Attempted to stop non-running agent: '#{name}'.")
|
|
908
|
+
true
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def _start_agent(name)
|
|
913
|
+
return @agents[name] if @agents.key?(name)
|
|
914
|
+
|
|
915
|
+
# Use proper error handling instead of halt (which only works in request contexts)
|
|
916
|
+
unless @definition_store
|
|
917
|
+
logger.error("Definition Store unavailable, cannot start agent '#{name}'.")
|
|
918
|
+
return nil
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
agent_definition = nil
|
|
922
|
+
begin
|
|
923
|
+
agent_definition = @definition_store.get_definition(name)
|
|
924
|
+
rescue StandardError => e
|
|
925
|
+
logger.error("Store error fetching definition for starting agent '#{name}': #{e.message}")
|
|
926
|
+
return nil
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
unless agent_definition
|
|
930
|
+
logger.error("Agent definition not found for '#{name}', cannot start.")
|
|
931
|
+
return nil
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
agent_description = agent_definition[:description]
|
|
935
|
+
selected_tool_names = agent_definition[:tools].map(&:to_sym)
|
|
936
|
+
model_name = agent_definition[:model]
|
|
937
|
+
fallback_mode_sym = agent_definition[:fallback_mode]
|
|
938
|
+
mcp_servers_json = agent_definition[:mcp_servers_json]
|
|
939
|
+
agent_instruction = agent_definition[:instruction]
|
|
940
|
+
|
|
941
|
+
mcp_server_count = 0
|
|
942
|
+
begin
|
|
943
|
+
parsed_mcp = JSON.parse(mcp_servers_json)
|
|
944
|
+
mcp_server_count = parsed_mcp.is_a?(Array) ? parsed_mcp.count : 0
|
|
945
|
+
rescue JSON::ParserError
|
|
946
|
+
end
|
|
947
|
+
logger.info("Attempting to start agent '#{name}' (Model: #{model_name}, Fallback: #{fallback_mode_sym}, MCP: #{mcp_server_count} servers)... Selected Tools: #{selected_tool_names.inspect}")
|
|
948
|
+
|
|
949
|
+
# Convert hash to Legate::AgentDefinition object
|
|
950
|
+
definition_obj = Legate::AgentDefinition.from_hash(agent_definition)
|
|
951
|
+
unless definition_obj
|
|
952
|
+
logger.error("Failed to convert agent definition hash to Legate::AgentDefinition object for '#{name}'")
|
|
953
|
+
return nil
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
agent = Legate::Agent.new(
|
|
957
|
+
definition: definition_obj,
|
|
958
|
+
session_service: @session_service
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
selected_tool_names.each do |tn|
|
|
962
|
+
inst = Legate::GlobalToolManager.create_instance(tn)
|
|
963
|
+
if inst
|
|
964
|
+
logger.debug("Adding selected native tool: #{tn}")
|
|
965
|
+
agent.add_tool(inst)
|
|
966
|
+
else
|
|
967
|
+
logger.debug("Tool '#{tn}' selected but not found in GlobalToolManager (assuming MCP tool).")
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
agent.start
|
|
972
|
+
@agents[name] = agent
|
|
973
|
+
logger.info("Agent '#{name}' started successfully.")
|
|
974
|
+
Legate::ActivityLog.safe_log(:agent_started, { name: name })
|
|
975
|
+
agent
|
|
976
|
+
rescue StandardError => e
|
|
977
|
+
logger.error("Failed to start agent '#{name}': #{e.class} - #{e.message}")
|
|
978
|
+
logger.error(e.backtrace.join("\n"))
|
|
979
|
+
@agents.delete(name)
|
|
980
|
+
nil
|
|
981
|
+
end
|
|
982
|
+
end # End App class
|
|
983
|
+
end # End Web module
|
|
984
|
+
end # End Legate module
|