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,803 @@
|
|
|
1
|
+
# File: lib/legate/web/routes/agent_definition_routes.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Legate
|
|
5
|
+
module Web
|
|
6
|
+
module AgentDefinitionRoutes
|
|
7
|
+
def self.registered(app)
|
|
8
|
+
# GET /agents - Display the main agent management page.
|
|
9
|
+
app.get '/agents' do
|
|
10
|
+
# `self` is the Sinatra app instance in a route block
|
|
11
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
12
|
+
|
|
13
|
+
view_agents_list = []
|
|
14
|
+
if definition_store
|
|
15
|
+
begin
|
|
16
|
+
agent_definitions = definition_store.list_definitions
|
|
17
|
+
|
|
18
|
+
active_agents_hash = instance_variable_get(:@agents)
|
|
19
|
+
view_agents_list = agent_definitions.map do |definition|
|
|
20
|
+
next unless definition && definition[:name] # Ensure definition and name are present
|
|
21
|
+
|
|
22
|
+
view_model = definition.dup # Create a mutable copy for the view
|
|
23
|
+
view_model[:configured_tools] = view_model.delete(:tools) || [] # Ensure it's an array
|
|
24
|
+
# Include agent_type in the view model, default to :llm if not present
|
|
25
|
+
view_model[:agent_type] = view_model[:agent_type]&.to_sym || :llm
|
|
26
|
+
# Running state is determined by the in-memory @agents hash, which
|
|
27
|
+
# is keyed by the agent's STRING name (the route-param form). The
|
|
28
|
+
# definition's :name is a Symbol, so normalize before the lookup.
|
|
29
|
+
view_model[:running] = active_agents_hash.key?(definition[:name].to_s)
|
|
30
|
+
view_model
|
|
31
|
+
end.compact # Remove any nils from failed definition fetches
|
|
32
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
33
|
+
logger.error("Store error fetching agent list (from AgentDefinitionRoutes): #{e.message}")
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
logger.error('Definition Store unavailable during GET /agents (from AgentDefinitionRoutes)')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
instance_variable_set(:@view_agents, view_agents_list)
|
|
40
|
+
instance_variable_set(:@available_tools, Legate::GlobalToolManager.list_all_tools)
|
|
41
|
+
instance_variable_set(:@available_models, Legate::Web::App::AVAILABLE_MODELS) # Access constant via App class
|
|
42
|
+
slim :agents
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# POST /agents - Create a new agent definition.
|
|
46
|
+
app.post '/agents' do
|
|
47
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
48
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
49
|
+
|
|
50
|
+
agent_name = params['name']&.strip
|
|
51
|
+
agent_description = params['description']&.strip
|
|
52
|
+
selected_tools = params['tools'] || []
|
|
53
|
+
selected_model = params['model']&.strip
|
|
54
|
+
selected_fallback = params['fallback_mode'] || 'error'
|
|
55
|
+
mcp_servers_json = params['mcp_servers_json']&.strip
|
|
56
|
+
instruction = params['instruction']&.strip
|
|
57
|
+
agent_type = params['agent_type']&.strip || 'llm'
|
|
58
|
+
planning_strategy = params['planning_strategy']&.strip
|
|
59
|
+
planning_strategy = 'plan' unless %w[plan react].include?(planning_strategy)
|
|
60
|
+
output_key = params['output_key']&.strip
|
|
61
|
+
output_key = output_key.empty? ? nil : output_key.to_sym if output_key
|
|
62
|
+
|
|
63
|
+
# Loop specific params
|
|
64
|
+
loop_max_iterations = params['loop_max_iterations']&.strip
|
|
65
|
+
loop_condition_state_key = params['loop_condition_state_key']&.strip
|
|
66
|
+
loop_condition_expected_value = params['loop_condition_expected_value']&.strip
|
|
67
|
+
|
|
68
|
+
# Get sub-agents for workflow agents
|
|
69
|
+
sub_agent_names = params['sub_agent_names'] || []
|
|
70
|
+
|
|
71
|
+
# Remove self from sub-agent selections to prevent circular references
|
|
72
|
+
sub_agent_names = sub_agent_names.reject { |name| name == agent_name }
|
|
73
|
+
|
|
74
|
+
# Validate agent_type
|
|
75
|
+
agent_type = 'llm' unless %w[llm sequential parallel loop].include?(agent_type)
|
|
76
|
+
|
|
77
|
+
mcp_servers_json_to_save = mcp_servers_json.nil? || mcp_servers_json.empty? ? '[]' : mcp_servers_json
|
|
78
|
+
model_to_save = selected_model && !selected_model.empty? ? selected_model : Legate::Agent::DEFAULT_MODEL
|
|
79
|
+
|
|
80
|
+
if agent_name.nil? || agent_name.empty? || agent_description.nil? || agent_description.empty?
|
|
81
|
+
status 400
|
|
82
|
+
halt "<div class='notification is-danger'>Name and description required.</div>"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
definition_params = {
|
|
87
|
+
name: agent_name,
|
|
88
|
+
description: agent_description,
|
|
89
|
+
tools: selected_tools,
|
|
90
|
+
model: model_to_save,
|
|
91
|
+
fallback_mode: selected_fallback, # Store will convert to symbol
|
|
92
|
+
mcp_servers_json: mcp_servers_json_to_save,
|
|
93
|
+
instruction: instruction,
|
|
94
|
+
agent_type: agent_type,
|
|
95
|
+
planning_strategy: planning_strategy,
|
|
96
|
+
output_key: output_key
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Add sub_agent_names or delegation_targets depending on type
|
|
100
|
+
unless sub_agent_names.empty?
|
|
101
|
+
if agent_type == 'llm'
|
|
102
|
+
definition_params[:delegation_targets] = sub_agent_names
|
|
103
|
+
else
|
|
104
|
+
definition_params[:sub_agent_names] = sub_agent_names
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Add loop params if type is loop
|
|
109
|
+
if agent_type == 'loop'
|
|
110
|
+
definition_params[:loop_max_iterations] = loop_max_iterations.to_i if loop_max_iterations && !loop_max_iterations.empty?
|
|
111
|
+
definition_params[:loop_condition_state_key] = loop_condition_state_key.to_sym if loop_condition_state_key && !loop_condition_state_key.empty?
|
|
112
|
+
definition_params[:loop_condition_expected_value] = loop_condition_expected_value if loop_condition_expected_value && !loop_condition_expected_value.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
definition_store.save_definition(**definition_params)
|
|
116
|
+
logger.info("Agent '#{agent_name}' definition saved (from AgentDefinitionRoutes)")
|
|
117
|
+
Legate::ActivityLog.safe_log(:agent_created, { name: agent_name })
|
|
118
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
119
|
+
logger.error("Store error saving agent definition (from AgentDefinitionRoutes): #{e.message}")
|
|
120
|
+
halt 500, 'Error saving agent definition.'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
content_type :html
|
|
124
|
+
agent_data = {
|
|
125
|
+
name: agent_name, description: agent_description, running: false,
|
|
126
|
+
configured_tools: selected_tools, model: model_to_save,
|
|
127
|
+
fallback_mode: selected_fallback.to_sym, # Ensure symbol for partial
|
|
128
|
+
instruction: instruction,
|
|
129
|
+
agent_type: agent_type.to_sym, # Convert to symbol for the partial
|
|
130
|
+
is_new: true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Include sub_agent_names if this is a workflow agent
|
|
134
|
+
agent_data[:sub_agent_names] = sub_agent_names if agent_type != 'llm' && !sub_agent_names.empty?
|
|
135
|
+
|
|
136
|
+
# available_tools needed by _agent_card partial
|
|
137
|
+
current_available_tools = Legate::GlobalToolManager.list_all_tools
|
|
138
|
+
agent_row_html = slim(:_agent_card, layout: false,
|
|
139
|
+
locals: { agent_info: agent_data, available_tools: current_available_tools })
|
|
140
|
+
oob_remove_message_html = "<tr id='no-agents-row' hx-swap-oob='true'></tr>"
|
|
141
|
+
headers 'HX-Trigger' => 'closeCreateAgentForm'
|
|
142
|
+
agent_row_html + oob_remove_message_html
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# DELETE /agents/:name - Delete an agent definition.
|
|
146
|
+
app.delete '/agents/:name' do |name|
|
|
147
|
+
logger.info("Received request to delete agent '#{name}' (from AgentDefinitionRoutes)")
|
|
148
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
149
|
+
active_agents_hash = instance_variable_get(:@agents)
|
|
150
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
151
|
+
|
|
152
|
+
if active_agents_hash.key?(name)
|
|
153
|
+
logger.info("Stopping running agent '#{name}' before deletion (from AgentDefinitionRoutes)...")
|
|
154
|
+
begin
|
|
155
|
+
active_agents_hash[name].stop
|
|
156
|
+
active_agents_hash.delete(name)
|
|
157
|
+
logger.info("Agent '#{name}' stopped (from AgentDefinitionRoutes).")
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
logger.error("Error stopping agent (from AgentDefinitionRoutes): #{e.message}")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
begin
|
|
163
|
+
definition_store.delete_definition(name)
|
|
164
|
+
logger.info("Agent '#{name}' definition deleted (from AgentDefinitionRoutes).")
|
|
165
|
+
Legate::ActivityLog.safe_log(:agent_deleted, { name: name })
|
|
166
|
+
status 200
|
|
167
|
+
body ''
|
|
168
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
169
|
+
logger.error("Store error deleting agent '#{name}' (from AgentDefinitionRoutes): #{e.message}")
|
|
170
|
+
halt 500, 'Database error during deletion.'
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# POST /agents/:name/duplicate - Create a copy of an agent.
|
|
175
|
+
app.post '/agents/:name/duplicate' do |name|
|
|
176
|
+
logger.info("Received request to duplicate agent '#{name}'")
|
|
177
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
178
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
179
|
+
|
|
180
|
+
original_definition = definition_store.get_definition(name)
|
|
181
|
+
halt 404, 'Agent not found' unless original_definition
|
|
182
|
+
|
|
183
|
+
# Generate unique name for the copy
|
|
184
|
+
base_name = "Copy of #{name}"
|
|
185
|
+
new_name = base_name
|
|
186
|
+
counter = 1
|
|
187
|
+
while definition_store.get_definition(new_name)
|
|
188
|
+
counter += 1
|
|
189
|
+
new_name = "#{base_name} (#{counter})"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Create the duplicate definition
|
|
193
|
+
new_definition = original_definition.dup
|
|
194
|
+
new_definition[:name] = new_name
|
|
195
|
+
new_definition[:description] = "Copy of: #{original_definition[:description]}"
|
|
196
|
+
|
|
197
|
+
begin
|
|
198
|
+
definition_store.save_definition(new_name, new_definition)
|
|
199
|
+
Legate::ActivityLog.safe_log(:agent_created, { name: new_name, source: 'duplicate' })
|
|
200
|
+
logger.info("Agent '#{name}' duplicated as '#{new_name}'")
|
|
201
|
+
|
|
202
|
+
# Redirect to the new agent
|
|
203
|
+
if request.xhr?
|
|
204
|
+
headers 'HX-Redirect' => "/agents/#{URI.encode_www_form_component(new_name)}"
|
|
205
|
+
status 200
|
|
206
|
+
body ''
|
|
207
|
+
else
|
|
208
|
+
redirect "/agents/#{URI.encode_www_form_component(new_name)}"
|
|
209
|
+
end
|
|
210
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
211
|
+
logger.error("Error duplicating agent: #{e.message}")
|
|
212
|
+
halt 500, 'Error duplicating agent.'
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# GET /agents/:name/export - Export agent configuration as JSON.
|
|
217
|
+
app.get '/agents/:name/export' do |name|
|
|
218
|
+
logger.info("Received request to export agent '#{name}'")
|
|
219
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
220
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
221
|
+
|
|
222
|
+
agent_definition = definition_store.get_definition(name)
|
|
223
|
+
halt 404, 'Agent not found' unless agent_definition
|
|
224
|
+
|
|
225
|
+
# Prepare export data (clean up internal fields)
|
|
226
|
+
export_data = {
|
|
227
|
+
name: agent_definition[:name],
|
|
228
|
+
description: agent_definition[:description],
|
|
229
|
+
model: agent_definition[:model],
|
|
230
|
+
instruction: agent_definition[:instruction],
|
|
231
|
+
tools: agent_definition[:tools],
|
|
232
|
+
fallback_mode: agent_definition[:fallback_mode],
|
|
233
|
+
agent_type: agent_definition[:agent_type],
|
|
234
|
+
sub_agent_names: agent_definition[:sub_agent_names],
|
|
235
|
+
mcp_servers_json: agent_definition[:mcp_servers_json]
|
|
236
|
+
}.compact
|
|
237
|
+
|
|
238
|
+
content_type 'application/json'
|
|
239
|
+
attachment "#{name}.json"
|
|
240
|
+
JSON.pretty_generate(export_data)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# GET /agents/:name/download - Download agent as Ruby file.
|
|
244
|
+
app.get '/agents/:name/download' do |name|
|
|
245
|
+
logger.info("Received request to download agent '#{name}' as Ruby file")
|
|
246
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
247
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
248
|
+
|
|
249
|
+
agent_definition = definition_store.get_definition(name)
|
|
250
|
+
halt 404, 'Agent not found' unless agent_definition
|
|
251
|
+
|
|
252
|
+
# Ensure name is included in the definition hash
|
|
253
|
+
agent_definition[:name] ||= name
|
|
254
|
+
|
|
255
|
+
# Generate Ruby code
|
|
256
|
+
require 'legate/agent_code_generator'
|
|
257
|
+
ruby_code = Legate::AgentCodeGenerator.generate(agent_definition)
|
|
258
|
+
|
|
259
|
+
content_type 'application/x-ruby'
|
|
260
|
+
attachment "#{name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')}.rb"
|
|
261
|
+
ruby_code
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# POST /agents/:name/save - Persist a (possibly runtime-created) agent to
|
|
265
|
+
# agents/<name>.rb so it survives a restart. Writes generated DSL code (no
|
|
266
|
+
# eval at request time) that the boot loader requires on next start.
|
|
267
|
+
app.post '/agents/:name/save' do |name|
|
|
268
|
+
content_type :json
|
|
269
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
270
|
+
halt 503, json(error: 'Definition Store unavailable.') unless definition_store
|
|
271
|
+
|
|
272
|
+
agent_definition = definition_store.get_definition(name)
|
|
273
|
+
halt 404, json(error: 'Agent not found.') unless agent_definition
|
|
274
|
+
agent_definition[:name] ||= name
|
|
275
|
+
|
|
276
|
+
require 'legate/agent_code_generator'
|
|
277
|
+
require 'fileutils'
|
|
278
|
+
ruby_code = Legate::AgentCodeGenerator.generate(agent_definition)
|
|
279
|
+
safe_name = name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')
|
|
280
|
+
dir = File.join(Dir.pwd, 'agents')
|
|
281
|
+
path = File.join(dir, "#{safe_name}.rb")
|
|
282
|
+
|
|
283
|
+
begin
|
|
284
|
+
FileUtils.mkdir_p(dir)
|
|
285
|
+
File.write(path, ruby_code)
|
|
286
|
+
rescue SystemCallError => e
|
|
287
|
+
logger.error("Failed to save agent '#{name}' to #{path}: #{e.message}")
|
|
288
|
+
halt 500, json(error: "Could not write file (filesystem may be read-only): #{e.message}")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
logger.info("Saved agent '#{name}' to #{path}")
|
|
292
|
+
json(ok: true, path: "agents/#{safe_name}.rb")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# GET /agents/:name - Display the detail page for a specific agent.
|
|
296
|
+
app.get '/agents/:name' do |name|
|
|
297
|
+
logger.info("GET /agents/#{name} route handler entered (from AgentDefinitionRoutes)")
|
|
298
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
299
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
300
|
+
|
|
301
|
+
agent_definition = nil
|
|
302
|
+
begin
|
|
303
|
+
agent_definition = definition_store.get_definition(name)
|
|
304
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
305
|
+
logger.error("Store error fetching definition for '#{name}' (from AgentDefinitionRoutes): #{e.message}")
|
|
306
|
+
halt 500, 'Error retrieving agent definition.'
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
unless agent_definition
|
|
310
|
+
logger.warn("Agent definition not found for '#{name}' in store (from AgentDefinitionRoutes).")
|
|
311
|
+
halt 404,
|
|
312
|
+
slim(:error_404, locals: { title: 'Agent Not Found', message: "Definition for '#{name}' not found." })
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
mcp_display_string = begin
|
|
316
|
+
parsed = JSON.parse(agent_definition[:mcp_servers_json])
|
|
317
|
+
parsed.is_a?(Array) && parsed.empty? ? 'No MCP Server(s) Configured.' : pretty_json(parsed)
|
|
318
|
+
rescue JSON::ParserError
|
|
319
|
+
agent_definition[:mcp_servers_json]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Running state is determined by in-memory @agents hash
|
|
323
|
+
active_agents_hash = instance_variable_get(:@agents)
|
|
324
|
+
is_running = active_agents_hash.key?(name)
|
|
325
|
+
|
|
326
|
+
# Calculate tool count for header display
|
|
327
|
+
tool_count = agent_definition[:tools]&.size || 0
|
|
328
|
+
|
|
329
|
+
instance_variable_set(:@view_agent_data, {
|
|
330
|
+
name: name,
|
|
331
|
+
description: agent_definition[:description],
|
|
332
|
+
running: is_running,
|
|
333
|
+
model: agent_definition[:model],
|
|
334
|
+
fallback_mode: agent_definition[:fallback_mode],
|
|
335
|
+
instruction: agent_definition[:instruction],
|
|
336
|
+
mcp_servers_json: agent_definition[:mcp_servers_json],
|
|
337
|
+
mcp_display_string: mcp_display_string,
|
|
338
|
+
configured_tool_names: agent_definition[:tools],
|
|
339
|
+
tool_count: tool_count,
|
|
340
|
+
# Include agent type and sub-agent names for hierarchy display
|
|
341
|
+
agent_type: agent_definition[:agent_type]&.to_sym || :llm,
|
|
342
|
+
planning_strategy: agent_definition[:planning_strategy]&.to_sym || :plan,
|
|
343
|
+
# For LLM agents, use delegation_targets as 'sub-agents' for display purposes
|
|
344
|
+
sub_agent_names: (agent_definition[:agent_type]&.to_sym == :llm ? agent_definition[:delegation_targets] : agent_definition[:sub_agent_names]) || [],
|
|
345
|
+
# Last run timestamp for display
|
|
346
|
+
last_run_at: agent_definition[:last_run_at],
|
|
347
|
+
# Additional config
|
|
348
|
+
output_key: agent_definition[:output_key],
|
|
349
|
+
loop_max_iterations: agent_definition[:loop_max_iterations],
|
|
350
|
+
loop_condition_state_key: agent_definition[:loop_condition_state_key],
|
|
351
|
+
loop_condition_expected_value: agent_definition[:loop_condition_expected_value]
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
# Tool metadata fetching logic (similar to what's in app.rb for this route)
|
|
355
|
+
all_native_tools_metadata = Legate::GlobalToolManager.list_all_tools.map do |tm|
|
|
356
|
+
params_array = []
|
|
357
|
+
if tm[:parameters].is_a?(Hash) && !tm[:parameters].empty?
|
|
358
|
+
tm[:parameters].each { |pn, d|
|
|
359
|
+
params_array << { name: pn, type: d[:type], description: d[:description], required: d[:required] }
|
|
360
|
+
}
|
|
361
|
+
end
|
|
362
|
+
tm.merge(parameters: params_array, source: :native, source_detail: 'Native')
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
resolved = resolve_available_tools(agent_definition[:mcp_servers_json], all_native_tools_metadata,
|
|
366
|
+
log_context: "GET /agents/#{name}")
|
|
367
|
+
all_available_tools_map = resolved[:map]
|
|
368
|
+
configured_tool_syms = agent_definition[:tools].map(&:to_sym)
|
|
369
|
+
view_tools = configured_tool_syms.map { |ts| all_available_tools_map[ts] }.compact
|
|
370
|
+
|
|
371
|
+
needs_check_job = view_tools.any? { |tm|
|
|
372
|
+
tm[:async] == true || Legate::GlobalToolManager.find_class(tm[:name])&.ancestors&.include?(Legate::Tools::BaseAsyncJobTool)
|
|
373
|
+
}
|
|
374
|
+
if needs_check_job && !view_tools.any? { |t| t[:name] == :check_job_status }
|
|
375
|
+
status_tool_meta = all_available_tools_map[:check_job_status]
|
|
376
|
+
if status_tool_meta
|
|
377
|
+
view_tools << status_tool_meta.dup.merge(
|
|
378
|
+
description: "(Implicitly added) #{status_tool_meta[:description]}", source_detail: 'Native (Implicit)'
|
|
379
|
+
)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
slim :agent, locals: { view_configured_tools: view_tools.sort_by! { |t|
|
|
384
|
+
t[:name].to_s
|
|
385
|
+
}, mcp_tool_results: resolved[:mcp_results] }
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# GET /agents/:name/edit/:field - Show edit form for a specific agent field.
|
|
389
|
+
app.get '/agents/:name/edit/:field' do |name, field|
|
|
390
|
+
supported_fields = %w[description model tools fallback mcp instruction hierarchy type output_key]
|
|
391
|
+
halt 404, "Editing field '#{field}' not supported." unless supported_fields.include?(field)
|
|
392
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
393
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
394
|
+
|
|
395
|
+
agent_definition = definition_store.get_definition(name)
|
|
396
|
+
halt 404, 'Agent definition not found.' unless agent_definition
|
|
397
|
+
|
|
398
|
+
agent_data = {
|
|
399
|
+
name: name, description: agent_definition[:description], model: agent_definition[:model],
|
|
400
|
+
fallback_mode: agent_definition[:fallback_mode],
|
|
401
|
+
mcp_servers_json: agent_definition[:mcp_servers_json],
|
|
402
|
+
instruction: agent_definition[:instruction],
|
|
403
|
+
agent_type: agent_definition[:agent_type]&.to_sym || :llm,
|
|
404
|
+
planning_strategy: agent_definition[:planning_strategy]&.to_sym || :plan,
|
|
405
|
+
output_key: agent_definition[:output_key],
|
|
406
|
+
sub_agent_names: (agent_definition[:agent_type]&.to_sym == :llm ? agent_definition[:delegation_targets] : agent_definition[:sub_agent_names]) || [],
|
|
407
|
+
loop_max_iterations: agent_definition[:loop_max_iterations],
|
|
408
|
+
loop_condition_state_key: agent_definition[:loop_condition_state_key],
|
|
409
|
+
loop_condition_expected_value: agent_definition[:loop_condition_expected_value]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
view_locals = { agent_data: agent_data }
|
|
413
|
+
|
|
414
|
+
if field == 'model'
|
|
415
|
+
view_locals[:available_models] = Legate::Web::App::AVAILABLE_MODELS
|
|
416
|
+
elsif field == 'tools'
|
|
417
|
+
# Ensure configured_tool_names is an array of strings for the view's .include? check
|
|
418
|
+
view_locals[:configured_tool_names] = agent_definition[:tools].map(&:to_s)
|
|
419
|
+
native_tools = Legate::GlobalToolManager.list_all_tools
|
|
420
|
+
|
|
421
|
+
mcp_configs = []
|
|
422
|
+
begin
|
|
423
|
+
mcp_json = agent_definition[:mcp_servers_json]
|
|
424
|
+
mcp_configs = JSON.parse(mcp_json) if mcp_json && !mcp_json.empty? && mcp_json != '[]'
|
|
425
|
+
rescue JSON::ParserError => e
|
|
426
|
+
logger.error("Invalid MCP JSON for agent '#{name}' (edit tools - AgentDefinitionRoutes): #{e.message}")
|
|
427
|
+
end
|
|
428
|
+
mcp_results = fetch_mcp_tools(mcp_configs)
|
|
429
|
+
|
|
430
|
+
fetched_mcp_meta = []
|
|
431
|
+
mcp_results.each do |res|
|
|
432
|
+
next unless res[:status] == :success && res[:tools]
|
|
433
|
+
|
|
434
|
+
res[:tools].each do |schema|
|
|
435
|
+
params = Legate::Mcp::Util::SchemaConverter.json_to_legate(schema.dig(:inputSchema, 'properties') || {},
|
|
436
|
+
schema.dig(:inputSchema, 'required') || [])
|
|
437
|
+
fetched_mcp_meta << { name: schema[:name].to_sym, description: schema[:description] || '',
|
|
438
|
+
parameters: params }
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
view_locals[:all_available_tools] = (native_tools + fetched_mcp_meta).uniq { |t|
|
|
442
|
+
t[:name]
|
|
443
|
+
}.sort_by { |t| t[:name].to_s }
|
|
444
|
+
elsif field == 'hierarchy'
|
|
445
|
+
# Get all available agent definitions for sub-agent selection
|
|
446
|
+
begin
|
|
447
|
+
all_agent_definitions = definition_store.list_definitions
|
|
448
|
+
# Filter out the current agent from available sub-agents to prevent self-reference
|
|
449
|
+
filtered_agent_definitions = all_agent_definitions.reject { |def_data| def_data[:name].to_s == name.to_s }
|
|
450
|
+
logger.info("Agent '#{name}' hierarchy edit view: Filtered out self-reference from #{all_agent_definitions.size} to #{filtered_agent_definitions.size} agents.")
|
|
451
|
+
view_locals[:all_agent_definitions] = filtered_agent_definitions || []
|
|
452
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
453
|
+
logger.error("Store error fetching agent list for hierarchy edit: #{e.message}")
|
|
454
|
+
view_locals[:all_agent_definitions] = []
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
slim :"_edit_agent_#{field}", layout: false, locals: view_locals
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# GET /agents/:name/display/tool_table - Render the tool table display partial.
|
|
461
|
+
# NOTE: This specific route must be defined BEFORE the generic /display/:field route
|
|
462
|
+
app.get '/agents/:name/display/tool_table' do |name|
|
|
463
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
464
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
465
|
+
agent_definition = definition_store.get_definition(name)
|
|
466
|
+
halt 404, 'Agent not found' unless agent_definition
|
|
467
|
+
|
|
468
|
+
agent_data = {
|
|
469
|
+
name: name, description: agent_definition[:description], model: agent_definition[:model],
|
|
470
|
+
fallback_mode: agent_definition[:fallback_mode], mcp_servers_json: agent_definition[:mcp_servers_json],
|
|
471
|
+
running: instance_variable_get(:@agents).key?(name)
|
|
472
|
+
}
|
|
473
|
+
configured_tool_names = agent_definition[:tools]
|
|
474
|
+
configured_tool_syms = configured_tool_names.map(&:to_sym)
|
|
475
|
+
|
|
476
|
+
all_native_tools_metadata = Legate::GlobalToolManager.list_all_tools.map { |tm|
|
|
477
|
+
tm.merge(source: :native, source_detail: 'Native')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
resolved = resolve_available_tools(agent_data[:mcp_servers_json], all_native_tools_metadata,
|
|
481
|
+
log_context: "display_tool_table #{name}")
|
|
482
|
+
all_available_tools_map = resolved[:map]
|
|
483
|
+
mcp_tool_fetch_results = resolved[:mcp_results]
|
|
484
|
+
view_configured_tools_list = configured_tool_syms.map { |ts| all_available_tools_map[ts] }.compact
|
|
485
|
+
|
|
486
|
+
if view_configured_tools_list.any? { |tm|
|
|
487
|
+
Legate::GlobalToolManager.find_class(tm[:name])&.ancestors&.include?(Legate::Tools::BaseAsyncJobTool)
|
|
488
|
+
}
|
|
489
|
+
status_tool_meta = all_available_tools_map[:check_job_status]
|
|
490
|
+
view_configured_tools_list << status_tool_meta if status_tool_meta && !view_configured_tools_list.any? { |t| t[:name] == :check_job_status }
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
slim :_agent_tool_table, layout: false, locals: {
|
|
494
|
+
agent_data: agent_data,
|
|
495
|
+
view_configured_tools: view_configured_tools_list.sort_by { |t| t[:name].to_s },
|
|
496
|
+
mcp_tool_results: mcp_tool_fetch_results
|
|
497
|
+
}
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# GET /agents/:name/display/:field - Display an agent field (after edit cancel).
|
|
501
|
+
app.get '/agents/:name/display/:field' do |name, field|
|
|
502
|
+
# 'tools' is intentionally excluded: there is no _display_agent_tools
|
|
503
|
+
# template; the tools view is the dedicated GET .../display/tool_table
|
|
504
|
+
# route. Listing it here previously produced a 500 for /display/tools.
|
|
505
|
+
supported_fields = %w[description model fallback mcp instruction hierarchy type output_key]
|
|
506
|
+
halt 404, "Displaying field '#{field}' not supported." unless supported_fields.include?(field)
|
|
507
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
508
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
509
|
+
|
|
510
|
+
agent_definition = definition_store.get_definition(name)
|
|
511
|
+
halt 404, 'Agent definition not found.' unless agent_definition
|
|
512
|
+
|
|
513
|
+
response_locals = { show_edit_button: true }
|
|
514
|
+
agent_data_for_display = {
|
|
515
|
+
name: name, description: agent_definition[:description], model: agent_definition[:model],
|
|
516
|
+
fallback_mode: agent_definition[:fallback_mode],
|
|
517
|
+
mcp_servers_json: agent_definition[:mcp_servers_json],
|
|
518
|
+
instruction: agent_definition[:instruction],
|
|
519
|
+
agent_type: agent_definition[:agent_type]&.to_sym || :llm
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if field == 'mcp'
|
|
523
|
+
mcp_json_val = agent_definition[:mcp_servers_json]
|
|
524
|
+
agent_data_for_display[:mcp_display_string] = begin
|
|
525
|
+
parsed = JSON.parse(mcp_json_val)
|
|
526
|
+
parsed.is_a?(Array) && parsed.empty? ? 'No MCP Server(s) Configured.' : pretty_json(parsed)
|
|
527
|
+
rescue JSON::ParserError
|
|
528
|
+
mcp_json_val
|
|
529
|
+
end
|
|
530
|
+
elsif field == 'hierarchy'
|
|
531
|
+
# Add sub_agent_names for hierarchy display
|
|
532
|
+
agent_data_for_display[:sub_agent_names] = agent_definition[:sub_agent_names] || []
|
|
533
|
+
agent_data_for_display[:agent_type] = agent_definition[:agent_type]&.to_sym || :llm
|
|
534
|
+
elsif field == 'type'
|
|
535
|
+
# Add agent_type and planning_strategy for type display
|
|
536
|
+
agent_data_for_display[:agent_type] = agent_definition[:agent_type]&.to_sym || :llm
|
|
537
|
+
agent_data_for_display[:planning_strategy] = agent_definition[:planning_strategy]&.to_sym || :plan
|
|
538
|
+
elsif field == 'output_key'
|
|
539
|
+
# Add output_key for display
|
|
540
|
+
agent_data_for_display[:output_key] = agent_definition[:output_key]
|
|
541
|
+
end
|
|
542
|
+
response_locals[:agent_data] = agent_data_for_display
|
|
543
|
+
|
|
544
|
+
slim :"_display_agent_#{field}", layout: false, locals: response_locals
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# PUT /agents/:name/update/:field - Update a specific field of an agent definition.
|
|
548
|
+
app.put '/agents/:name/update/:field' do |name, field|
|
|
549
|
+
supported_fields = %w[description model tools fallback mcp instruction type hierarchy output_key]
|
|
550
|
+
halt 404, "Updating field '#{field}' not supported." unless supported_fields.include?(field)
|
|
551
|
+
definition_store = instance_variable_get(:@definition_store)
|
|
552
|
+
active_agents_hash = instance_variable_get(:@agents)
|
|
553
|
+
halt 503, 'Definition Store unavailable.' unless definition_store
|
|
554
|
+
|
|
555
|
+
field_to_update_in_store = case field
|
|
556
|
+
when 'fallback' then 'fallback_mode'
|
|
557
|
+
when 'mcp' then 'mcp_servers_json'
|
|
558
|
+
when 'type' then 'agent_type'
|
|
559
|
+
when 'output_key' then 'output_key'
|
|
560
|
+
else field
|
|
561
|
+
end
|
|
562
|
+
new_value_for_store = nil
|
|
563
|
+
agent_data_for_display_partial = { name: name }
|
|
564
|
+
|
|
565
|
+
case field
|
|
566
|
+
when 'output_key'
|
|
567
|
+
new_value_for_store = params['value']&.strip
|
|
568
|
+
new_value_for_store = new_value_for_store.empty? ? nil : new_value_for_store.to_sym
|
|
569
|
+
agent_data_for_display_partial[:output_key] = new_value_for_store
|
|
570
|
+
when 'tools'
|
|
571
|
+
current_definition = definition_store.get_definition(name)
|
|
572
|
+
halt 404, 'Agent not found for tool update.' unless current_definition
|
|
573
|
+
mcp_json = current_definition[:mcp_servers_json]
|
|
574
|
+
native_tool_names = Legate::GlobalToolManager.list_all_tools.map { |t| t[:name].to_s }
|
|
575
|
+
mcp_configs = begin
|
|
576
|
+
JSON.parse(mcp_json)
|
|
577
|
+
rescue StandardError
|
|
578
|
+
[]
|
|
579
|
+
end
|
|
580
|
+
mcp_results = fetch_mcp_tools(mcp_configs)
|
|
581
|
+
mcp_tool_names = mcp_results.flat_map { |res|
|
|
582
|
+
if res[:status] == :success
|
|
583
|
+
res[:tools].map { |t|
|
|
584
|
+
t[:name].to_s
|
|
585
|
+
}
|
|
586
|
+
else
|
|
587
|
+
[]
|
|
588
|
+
end
|
|
589
|
+
}.uniq
|
|
590
|
+
all_valid_tool_names = (native_tool_names + mcp_tool_names).uniq
|
|
591
|
+
|
|
592
|
+
submitted_tools = params['tools'] || []
|
|
593
|
+
new_value_for_store = submitted_tools.select { |st| all_valid_tool_names.include?(st) }
|
|
594
|
+
# For display partial:
|
|
595
|
+
# Rebuild metadata for validated tools
|
|
596
|
+
all_native_meta = Legate::GlobalToolManager.list_all_tools.map do |tm|
|
|
597
|
+
params_array = []
|
|
598
|
+
if tm[:parameters].is_a?(Hash) && !tm[:parameters].empty?
|
|
599
|
+
tm[:parameters].each { |pn, d|
|
|
600
|
+
params_array << { name: pn, type: d[:type], description: d[:description], required: d[:required] }
|
|
601
|
+
}
|
|
602
|
+
end
|
|
603
|
+
tm.merge(parameters: params_array, source: :native, source_detail: 'Native')
|
|
604
|
+
end
|
|
605
|
+
fetched_mcp_meta = []
|
|
606
|
+
mcp_results.each do |res|
|
|
607
|
+
next unless res[:status] == :success && res[:tools]
|
|
608
|
+
|
|
609
|
+
res[:tools].each do |schema|
|
|
610
|
+
params = Legate::Mcp::Util::SchemaConverter.json_to_legate(
|
|
611
|
+
schema.dig(:inputSchema, 'properties') || {}, schema.dig(:inputSchema, 'required') || []
|
|
612
|
+
)
|
|
613
|
+
fetched_mcp_meta << { name: schema[:name].to_sym, description: schema[:description] || '',
|
|
614
|
+
parameters: params, source: :mcp, source_detail: "MCP (#{res[:server]})" }
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
all_available_meta_map = (all_native_meta + fetched_mcp_meta).each_with_object({}) { |tool, map|
|
|
618
|
+
map[tool[:name]] ||= tool
|
|
619
|
+
}
|
|
620
|
+
agent_data_for_display_partial[:view_configured_tools] = new_value_for_store.map { |tn|
|
|
621
|
+
all_available_meta_map[tn.to_sym]
|
|
622
|
+
}.compact
|
|
623
|
+
agent_data_for_display_partial[:mcp_tool_results] = mcp_results # For errors
|
|
624
|
+
when 'mcp'
|
|
625
|
+
submitted_json = params['value']&.strip
|
|
626
|
+
new_value_for_store = submitted_json.nil? || submitted_json.empty? ? '[]' : submitted_json
|
|
627
|
+
begin
|
|
628
|
+
parsed = JSON.parse(new_value_for_store)
|
|
629
|
+
raise JSON::ParserError, 'Input must be a valid JSON array.' unless parsed.is_a?(Array)
|
|
630
|
+
rescue JSON::ParserError => e
|
|
631
|
+
current_def = definition_store.get_definition(name)
|
|
632
|
+
edit_locals = {
|
|
633
|
+
agent_data: { name: name,
|
|
634
|
+
mcp_servers_json: current_def ? current_def[:mcp_servers_json] : new_value_for_store }, error_message: "Invalid JSON: #{e.message}"
|
|
635
|
+
}
|
|
636
|
+
halt 200, slim(:_edit_agent_mcp, layout: false, locals: edit_locals) # Return 200 for HTMX form error display
|
|
637
|
+
end
|
|
638
|
+
agent_data_for_display_partial[:mcp_servers_json] = new_value_for_store
|
|
639
|
+
agent_data_for_display_partial[:mcp_display_string] =
|
|
640
|
+
JSON.parse(new_value_for_store).empty? ? 'No MCP Server(s) Configured.' : pretty_json(JSON.parse(new_value_for_store))
|
|
641
|
+
|
|
642
|
+
when 'fallback'
|
|
643
|
+
submitted_value = params['value']&.strip
|
|
644
|
+
unless %w[error echo].include?(submitted_value)
|
|
645
|
+
current_def = definition_store.get_definition(name)
|
|
646
|
+
edit_locals = {
|
|
647
|
+
agent_data: { name: name,
|
|
648
|
+
fallback_mode: current_def ? current_def[:fallback_mode] : :error }, error_message: 'Invalid fallback.'
|
|
649
|
+
}
|
|
650
|
+
halt 400, slim(:_edit_agent_fallback, layout: false, locals: edit_locals)
|
|
651
|
+
end
|
|
652
|
+
new_value_for_store = submitted_value.to_sym
|
|
653
|
+
agent_data_for_display_partial[:fallback_mode] = new_value_for_store
|
|
654
|
+
when 'type'
|
|
655
|
+
submitted_value = params['agent_type']&.strip
|
|
656
|
+
unless %w[llm sequential parallel loop].include?(submitted_value)
|
|
657
|
+
current_def = definition_store.get_definition(name)
|
|
658
|
+
edit_locals = {
|
|
659
|
+
agent_data: { name: name, agent_type: current_def ? current_def[:agent_type]&.to_sym : :llm },
|
|
660
|
+
error_message: 'Invalid agent type.'
|
|
661
|
+
}
|
|
662
|
+
halt 400, slim(:_edit_agent_type, layout: false, locals: edit_locals)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# Planning strategy is edited alongside the type (it governs how an
|
|
666
|
+
# :llm agent runs). Persist it here; the generic update below handles
|
|
667
|
+
# agent_type itself.
|
|
668
|
+
submitted_strategy = params['planning_strategy']&.strip
|
|
669
|
+
submitted_strategy = 'plan' unless %w[plan react].include?(submitted_strategy)
|
|
670
|
+
definition_store.update_definition(name, planning_strategy: submitted_strategy.to_sym)
|
|
671
|
+
agent_data_for_display_partial[:planning_strategy] = submitted_strategy.to_sym
|
|
672
|
+
|
|
673
|
+
# Check if switching to LLM type and clear sub-agent lists if so
|
|
674
|
+
if submitted_value == 'llm'
|
|
675
|
+
# Get current definition to check current type
|
|
676
|
+
current_def = definition_store.get_definition(name)
|
|
677
|
+
current_type = current_def ? current_def[:agent_type]&.to_s : nil
|
|
678
|
+
|
|
679
|
+
# Only clear sub-agents if switching from a workflow type to LLM
|
|
680
|
+
if current_type && %w[sequential parallel loop].include?(current_type)
|
|
681
|
+
# Update sub-agent fields first
|
|
682
|
+
begin
|
|
683
|
+
definition_store.update_definition(name, {
|
|
684
|
+
sub_agent_names: [],
|
|
685
|
+
sequential_sub_agent_names: [],
|
|
686
|
+
parallel_sub_agent_names: [],
|
|
687
|
+
loop_sub_agent_names: []
|
|
688
|
+
})
|
|
689
|
+
logger.info("Agent '#{name}' switched from '#{current_type}' to 'llm', cleared all sub-agent lists.")
|
|
690
|
+
rescue StandardError => e
|
|
691
|
+
logger.error("Failed to clear sub-agent lists for agent '#{name}': #{e.message}")
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
new_value_for_store = submitted_value
|
|
697
|
+
agent_data_for_display_partial[:agent_type] = submitted_value.to_sym
|
|
698
|
+
when 'instruction', 'description', 'model'
|
|
699
|
+
new_value_for_store = params['value']&.strip || (field == 'instruction' ? '' : nil)
|
|
700
|
+
if new_value_for_store.nil? && field != 'instruction' # Description and model cannot be nil (empty is ok for description)
|
|
701
|
+
current_def = definition_store.get_definition(name)
|
|
702
|
+
edit_locals = {
|
|
703
|
+
agent_data: { name: name, description: current_def[:description], model: current_def[:model],
|
|
704
|
+
instruction: current_def[:instruction] }, error_message: "#{field.capitalize} cannot be empty."
|
|
705
|
+
}
|
|
706
|
+
halt 400, slim(:"_edit_agent_#{field}", layout: false, locals: edit_locals)
|
|
707
|
+
end
|
|
708
|
+
agent_data_for_display_partial[field.to_sym] = new_value_for_store
|
|
709
|
+
when 'hierarchy'
|
|
710
|
+
# Get selected sub-agent names from the form
|
|
711
|
+
sub_agent_names = params['sub_agent_names'] || []
|
|
712
|
+
|
|
713
|
+
# Update the definition via a separate field
|
|
714
|
+
begin
|
|
715
|
+
update_success = definition_store.update_definition(name, sub_agent_names: sub_agent_names)
|
|
716
|
+
halt 404, 'Agent not found for update.' unless update_success
|
|
717
|
+
logger.info("Agent '#{name}' hierarchy updated with #{sub_agent_names.size} sub-agents (from AgentDefinitionRoutes)")
|
|
718
|
+
|
|
719
|
+
# Refresh agent data for display
|
|
720
|
+
updated_definition = definition_store.get_definition(name)
|
|
721
|
+
agent_data = {
|
|
722
|
+
name: name,
|
|
723
|
+
description: updated_definition[:description],
|
|
724
|
+
agent_type: updated_definition[:agent_type]&.to_sym || :llm,
|
|
725
|
+
sub_agent_names: updated_definition[:sub_agent_names] || [],
|
|
726
|
+
show_edit_button: true
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
# Return the updated display partial directly
|
|
730
|
+
return slim :_display_agent_hierarchy, layout: false, locals: { agent_data: agent_data }
|
|
731
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
732
|
+
logger.error("Store error updating agent hierarchy: #{e.message}")
|
|
733
|
+
halt 500, 'Error updating agent hierarchy.'
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
begin
|
|
738
|
+
update_success = definition_store.update_definition(name,
|
|
739
|
+
{ field_to_update_in_store.to_sym => new_value_for_store })
|
|
740
|
+
halt 404, 'Agent not found for update.' unless update_success
|
|
741
|
+
logger.info("Agent '#{name}' field '#{field_to_update_in_store}' updated (from AgentDefinitionRoutes).")
|
|
742
|
+
|
|
743
|
+
was_running = active_agents_hash.key?(name)
|
|
744
|
+
if was_running
|
|
745
|
+
logger.info("Agent '#{name}' config updated while running. Triggering auto-restart (from AgentDefinitionRoutes).")
|
|
746
|
+
send(:_stop_agent, name)
|
|
747
|
+
newly_started_agent = send(:_start_agent, name)
|
|
748
|
+
agent_data_for_display_partial[:running] = !newly_started_agent.nil?
|
|
749
|
+
headers 'HX-Trigger-After-Swap' => (agent_data_for_display_partial[:running] ? 'showRestartToast' : 'showRestartErrorToast')
|
|
750
|
+
else
|
|
751
|
+
agent_data_for_display_partial[:running] = false
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Re-fetch full definition for display consistency
|
|
755
|
+
full_updated_def = definition_store.get_definition(name)
|
|
756
|
+
agent_data_for_display_partial.merge!(
|
|
757
|
+
description: full_updated_def[:description], model: full_updated_def[:model],
|
|
758
|
+
fallback_mode: full_updated_def[:fallback_mode], mcp_servers_json: full_updated_def[:mcp_servers_json],
|
|
759
|
+
instruction: full_updated_def[:instruction]
|
|
760
|
+
)
|
|
761
|
+
# Ensure mcp_display_string is set if field was 'mcp'
|
|
762
|
+
if field == 'mcp'
|
|
763
|
+
agent_data_for_display_partial[:mcp_display_string] ||= JSON.parse(new_value_for_store).empty? ? 'No MCP Server(s) Configured.' : pretty_json(JSON.parse(new_value_for_store))
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
response_locals_for_display = { agent_data: agent_data_for_display_partial, show_edit_button: true }
|
|
767
|
+
|
|
768
|
+
if field == 'tools'
|
|
769
|
+
# For tools, the _agent_tool_table partial is rendered
|
|
770
|
+
# It expects :view_configured_tools and :mcp_tool_results
|
|
771
|
+
# We already prepared agent_data_for_display_partial[:view_configured_tools]
|
|
772
|
+
# and agent_data_for_display_partial[:mcp_tool_results]
|
|
773
|
+
slim :_agent_tool_table, layout: false, locals: agent_data_for_display_partial # Pass the whole hash
|
|
774
|
+
else
|
|
775
|
+
response_html = slim :"_display_agent_#{field}", layout: false, locals: response_locals_for_display
|
|
776
|
+
|
|
777
|
+
# Add OOB update for hierarchy section if changing to LLM type
|
|
778
|
+
if field == 'type' && new_value_for_store == 'llm'
|
|
779
|
+
# Add an out-of-band swap to update the hierarchy section with empty sub-agents
|
|
780
|
+
empty_hierarchy_data = {
|
|
781
|
+
name: name,
|
|
782
|
+
agent_type: :llm,
|
|
783
|
+
sub_agent_names: [],
|
|
784
|
+
show_edit_button: true
|
|
785
|
+
}
|
|
786
|
+
response_html += '<div id="agent-hierarchy-display" hx-swap-oob="true">' +
|
|
787
|
+
slim(:_display_agent_hierarchy, layout: false, locals: { agent_data: empty_hierarchy_data }) +
|
|
788
|
+
'</div>'
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
response_html
|
|
792
|
+
end
|
|
793
|
+
rescue Legate::DefinitionStore::StoreError => e
|
|
794
|
+
logger.error("Store error updating agent '#{name}' (from AgentDefinitionRoutes): #{e.message}")
|
|
795
|
+
halt 500, 'Error updating agent definition.'
|
|
796
|
+
rescue ArgumentError => e # From store validation
|
|
797
|
+
halt 400, "Invalid input: #{e.message}"
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
end
|