From c05dda37e292d845ddeb334746b5364fe989d27a Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 2 Apr 2025 12:00:32 +0200 Subject: [PATCH] [workchat] reintegrate into `main` (#215627) ## Summary ~**DO NOT MERGE:** depends on https://github.com/elastic/kibana/issues/213468~ This PR reintegrates the work from the `workchat_m1` branch into `main`: - introduces a 4th solution type, `chat`, that will be used for the *WorkChat* project type. - edit things in various platform code to introduce/handle that new project type - add plugins and packages for the workchat app. ### To AppEx reviewers: File change count is scary, but you can safely ignore anything from `xpack/solutions/chat` (given it's solution code), and focus on your owned changes, which are way more reasonable --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe McElroy Co-authored-by: Rodney Norris Co-authored-by: Jedr Blaszyk Co-authored-by: Elastic Machine Co-authored-by: Meghan Murphy --- .github/CODEOWNERS | 11 + config/serverless.chat.yml | 12 +- docs/extend/plugin-list.md | 4 + package.json | 17 +- .../current_fields.json | 32 ++ .../current_mappings.json | 101 +++++ packages/kbn-optimizer/limits.yml | 4 + renovate.json | 22 + src/cli/serve/serve.js | 2 +- .../common/src/default_app_categories.ts | 9 +- .../chrome/browser/src/project_navigation.ts | 9 +- .../packages/chrome/browser/tsconfig.json | 3 +- .../src/import_resolver.ts | 5 + .../packages/shared/deeplinks/chat/README.md | 3 + .../shared/deeplinks/chat/constants.ts | 10 + .../shared/deeplinks/chat/deep_links.ts | 16 + .../packages/shared/deeplinks/chat/index.ts | 11 + .../shared/deeplinks/chat/jest.config.js | 14 + .../shared/deeplinks/chat/kibana.jsonc | 7 + .../shared/deeplinks/chat/package.json | 6 + .../shared/deeplinks/chat/tsconfig.json | 17 + .../src/ui/components/feedback_btn.tsx | 1 + .../shared/navigation/common/constants.ts | 1 + tsconfig.base.json | 22 + .../shared/features/server/config.test.ts | 2 +- .../solution_view_tour/solution_view_tour.tsx | 3 + .../public/space_solution_badge/badge.tsx | 4 + .../space_solution_disabled_features.test.ts | 29 +- .../utils/space_solution_disabled_features.ts | 18 +- .../chat/packages/wc-genai-utils/README.md | 3 + .../chat/packages/wc-genai-utils/index.ts | 8 + .../packages/wc-genai-utils/jest.config.js | 12 + .../chat/packages/wc-genai-utils/kibana.jsonc | 7 + .../chat/packages/wc-genai-utils/package.json | 6 + .../src/connectors/get_connector_list.ts | 32 ++ .../src/connectors/get_default_connector.ts | 32 ++ .../wc-genai-utils/src/connectors/index.ts | 9 + .../packages/wc-genai-utils/tsconfig.json | 21 + .../wc-index-schema-builder/README.md | 3 + .../packages/wc-index-schema-builder/index.ts | 8 + .../wc-index-schema-builder}/jest.config.js | 7 +- .../wc-index-schema-builder/kibana.jsonc | 7 + .../wc-index-schema-builder/package.json | 6 + .../src/build_schema.ts | 29 ++ .../src/utils/get_index_information.ts | 29 ++ .../src/utils/get_leaf_fields.test.ts | 111 +++++ .../src/utils/get_leaf_fields.ts | 49 +++ .../src/utils/get_sample_documents.ts | 29 ++ .../src/utils/index.ts | 10 + .../src/workflows/build_index_schema.ts | 286 +++++++++++++ .../src/workflows/prompts.ts | 319 ++++++++++++++ .../wc-index-schema-builder/tsconfig.json | 23 ++ .../packages/wc-integration-utils/README.md | 3 + .../packages/wc-integration-utils/index.ts | 14 + .../wc-integration-utils/jest.config.js | 12 + .../wc-integration-utils/kibana.jsonc | 7 + .../wc-integration-utils/package.json | 6 + .../get_field_type_by_path.test.ts | 62 +++ .../elasticsearch/get_field_type_by_path.ts | 48 +++ .../elasticsearch/get_fields_top_values.ts | 55 +++ .../src/elasticsearch/index.ts | 9 + .../src/tools/create_filter_clauses.test.ts | 106 +++++ .../src/tools/create_filter_clauses.ts | 33 ++ .../src/tools/generate_search_schema.test.ts | 145 +++++++ .../src/tools/generate_search_schema.ts | 63 +++ .../src/tools/hit_to_content.test.ts | 98 +++++ .../src/tools/hit_to_content.ts | 29 ++ .../wc-integration-utils/src/tools/index.ts | 10 + .../wc-integration-utils/tsconfig.json | 20 + .../chat/packages/wci-browser/README.md | 3 + .../chat/packages/wci-browser/index.ts | 12 + .../chat/packages/wci-browser/jest.config.js | 12 + .../chat/packages/wci-browser/kibana.jsonc | 7 + .../chat/packages/wci-browser/package.json | 7 + .../src/integration_ui_descriptor.ts | 40 ++ .../chat/packages/wci-browser/tsconfig.json | 21 + .../chat/packages/wci-common/README.md | 3 + .../chat/packages/wci-common/index.ts | 20 + .../chat/packages/wci-common/jest.config.js | 12 + .../chat/packages/wci-common/kibana.jsonc | 9 + .../chat/packages/wci-common/package.json | 7 + .../chat/packages/wci-common/src/constants.ts | 12 + .../packages/wci-common/src/index_source.ts | 87 ++++ .../wci-common/src/integration_tools.test.ts | 50 +++ .../wci-common/src/integration_tools.ts | 32 ++ .../packages/wci-common/src/integrations.ts | 21 + .../packages/wci-common/src/tool_calls.ts | 24 ++ .../chat/packages/wci-common/tsconfig.json | 20 + .../chat/packages/wci-server/README.md | 3 + .../chat/packages/wci-server/index.ts | 24 ++ .../chat/packages/wci-server/jest.config.js | 12 + .../chat/packages/wci-server/kibana.jsonc | 9 + .../chat/packages/wci-server/package.json | 7 + .../packages/wci-server/src/integration.ts | 46 +++ .../chat/packages/wci-server/src/mcp.ts | 65 +++ .../src/utils/create_external_client.test.ts | 112 +++++ .../src/utils/create_external_client.ts | 53 +++ .../src/utils/create_internal_client.test.ts | 107 +++++ .../src/utils/create_internal_client.ts | 50 +++ .../wci-server/src/utils/create_mcp_server.ts | 32 ++ .../packages/wci-server/src/utils/index.ts | 10 + .../chat/packages/wci-server/tsconfig.json | 24 ++ .../serverless_chat/jest.config.dev.js | 5 +- .../serverless_chat/public/jest.config.js | 30 -- .../serverless_chat/public/navigation_tree.ts | 137 +++++++ .../plugins/serverless_chat/public/plugin.ts | 8 +- .../plugins/serverless_chat/tsconfig.json | 2 + .../plugins/wci-external-server/README.md | 3 + .../wci-external-server/common/index.ts | 9 + .../wci-external-server/common/types.ts | 13 + .../wci-external-server/jest.config.js | 13 + .../plugins/wci-external-server/kibana.jsonc | 17 + .../plugins/wci-external-server/package.json | 11 + .../wci-external-server/public/index.ts | 25 ++ .../public/integration/configuration.tsx | 61 +++ .../external_server_integration.ts | 19 + .../public/integration/tool.tsx | 65 +++ .../wci-external-server/public/plugin.tsx | 45 ++ .../wci-external-server/public/types.ts | 21 + .../wci-external-server/server/config.ts | 20 + .../wci-external-server/server/index.ts | 17 + .../external_server_integration.ts | 34 ++ .../server/integration/index.ts | 8 + .../wci-external-server/server/plugin.ts | 58 +++ .../wci-external-server/server/types.ts | 20 + .../plugins/wci-external-server/tsconfig.json | 26 ++ .../chat/plugins/wci-index-source/README.md | 3 + .../common/http_api/configuration.ts | 12 + .../plugins/wci-index-source/common/index.ts | 9 + .../plugins/wci-index-source/common/types.ts | 31 ++ .../plugins/wci-index-source/jest.config.js | 13 + .../plugins/wci-index-source/kibana.jsonc | 17 + .../plugins/wci-index-source/package.json | 11 + .../public/hooks/use_generate_schema.ts | 36 ++ .../plugins/wci-index-source/public/index.ts | 25 ++ .../public/integration/configuration.tsx | 334 +++++++++++++++ .../integration/index_source_integration.ts | 19 + .../public/integration/tool.tsx | 66 +++ .../wci-index-source/public/plugin.tsx | 45 ++ .../plugins/wci-index-source/public/types.ts | 21 + .../plugins/wci-index-source/server/config.ts | 20 + .../plugins/wci-index-source/server/index.ts | 17 + .../server/integration/index.ts | 8 + .../integration/index_source_integration.ts | 47 +++ .../server/integration/mcp_server.ts | 130 ++++++ .../plugins/wci-index-source/server/plugin.ts | 66 +++ .../server/routes/configuration.ts | 64 +++ .../wci-index-source/server/routes/index.ts | 13 + .../wci-index-source/server/routes/types.ts | 15 + .../plugins/wci-index-source/server/types.ts | 24 ++ .../plugins/wci-index-source/tsconfig.json | 32 ++ .../chat/plugins/wci-salesforce/README.md | 3 + .../plugins/wci-salesforce/common/index.ts | 9 + .../plugins/wci-salesforce/common/types.ts | 10 + .../plugins/wci-salesforce/jest.config.js | 13 + .../chat/plugins/wci-salesforce/kibana.jsonc | 17 + .../chat/plugins/wci-salesforce/package.json | 11 + .../plugins/wci-salesforce/public/index.ts | 25 ++ .../public/integration/configuration.tsx | 39 ++ .../integration/salesforce_integration.ts | 19 + .../public/integration/tool.tsx | 65 +++ .../plugins/wci-salesforce/public/plugin.tsx | 44 ++ .../plugins/wci-salesforce/public/types.ts | 21 + .../plugins/wci-salesforce/server/config.ts | 20 + .../plugins/wci-salesforce/server/index.ts | 17 + .../server/integration/index.ts | 8 + .../server/integration/mcp_server.ts | 243 +++++++++++ .../integration/salesforce_integration.ts | 38 ++ .../server/integration/tools.ts | 161 ++++++++ .../server/integration/types.ts | 40 ++ .../plugins/wci-salesforce/server/plugin.ts | 58 +++ .../plugins/wci-salesforce/server/types.ts | 25 ++ .../chat/plugins/wci-salesforce/tsconfig.json | 27 ++ .../chat/plugins/workchat-app/README.md | 3 + .../plugins/workchat-app/common/agents.ts | 28 ++ .../workchat-app/common/chat_events.test.ts | 236 +++++++++++ .../workchat-app/common/chat_events.ts | 165 ++++++++ .../plugins/workchat-app/common/constants.ts | 8 + .../common/conversation_events.test.ts | 196 +++++++++ .../common/conversation_events.ts | 128 ++++++ .../workchat-app/common/conversations.ts | 32 ++ .../plugins/workchat-app/common/errors.ts | 40 ++ .../plugins/workchat-app/common/features.ts | 32 ++ .../workchat-app/common/http_api/agents.ts | 22 + .../common/http_api/connectors.ts | 12 + .../common/http_api/conversation.ts | 21 + .../common/http_api/integrations.ts | 38 ++ .../workchat-app/common/integrations.ts | 17 + .../plugins/workchat-app/common/shared.ts | 13 + .../chat/plugins/workchat-app/jest.config.js | 13 + .../chat/plugins/workchat-app/kibana.jsonc | 17 + .../chat/plugins/workchat-app/package.json | 11 + .../public/application/app_paths.ts | 31 ++ .../agents/edition/agent_edit_view.tsx | 174 ++++++++ .../application/components/agents/i18n.ts | 50 +++ .../agents/listing/agent_list_view.tsx | 63 +++ .../application/components/chat/chat.tsx | 93 +++++ .../components/chat/chat_conversation.tsx | 44 ++ .../chat/chat_conversation_item.tsx | 35 ++ .../chat/chat_conversation_message.tsx | 69 ++++ .../chat/chat_conversation_tool_call.tsx | 37 ++ .../chat/chat_default_tool_call.tsx | 66 +++ .../components/chat/chat_header.tsx | 46 +++ .../chat/chat_header_connector_selector.tsx | 77 ++++ .../components/chat/chat_input_form.tsx | 81 ++++ .../components/chat/chat_message_avatar.tsx | 33 ++ .../components/chat/chat_message_text.tsx | 187 +++++++++ .../application/components/chat/chat_view.tsx | 103 +++++ .../components/chat/conversation_list.tsx | 150 +++++++ .../components/home/home_agent_section.tsx | 70 ++++ .../home/home_integration_section.tsx | 68 +++ .../application/components/home/home_view.tsx | 22 + .../edit/integration_edit_view.tsx | 270 ++++++++++++ .../components/integrations/i18n.ts | 56 +++ .../listing/integration_list_view.tsx | 68 +++ .../components/integrations/utils.ts | 21 + .../context/workchat_services_context.tsx | 11 + .../public/application/hooks/use_agent.ts | 26 ++ .../application/hooks/use_agent_edition.ts | 90 ++++ .../application/hooks/use_agent_list.ts | 32 ++ .../application/hooks/use_breadcrumbs.ts | 59 +++ .../application/hooks/use_capabilities.ts | 30 ++ .../public/application/hooks/use_chat.ts | 157 +++++++ .../application/hooks/use_connectors.ts | 31 ++ .../application/hooks/use_conversation.ts | 57 +++ .../hooks/use_conversation_list.ts | 32 ++ .../application/hooks/use_current_user.ts | 25 ++ .../use_integration_configuration_form.ts | 28 ++ .../hooks/use_integration_delete.ts | 46 +++ .../application/hooks/use_integration_edit.ts | 90 ++++ .../application/hooks/use_integration_list.ts | 32 ++ .../hooks/use_integration_tool_view.tsx | 48 +++ .../public/application/hooks/use_kibana.ts | 18 + .../application/hooks/use_navigation.ts | 35 ++ .../application/hooks/use_stick_to_bottom.ts | 54 +++ .../application/hooks/use_workchat_service.ts | 19 + .../workchat-app/public/application/index.ts | 8 + .../workchat-app/public/application/mount.tsx | 57 +++ .../pages/agent_edit_or_create.tsx | 24 ++ .../public/application/pages/agents.tsx | 17 + .../public/application/pages/chat.tsx | 31 ++ .../public/application/pages/home.tsx | 15 + .../pages/integration_edit_or_create.tsx | 24 ++ .../public/application/pages/integrations.tsx | 17 + .../public/application/query_keys.ts | 32 ++ .../public/application/register.ts | 40 ++ .../public/application/routes.tsx | 51 +++ .../application/utils/conversation_items.ts | 100 +++++ .../utils/get_chart_conversation_items.ts | 82 ++++ .../application/utils/has_capability.ts | 17 + .../utils/sort_and_group_conversations.ts | 105 +++++ .../chat/plugins/workchat-app/public/index.ts | 25 ++ .../plugins/workchat-app/public/plugin.tsx | 85 ++++ .../public/services/agent/agent_service.ts | 45 ++ .../public/services/chat/chat_service.ts | 49 +++ .../conversation/conversation_service.ts | 37 ++ .../workchat-app/public/services/index.ts | 11 + .../integration/integration_registry.ts | 45 ++ .../integration/integration_service.ts | 53 +++ .../public/services/integration/types.ts | 6 + .../workchat-app/public/services/types.ts | 19 + .../chat/plugins/workchat-app/public/types.ts | 25 ++ .../plugins/workchat-app/server/config.ts | 20 + .../plugins/workchat-app/server/errors.ts | 16 + .../plugins/workchat-app/server/features.ts | 52 +++ .../chat/plugins/workchat-app/server/index.ts | 17 + .../plugins/workchat-app/server/plugin.ts | 88 ++++ .../workchat-app/server/routes/agents.ts | 157 +++++++ .../workchat-app/server/routes/chat.ts | 73 ++++ .../workchat-app/server/routes/connectors.ts | 41 ++ .../server/routes/conversation.ts | 92 +++++ .../workchat-app/server/routes/index.ts | 21 + .../server/routes/integrations.ts | 187 +++++++++ .../workchat-app/server/routes/types.ts | 17 + .../server/routes/wrap_handler.ts | 29 ++ .../server/saved_objects/agents.ts | 47 +++ .../server/saved_objects/conversations.ts | 54 +++ .../server/saved_objects/index.ts | 21 + .../server/saved_objects/integrations.ts | 41 ++ .../server/services/agents/agent_client.ts | 133 ++++++ .../server/services/agents/agent_service.ts | 56 +++ .../server/services/agents/convert_model.ts | 64 +++ .../server/services/agents/index.ts | 9 + .../server/services/agents/mocks.ts | 31 ++ .../server/services/chat/chat_service.ts | 317 ++++++++++++++ .../chat/generate_conversation_title.ts | 42 ++ .../server/services/chat/index.ts | 8 + .../conversations/conversation_client.ts | 134 ++++++ .../conversations/conversation_service.ts | 56 +++ .../services/conversations/convert_model.ts | 64 +++ .../server/services/conversations/index.ts | 9 + .../server/services/conversations/mocks.ts | 31 ++ .../server/services/conversations/types.ts | 11 + .../server/services/create_services.ts | 75 ++++ .../workchat-app/server/services/index.ts | 13 + .../services/integrations/convert_model.ts | 77 ++++ .../server/services/integrations/index.ts | 10 + .../integrations/integration_client.ts | 117 ++++++ .../integrations/integration_registry.ts | 45 ++ .../integrations/integrations_service.ts | 128 ++++++ .../server/services/integrations/mocks.ts | 33 ++ .../server/services/integrations/types.ts | 12 + .../services/orchestration/agent_factory.ts | 82 ++++ .../services/orchestration/agent_graph.ts | 85 ++++ .../services/orchestration/agent_runner.ts | 84 ++++ .../base_tools/base_tools_provider.ts | 31 ++ .../orchestration/base_tools/calculator.ts | 34 ++ .../orchestration/base_tools/index.ts | 8 + .../server/services/orchestration/index.ts | 9 + .../orchestration/mcp_gateway/index.ts | 8 + .../orchestration/mcp_gateway/session.test.ts | 101 +++++ .../orchestration/mcp_gateway/session.ts | 90 ++++ .../orchestration/mcp_gateway/types.ts | 23 ++ .../orchestration/mcp_gateway/utils/index.ts | 9 + .../mcp_gateway/utils/list_clients_tools.ts | 44 ++ .../mcp_gateway/utils/to_langchain_tool.ts | 46 +++ .../server/services/orchestration/prompts.ts | 38 ++ .../server/services/orchestration/types.ts | 39 ++ .../utils/convert_langchain_events.test.ts | 160 ++++++++ .../utils/convert_langchain_events.ts | 69 ++++ .../utils/events_to_messages.test.ts | 104 +++++ .../orchestration/utils/events_to_messages.ts | 58 +++ .../utils/from_langchain_messages.test.ts | 131 ++++++ .../utils/from_langchain_messages.ts | 58 +++ .../services/orchestration/utils/index.ts | 9 + .../workchat-app/server/services/types.ts | 20 + .../chat/plugins/workchat-app/server/types.ts | 28 ++ .../workchat-app/server/utils/index.ts | 8 + .../server/utils/so_filters.test.ts | 47 +++ .../workchat-app/server/utils/so_filters.ts | 50 +++ .../chat/plugins/workchat-app/tsconfig.json | 46 +++ x-pack/test/common/services/spaces.ts | 2 +- yarn.lock | 388 ++++++++++++++++-- 333 files changed, 14922 insertions(+), 93 deletions(-) create mode 100644 src/platform/packages/shared/deeplinks/chat/README.md create mode 100644 src/platform/packages/shared/deeplinks/chat/constants.ts create mode 100644 src/platform/packages/shared/deeplinks/chat/deep_links.ts create mode 100644 src/platform/packages/shared/deeplinks/chat/index.ts create mode 100644 src/platform/packages/shared/deeplinks/chat/jest.config.js create mode 100644 src/platform/packages/shared/deeplinks/chat/kibana.jsonc create mode 100644 src/platform/packages/shared/deeplinks/chat/package.json create mode 100644 src/platform/packages/shared/deeplinks/chat/tsconfig.json create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/README.md create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/index.ts create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/jest.config.js create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/kibana.jsonc create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/package.json create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_connector_list.ts create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_default_connector.ts create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/index.ts create mode 100644 x-pack/solutions/chat/packages/wc-genai-utils/tsconfig.json create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/README.md create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/index.ts rename x-pack/solutions/chat/{plugins/serverless_chat/server => packages/wc-index-schema-builder}/jest.config.js (71%) create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/kibana.jsonc create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/package.json create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/build_schema.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_index_information.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.test.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_sample_documents.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/index.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/build_index_schema.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/prompts.ts create mode 100644 x-pack/solutions/chat/packages/wc-index-schema-builder/tsconfig.json create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/README.md create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/index.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/jest.config.js create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/kibana.jsonc create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/package.json create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.test.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_fields_top_values.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/index.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.test.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.test.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.test.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/src/tools/index.ts create mode 100644 x-pack/solutions/chat/packages/wc-integration-utils/tsconfig.json create mode 100644 x-pack/solutions/chat/packages/wci-browser/README.md create mode 100644 x-pack/solutions/chat/packages/wci-browser/index.ts create mode 100644 x-pack/solutions/chat/packages/wci-browser/jest.config.js create mode 100644 x-pack/solutions/chat/packages/wci-browser/kibana.jsonc create mode 100644 x-pack/solutions/chat/packages/wci-browser/package.json create mode 100644 x-pack/solutions/chat/packages/wci-browser/src/integration_ui_descriptor.ts create mode 100644 x-pack/solutions/chat/packages/wci-browser/tsconfig.json create mode 100644 x-pack/solutions/chat/packages/wci-common/README.md create mode 100644 x-pack/solutions/chat/packages/wci-common/index.ts create mode 100644 x-pack/solutions/chat/packages/wci-common/jest.config.js create mode 100644 x-pack/solutions/chat/packages/wci-common/kibana.jsonc create mode 100644 x-pack/solutions/chat/packages/wci-common/package.json create mode 100644 x-pack/solutions/chat/packages/wci-common/src/constants.ts create mode 100644 x-pack/solutions/chat/packages/wci-common/src/index_source.ts create mode 100644 x-pack/solutions/chat/packages/wci-common/src/integration_tools.test.ts create mode 100644 x-pack/solutions/chat/packages/wci-common/src/integration_tools.ts create mode 100644 x-pack/solutions/chat/packages/wci-common/src/integrations.ts create mode 100644 x-pack/solutions/chat/packages/wci-common/src/tool_calls.ts create mode 100644 x-pack/solutions/chat/packages/wci-common/tsconfig.json create mode 100644 x-pack/solutions/chat/packages/wci-server/README.md create mode 100644 x-pack/solutions/chat/packages/wci-server/index.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/jest.config.js create mode 100644 x-pack/solutions/chat/packages/wci-server/kibana.jsonc create mode 100644 x-pack/solutions/chat/packages/wci-server/package.json create mode 100644 x-pack/solutions/chat/packages/wci-server/src/integration.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/src/mcp.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.test.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.test.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/src/utils/create_mcp_server.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/src/utils/index.ts create mode 100644 x-pack/solutions/chat/packages/wci-server/tsconfig.json delete mode 100644 x-pack/solutions/chat/plugins/serverless_chat/public/jest.config.js create mode 100644 x-pack/solutions/chat/plugins/serverless_chat/public/navigation_tree.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/README.md create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/common/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/common/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/jest.config.js create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/kibana.jsonc create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/package.json create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/public/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/public/integration/configuration.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/public/integration/external_server_integration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/public/integration/tool.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/public/plugin.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/public/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/server/config.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/server/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/server/integration/external_server_integration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/server/integration/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/server/plugin.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/server/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-external-server/tsconfig.json create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/README.md create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/common/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/common/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/jest.config.js create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/kibana.jsonc create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/package.json create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/public/hooks/use_generate_schema.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/public/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/public/integration/index_source_integration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/public/integration/tool.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/public/plugin.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/public/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/config.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/integration/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/integration/index_source_integration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/integration/mcp_server.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/plugin.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/routes/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/routes/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/server/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-index-source/tsconfig.json create mode 100755 x-pack/solutions/chat/plugins/wci-salesforce/README.md create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/common/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/common/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/jest.config.js create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/kibana.jsonc create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/package.json create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/public/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/public/integration/configuration.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/public/integration/salesforce_integration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/public/integration/tool.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/public/plugin.tsx create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/public/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/config.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/integration/index.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/integration/mcp_server.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/integration/salesforce_integration.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/integration/tools.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/integration/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/plugin.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/server/types.ts create mode 100644 x-pack/solutions/chat/plugins/wci-salesforce/tsconfig.json create mode 100755 x-pack/solutions/chat/plugins/workchat-app/README.md create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/agents.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/chat_events.test.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/chat_events.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/constants.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.test.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/conversations.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/errors.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/features.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/http_api/agents.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/http_api/connectors.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/http_api/conversation.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/http_api/integrations.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/integrations.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/common/shared.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/jest.config.js create mode 100644 x-pack/solutions/chat/plugins/workchat-app/kibana.jsonc create mode 100644 x-pack/solutions/chat/plugins/workchat-app/package.json create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/app_paths.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/edition/agent_edit_view.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/i18n.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/listing/agent_list_view.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_item.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_message.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_tool_call.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_default_tool_call.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header_connector_selector.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_input_form.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_avatar.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_text.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_view.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/conversation_list.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_agent_section.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_integration_section.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_view.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/edit/integration_edit_view.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/i18n.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/listing/integration_list_view.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/utils.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/context/workchat_services_context.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_edition.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_list.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_breadcrumbs.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_capabilities.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_chat.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_connectors.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation_list.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_current_user.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_configuration_form.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_delete.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_edit.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_list.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_tool_view.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_kibana.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_navigation.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_stick_to_bottom.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_workchat_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/mount.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agent_edit_or_create.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agents.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/pages/chat.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/pages/home.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integration_edit_or_create.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integrations.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/query_keys.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/register.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/routes.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/utils/conversation_items.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/utils/get_chart_conversation_items.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/utils/has_capability.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/application/utils/sort_and_group_conversations.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/plugin.tsx create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/agent/agent_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/chat/chat_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/conversation/conversation_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_registry.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/integration/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/services/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/public/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/config.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/errors.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/features.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/plugin.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/agents.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/chat.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/connectors.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/conversation.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/integrations.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/routes/wrap_handler.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/agents.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/conversations.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/integrations.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_client.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/agents/convert_model.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/agents/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/agents/mocks.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/chat/chat_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/chat/generate_conversation_title.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/chat/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_client.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/convert_model.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/mocks.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/create_services.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/convert_model.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_client.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_registry.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integrations_service.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/mocks.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_factory.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_graph.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_runner.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/base_tools_provider.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/calculator.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.test.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/list_clients_tools.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/to_langchain_tool.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/prompts.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.test.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.test.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.test.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/services/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/types.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/utils/index.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.test.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.ts create mode 100644 x-pack/solutions/chat/plugins/workchat-app/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aafcd7dfbf48..3a90aecf7ac3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -390,6 +390,7 @@ src/platform/packages/shared/content-management/table_list_view_common @elastic/ src/platform/packages/shared/content-management/table_list_view_table @elastic/appex-sharedux src/platform/packages/shared/content-management/user_profiles @elastic/appex-sharedux src/platform/packages/shared/deeplinks/analytics @elastic/kibana-data-discovery @elastic/kibana-presentation @elastic/kibana-visualizations +src/platform/packages/shared/deeplinks/chat @elastic/search-kibana src/platform/packages/shared/deeplinks/devtools @elastic/kibana-management src/platform/packages/shared/deeplinks/fleet @elastic/fleet src/platform/packages/shared/deeplinks/management @elastic/kibana-management @@ -937,7 +938,17 @@ x-pack/platform/plugins/shared/streams_app @elastic/streams-program-team x-pack/platform/plugins/shared/task_manager @elastic/response-ops x-pack/platform/plugins/shared/timelines @elastic/security-threat-hunting-investigations x-pack/platform/plugins/shared/triggers_actions_ui @elastic/response-ops +x-pack/solutions/chat/packages/wc-genai-utils @elastic/search-kibana +x-pack/solutions/chat/packages/wc-index-schema-builder @elastic/search-kibana +x-pack/solutions/chat/packages/wc-integration-utils @elastic/search-kibana +x-pack/solutions/chat/packages/wci-browser @elastic/search-kibana +x-pack/solutions/chat/packages/wci-common @elastic/search-kibana +x-pack/solutions/chat/packages/wci-server @elastic/search-kibana x-pack/solutions/chat/plugins/serverless_chat @elastic/search-kibana +x-pack/solutions/chat/plugins/wci-external-server @elastic/search-kibana +x-pack/solutions/chat/plugins/wci-index-source @elastic/search-kibana +x-pack/solutions/chat/plugins/wci-salesforce @elastic/search-kibana +x-pack/solutions/chat/plugins/workchat-app @elastic/search-kibana x-pack/solutions/observability/packages/alert-details @elastic/obs-ux-management-team x-pack/solutions/observability/packages/alerting-test-data @elastic/obs-ux-management-team x-pack/solutions/observability/packages/get-padded-alert-time-range-util @elastic/obs-ux-management-team diff --git a/config/serverless.chat.yml b/config/serverless.chat.yml index 7bf1e1084717..08c9b5e1ace6 100644 --- a/config/serverless.chat.yml +++ b/config/serverless.chat.yml @@ -7,7 +7,17 @@ xpack.serverless.chat.enabled: true xpack.cloud.serverless.project_type: search ## Set the home route -uiSettings.overrides.defaultRoute: /app/home +uiSettings.overrides.defaultRoute: /app/workchat + +## Enable workchat plugins +xpack.workchatApp.enabled: true +xpack.wciSalesforce.enabled: true +xpack.wciIndexSource.enabled: true +xpack.wciExternalServer.enabled: true + +# Disable spaces +xpack.spaces.maxSpaces: 1 + ## Disable plugins that belong to other solutions xpack.apm.enabled: false diff --git a/docs/extend/plugin-list.md b/docs/extend/plugin-list.md index 7e75314b97d6..06ebaead223f 100644 --- a/docs/extend/plugin-list.md +++ b/docs/extend/plugin-list.md @@ -240,3 +240,7 @@ mapped_pages: | [urlDrilldown](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/private/drilldowns/url_drilldown/README.md) | NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin. | | [ux](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/ux/readme.md) | https://docs.elastic.dev/kibana-dev-docs/welcome | | [watcher](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/private/watcher/README.md) | This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): | +| [wciExternalServer](https://github.com/elastic/kibana/blob/main/x-pack/solutions/chat/plugins/wci-external-server/README.md) | WorkChat External Server integration plugin that provides functionality to connect to external servers for the WorkChat application. | +| [wciIndexSource](https://github.com/elastic/kibana/blob/main/x-pack/solutions/chat/plugins/wci-index-source/README.md) | WorkChat Index Source integration plugin that provides functionality to search Elasticsearch indices for the WorkChat application. | +| [wciSalesforce](https://github.com/elastic/kibana/blob/main/x-pack/solutions/chat/plugins/wci-salesforce/README.md) | WorkChat Salesforce integration plugin that provides Salesforce-specific functionality for the WorkChat application. | +| [workchatApp](https://github.com/elastic/kibana/blob/main/x-pack/solutions/chat/plugins/workchat-app/README.md) | WorkChat application plugin | diff --git a/package.json b/package.json index 6c8f4902a604..9ae7d0f327e2 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,8 @@ "@types/react": "~18.2.0", "@types/react-dom": "~18.2.0", "@xstate5/react/**/xstate": "^5.19.2", - "globby/fast-glob": "^3.3.2" + "globby/fast-glob": "^3.3.2", + "pkce-challenge": "3.1.0" }, "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.9.1", @@ -447,6 +448,7 @@ "@kbn/dataset-quality-plugin": "link:x-pack/platform/plugins/shared/dataset_quality", "@kbn/datemath": "link:src/platform/packages/shared/kbn-datemath", "@kbn/deeplinks-analytics": "link:src/platform/packages/shared/deeplinks/analytics", + "@kbn/deeplinks-chat": "link:src/platform/packages/shared/deeplinks/chat", "@kbn/deeplinks-devtools": "link:src/platform/packages/shared/deeplinks/devtools", "@kbn/deeplinks-fleet": "link:src/platform/packages/shared/deeplinks/fleet", "@kbn/deeplinks-management": "link:src/platform/packages/shared/deeplinks/management", @@ -1037,6 +1039,16 @@ "@kbn/visualization-utils": "link:src/platform/packages/shared/kbn-visualization-utils", "@kbn/visualizations-plugin": "link:src/platform/plugins/shared/visualizations", "@kbn/watcher-plugin": "link:x-pack/platform/plugins/private/watcher", + "@kbn/wc-genai-utils": "link:x-pack/solutions/chat/packages/wc-genai-utils", + "@kbn/wc-index-schema-builder": "link:x-pack/solutions/chat/packages/wc-index-schema-builder", + "@kbn/wc-integration-utils": "link:x-pack/solutions/chat/packages/wc-integration-utils", + "@kbn/wci-browser": "link:x-pack/solutions/chat/packages/wci-browser", + "@kbn/wci-common": "link:x-pack/solutions/chat/packages/wci-common", + "@kbn/wci-external-server": "link:x-pack/solutions/chat/plugins/wci-external-server", + "@kbn/wci-index-source": "link:x-pack/solutions/chat/plugins/wci-index-source", + "@kbn/wci-salesforce": "link:x-pack/solutions/chat/plugins/wci-salesforce", + "@kbn/wci-server": "link:x-pack/solutions/chat/packages/wci-server", + "@kbn/workchat-app": "link:x-pack/solutions/chat/plugins/workchat-app", "@kbn/xstate-utils": "link:src/platform/packages/shared/kbn-xstate-utils", "@kbn/zod": "link:src/platform/packages/shared/kbn-zod", "@kbn/zod-helpers": "link:src/platform/packages/shared/kbn-zod-helpers", @@ -1059,6 +1071,8 @@ "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mapbox/mapbox-gl-supported": "2.0.1", "@mapbox/vector-tile": "1.3.1", + "@modelcontextprotocol/sdk": "^1.6.0", + "@n8n/json-schema-to-zod": "^1.1.0", "@openfeature/core": "^1.7.2", "@openfeature/launchdarkly-client-provider": "^0.3.2", "@openfeature/server-sdk": "^1.17.1", @@ -1149,6 +1163,7 @@ "execa": "^5.1.1", "expiry-js": "0.1.7", "exponential-backoff": "^3.1.1", + "expr-eval": "^2.0.2", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "fast-glob": "^3.3.2", diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index d575b6328fab..b571428f176f 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -1169,5 +1169,37 @@ "title", "version" ], + "workchat_agent": [ + "access_control", + "access_control.public", + "agent_id", + "agent_name", + "configuration", + "description", + "last_updated", + "user_id", + "user_name" + ], + "workchat_conversation": [ + "access_control", + "access_control.public", + "agent_id", + "conversation_id", + "events", + "last_updated", + "title", + "user_id", + "user_name" + ], + "workchat_integration": [ + "configuration", + "created_at", + "created_by", + "description", + "integration_id", + "name", + "type", + "updated_at" + ], "workplace_search_telemetry": [] } diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 50128a073b14..95584ea3bdc4 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -3863,6 +3863,107 @@ } } }, + "workchat_agent": { + "dynamic": "strict", + "properties": { + "access_control": { + "properties": { + "public": { + "type": "boolean" + } + } + }, + "agent_id": { + "type": "keyword" + }, + "agent_name": { + "type": "text" + }, + "configuration": { + "dynamic": false, + "properties": {}, + "type": "object" + }, + "description": { + "type": "text" + }, + "last_updated": { + "type": "date" + }, + "user_id": { + "type": "keyword" + }, + "user_name": { + "type": "keyword" + } + } + }, + "workchat_conversation": { + "dynamic": "strict", + "properties": { + "access_control": { + "properties": { + "public": { + "type": "boolean" + } + } + }, + "agent_id": { + "type": "keyword" + }, + "conversation_id": { + "type": "keyword" + }, + "events": { + "dynamic": false, + "properties": {}, + "type": "object" + }, + "last_updated": { + "type": "date" + }, + "title": { + "type": "text" + }, + "user_id": { + "type": "keyword" + }, + "user_name": { + "type": "keyword" + } + } + }, + "workchat_integration": { + "dynamic": "strict", + "properties": { + "configuration": { + "dynamic": false, + "properties": {}, + "type": "object" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "integration_id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, "workplace_search_telemetry": { "dynamic": false, "properties": {} diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 2f6684596f4f..c8a3bdca9327 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -199,3 +199,7 @@ pageLoadAssetSize: visTypeXy: 46868 visualizations: 41000 watcher: 43598 + wciExternalServer: 35000 + wciIndexSource: 40000 + wciSalesforce: 25000 + workchatApp: 25000 diff --git a/renovate.json b/renovate.json index 6622d7b756bd..0b6f2ffe4012 100644 --- a/renovate.json +++ b/renovate.json @@ -2140,6 +2140,28 @@ "minimumReleaseAge": "7 days", "enabled": true }, + { + "groupName": "workchat gen-ai dependencies", + "matchDepNames": [ + "@modelcontextprotocol/sdk", + "@n8n/json-schema-to-zod", + "expr-eval" + ], + "reviewers": [ + "team:search-kibana" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Search", + "Team:Enterprise Search", + "backport:all-open" + ], + "minimumReleaseAge": "7 days", + "enabled": true + }, { "groupName": "vinyl", "matchDepNames": [ diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 96b4537b4f3c..403cddb78c05 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -248,7 +248,7 @@ export default function (program) { 'Adds plugin paths for all the Kibana example plugins and runs with no base path' ) .option( - '--serverless [oblt|security|es]', + '--serverless [oblt|security|es|chat]', 'Start Kibana in a specific serverless project mode. ' + 'If no mode is provided, it starts Kibana in the most recent serverless project mode (default is es)' ); diff --git a/src/core/packages/application/common/src/default_app_categories.ts b/src/core/packages/application/common/src/default_app_categories.ts index 509191938870..72c36d26230c 100644 --- a/src/core/packages/application/common/src/default_app_categories.ts +++ b/src/core/packages/application/common/src/default_app_categories.ts @@ -20,7 +20,6 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze euiIconType: 'logoKibana', order: 1000, }, - // BOOKMARK - List of Kibana solutions - TODO handle the new 'chat' project type - https://elastic.slack.com/archives/C061KHPJS2C/p1741691346619339 enterpriseSearch: { id: 'enterpriseSearch', label: i18n.translate('core.ui.searchNavList.label', { @@ -45,6 +44,14 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze order: 4000, euiIconType: 'logoSecurity', }, + chat: { + id: 'chat', + label: i18n.translate('core.ui.chatNavList.label', { + defaultMessage: 'Workchat', + }), + order: 4500, + euiIconType: 'logoElasticsearch', + }, management: { id: 'management', label: i18n.translate('core.ui.managementNavList.label', { diff --git a/src/core/packages/chrome/browser/src/project_navigation.ts b/src/core/packages/chrome/browser/src/project_navigation.ts index 102e55d97167..e41f69b65a1d 100644 --- a/src/core/packages/chrome/browser/src/project_navigation.ts +++ b/src/core/packages/chrome/browser/src/project_navigation.ts @@ -36,11 +36,12 @@ import type { import type { AppId as SecurityApp, DeepLinkId as SecurityLink } from '@kbn/deeplinks-security'; import type { AppId as FleetApp, DeepLinkId as FleetLink } from '@kbn/deeplinks-fleet'; import type { AppId as SharedApp, DeepLinkId as SharedLink } from '@kbn/deeplinks-shared'; +import type { WorkchatApp, DeepLinkId as ChatLink } from '@kbn/deeplinks-chat'; import type { ChromeNavLink } from './nav_links'; import type { ChromeRecentlyAccessedHistoryItem } from './recently_accessed'; -export type SolutionId = 'es' | 'oblt' | 'security'; +export type SolutionId = 'es' | 'oblt' | 'security' | 'chat'; /** @public */ export type AppId = @@ -56,7 +57,8 @@ export type AppId = | ObservabilityApp | SecurityApp | FleetApp - | SharedApp; + | SharedApp + | WorkchatApp; /** @public */ export type AppDeepLinkId = @@ -68,7 +70,8 @@ export type AppDeepLinkId = | ObservabilityLink | SecurityLink | FleetLink - | SharedLink; + | SharedLink + | ChatLink; /** @public */ export type CloudLinkId = diff --git a/src/core/packages/chrome/browser/tsconfig.json b/src/core/packages/chrome/browser/tsconfig.json index d2bbfd79414c..62da31454f93 100644 --- a/src/core/packages/chrome/browser/tsconfig.json +++ b/src/core/packages/chrome/browser/tsconfig.json @@ -23,7 +23,8 @@ "@kbn/core-application-browser", "@kbn/deeplinks-security", "@kbn/deeplinks-fleet", - "@kbn/deeplinks-shared" + "@kbn/deeplinks-shared", + "@kbn/deeplinks-chat" ], "exclude": [ "target/**/*", diff --git a/src/platform/packages/private/kbn-import-resolver/src/import_resolver.ts b/src/platform/packages/private/kbn-import-resolver/src/import_resolver.ts index e012a7b6004a..e354b1175e16 100644 --- a/src/platform/packages/private/kbn-import-resolver/src/import_resolver.ts +++ b/src/platform/packages/private/kbn-import-resolver/src/import_resolver.ts @@ -142,6 +142,11 @@ export class ImportResolver { return this.adaptReq('src/core/server', dirname); } + if (req.startsWith('@modelcontextprotocol/sdk')) { + const relPath = req.split('@modelcontextprotocol/sdk')[1]; + return Path.resolve(REPO_ROOT, `node_modules/@modelcontextprotocol/sdk/dist/esm/${relPath}`); + } + // turn root-relative paths into relative paths if ( req.startsWith('src/') || diff --git a/src/platform/packages/shared/deeplinks/chat/README.md b/src/platform/packages/shared/deeplinks/chat/README.md new file mode 100644 index 000000000000..c6275cdd7b03 --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/README.md @@ -0,0 +1,3 @@ +# @kbn/deeplinks-chat + +Deeplinks for apps from the chat project type. \ No newline at end of file diff --git a/src/platform/packages/shared/deeplinks/chat/constants.ts b/src/platform/packages/shared/deeplinks/chat/constants.ts new file mode 100644 index 000000000000..7b2d993e658b --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const WORKCHAT_APP_ID = 'workchat'; diff --git a/src/platform/packages/shared/deeplinks/chat/deep_links.ts b/src/platform/packages/shared/deeplinks/chat/deep_links.ts new file mode 100644 index 000000000000..8a85e0023125 --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/deep_links.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { WORKCHAT_APP_ID } from './constants'; + +export type WorkchatApp = typeof WORKCHAT_APP_ID; + +export type WorkchatLinkId = 'agents' | 'integrations'; + +export type DeepLinkId = WorkchatApp | `${WorkchatApp}:${WorkchatLinkId}`; diff --git a/src/platform/packages/shared/deeplinks/chat/index.ts b/src/platform/packages/shared/deeplinks/chat/index.ts new file mode 100644 index 000000000000..ae37eba10eb2 --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { WORKCHAT_APP_ID } from './constants'; +export type { WorkchatApp, WorkchatLinkId, DeepLinkId } from './deep_links'; diff --git a/src/platform/packages/shared/deeplinks/chat/jest.config.js b/src/platform/packages/shared/deeplinks/chat/jest.config.js new file mode 100644 index 000000000000..1d8784cc0c69 --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../../..', + roots: ['/src/platform/packages/shared/deeplinks/chat'], +}; diff --git a/src/platform/packages/shared/deeplinks/chat/kibana.jsonc b/src/platform/packages/shared/deeplinks/chat/kibana.jsonc new file mode 100644 index 000000000000..1b9f26aba83b --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/deeplinks-chat", + "owner": "@elastic/search-kibana", + "group": "platform", + "visibility": "shared" +} diff --git a/src/platform/packages/shared/deeplinks/chat/package.json b/src/platform/packages/shared/deeplinks/chat/package.json new file mode 100644 index 000000000000..a98141a86592 --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/deeplinks-chat", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/src/platform/packages/shared/deeplinks/chat/tsconfig.json b/src/platform/packages/shared/deeplinks/chat/tsconfig.json new file mode 100644 index 000000000000..63f0b5ff33fa --- /dev/null +++ b/src/platform/packages/shared/deeplinks/chat/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx index 3dbd0b3a25f6..83daa9c96b42 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/feedback_btn.tsx @@ -14,6 +14,7 @@ import type { SolutionId } from '@kbn/core-chrome-browser'; const feedbackUrls: { [id in SolutionId]: string } = { es: 'https://ela.st/search-nav-feedback', + chat: 'https://ela.st/search-nav-feedback', oblt: 'https://ela.st/o11y-nav-feedback', security: 'https://ela.st/security-nav-feedback', }; diff --git a/src/platform/plugins/shared/navigation/common/constants.ts b/src/platform/plugins/shared/navigation/common/constants.ts index 0d527e3b4905..74f75fce4c2b 100644 --- a/src/platform/plugins/shared/navigation/common/constants.ts +++ b/src/platform/plugins/shared/navigation/common/constants.ts @@ -14,4 +14,5 @@ export const DEFAULT_ROUTES = { es: '/app/elasticsearch/overview', oblt: '/app/observabilityOnboarding', security: '/app/security/get_started', + chat: '/app/workchat', }; diff --git a/tsconfig.base.json b/tsconfig.base.json index de354ac33dd4..76737f5afd46 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -740,6 +740,8 @@ "@kbn/datemath/*": ["src/platform/packages/shared/kbn-datemath/*"], "@kbn/deeplinks-analytics": ["src/platform/packages/shared/deeplinks/analytics"], "@kbn/deeplinks-analytics/*": ["src/platform/packages/shared/deeplinks/analytics/*"], + "@kbn/deeplinks-chat": ["src/platform/packages/shared/deeplinks/chat"], + "@kbn/deeplinks-chat/*": ["src/platform/packages/shared/deeplinks/chat/*"], "@kbn/deeplinks-devtools": ["src/platform/packages/shared/deeplinks/devtools"], "@kbn/deeplinks-devtools/*": ["src/platform/packages/shared/deeplinks/devtools/*"], "@kbn/deeplinks-fleet": ["src/platform/packages/shared/deeplinks/fleet"], @@ -2134,10 +2136,30 @@ "@kbn/visualizations-plugin/*": ["src/platform/plugins/shared/visualizations/*"], "@kbn/watcher-plugin": ["x-pack/platform/plugins/private/watcher"], "@kbn/watcher-plugin/*": ["x-pack/platform/plugins/private/watcher/*"], + "@kbn/wc-genai-utils": ["x-pack/solutions/chat/packages/wc-genai-utils"], + "@kbn/wc-genai-utils/*": ["x-pack/solutions/chat/packages/wc-genai-utils/*"], + "@kbn/wc-index-schema-builder": ["x-pack/solutions/chat/packages/wc-index-schema-builder"], + "@kbn/wc-index-schema-builder/*": ["x-pack/solutions/chat/packages/wc-index-schema-builder/*"], + "@kbn/wc-integration-utils": ["x-pack/solutions/chat/packages/wc-integration-utils"], + "@kbn/wc-integration-utils/*": ["x-pack/solutions/chat/packages/wc-integration-utils/*"], + "@kbn/wci-browser": ["x-pack/solutions/chat/packages/wci-browser"], + "@kbn/wci-browser/*": ["x-pack/solutions/chat/packages/wci-browser/*"], + "@kbn/wci-common": ["x-pack/solutions/chat/packages/wci-common"], + "@kbn/wci-common/*": ["x-pack/solutions/chat/packages/wci-common/*"], + "@kbn/wci-external-server": ["x-pack/solutions/chat/plugins/wci-external-server"], + "@kbn/wci-external-server/*": ["x-pack/solutions/chat/plugins/wci-external-server/*"], + "@kbn/wci-index-source": ["x-pack/solutions/chat/plugins/wci-index-source"], + "@kbn/wci-index-source/*": ["x-pack/solutions/chat/plugins/wci-index-source/*"], + "@kbn/wci-salesforce": ["x-pack/solutions/chat/plugins/wci-salesforce"], + "@kbn/wci-salesforce/*": ["x-pack/solutions/chat/plugins/wci-salesforce/*"], + "@kbn/wci-server": ["x-pack/solutions/chat/packages/wci-server"], + "@kbn/wci-server/*": ["x-pack/solutions/chat/packages/wci-server/*"], "@kbn/web-worker-stub": ["packages/kbn-web-worker-stub"], "@kbn/web-worker-stub/*": ["packages/kbn-web-worker-stub/*"], "@kbn/whereis-pkg-cli": ["packages/kbn-whereis-pkg-cli"], "@kbn/whereis-pkg-cli/*": ["packages/kbn-whereis-pkg-cli/*"], + "@kbn/workchat-app": ["x-pack/solutions/chat/plugins/workchat-app"], + "@kbn/workchat-app/*": ["x-pack/solutions/chat/plugins/workchat-app/*"], "@kbn/xstate-utils": ["src/platform/packages/shared/kbn-xstate-utils"], "@kbn/xstate-utils/*": ["src/platform/packages/shared/kbn-xstate-utils/*"], "@kbn/yarn-lock-validator": ["packages/kbn-yarn-lock-validator"], diff --git a/x-pack/platform/plugins/shared/features/server/config.test.ts b/x-pack/platform/plugins/shared/features/server/config.test.ts index 096a7f8b9477..dcf8597858e6 100644 --- a/x-pack/platform/plugins/shared/features/server/config.test.ts +++ b/x-pack/platform/plugins/shared/features/server/config.test.ts @@ -128,7 +128,7 @@ describe('config schema', () => { { serverless: true } ) ).toThrowErrorMatchingInlineSnapshot( - `"[overrides.featureA.category]: Unknown category \\"unknown\\". Should be one of kibana, enterpriseSearch, observability, security, management"` + `"[overrides.featureA.category]: Unknown category \\"unknown\\". Should be one of kibana, enterpriseSearch, observability, security, chat, management"` ); }); it('properly validates sub-feature privilege inclusion override', () => { diff --git a/x-pack/platform/plugins/shared/spaces/public/nav_control/solution_view_tour/solution_view_tour.tsx b/x-pack/platform/plugins/shared/spaces/public/nav_control/solution_view_tour/solution_view_tour.tsx index dc4f77a3b3e0..789a7c5b85b9 100644 --- a/x-pack/platform/plugins/shared/spaces/public/nav_control/solution_view_tour/solution_view_tour.tsx +++ b/x-pack/platform/plugins/shared/spaces/public/nav_control/solution_view_tour/solution_view_tour.tsx @@ -36,6 +36,9 @@ const solutionMap: Record = { oblt: i18n.translate('xpack.spaces.navControl.tour.obltSolution', { defaultMessage: 'Observability', }), + chat: i18n.translate('xpack.spaces.navControl.tour.chatSolution', { + defaultMessage: 'Workchat', + }), }; interface Props extends PropsWithChildren<{}> { diff --git a/x-pack/platform/plugins/shared/spaces/public/space_solution_badge/badge.tsx b/x-pack/platform/plugins/shared/spaces/public/space_solution_badge/badge.tsx index 7cdd605a9de6..f8d50ac7207a 100644 --- a/x-pack/platform/plugins/shared/spaces/public/space_solution_badge/badge.tsx +++ b/x-pack/platform/plugins/shared/spaces/public/space_solution_badge/badge.tsx @@ -25,6 +25,10 @@ const SolutionOptions: Record< /> ), }, + chat: { + iconType: 'logoElasticsearch', + label: , + }, security: { iconType: 'logoSecurity', label: ( diff --git a/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.test.ts b/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.test.ts index f19b4d585dc2..477e9fac1936 100644 --- a/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.test.ts +++ b/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.test.ts @@ -12,6 +12,7 @@ const features = [ { id: 'feature1', category: { id: 'observability' } }, { id: 'feature2', category: { id: 'enterpriseSearch' } }, { id: 'feature3', category: { id: 'securitySolution' } }, + { id: 'feature5', category: { id: 'chat' } }, { id: 'feature4', category: { id: 'should_not_be_returned' } }, // not a solution, it should never appeared in the disabled features ] as KibanaFeature[]; @@ -42,7 +43,7 @@ describe('#withSpaceSolutionDisabledFeatures', () => { }); describe('when the space solution is "es"', () => { - test('it removes the "oblt" and "security" features', () => { + test('it removes the "oblt", "security" and "chat" features', () => { const spaceDisabledFeatures: string[] = ['foo']; const spaceSolution = 'es'; @@ -53,12 +54,12 @@ describe('#withSpaceSolutionDisabledFeatures', () => { ); // merges the spaceDisabledFeatures with the disabledFeatureKeysFromSolution - expect(result).toEqual(['feature1', 'feature3']); // "foo" from the spaceDisabledFeatures should not be removed + expect(result).toEqual(['feature1', 'feature3', 'feature5']); // "foo" from the spaceDisabledFeatures should not be removed }); }); describe('when the space solution is "oblt"', () => { - test('it removes the "security" features', () => { + test('it removes the "security" and "chat" features', () => { const spaceDisabledFeatures: string[] = []; const spaceSolution = 'oblt'; @@ -68,12 +69,12 @@ describe('#withSpaceSolutionDisabledFeatures', () => { spaceSolution ); - expect(result).toEqual(['feature3']); + expect(result).toEqual(['feature3', 'feature5']); }); }); describe('when the space solution is "security"', () => { - test('it removes the "observability" and "enterpriseSearch" features', () => { + test('it removes the "observability", "enterpriseSearch" and "chat" features', () => { const spaceDisabledFeatures: string[] = ['baz']; const spaceSolution = 'security'; @@ -83,7 +84,23 @@ describe('#withSpaceSolutionDisabledFeatures', () => { spaceSolution ); - expect(result).toEqual(['feature1', 'feature2']); // "baz" from the spaceDisabledFeatures should not be removed + expect(result).toEqual(['feature1', 'feature2', 'feature5']); // "baz" from the spaceDisabledFeatures should not be removed + }); + }); + + describe('when the space solution is "chat"', () => { + test('it removes the "oblt", "es" and "security" features', () => { + const spaceDisabledFeatures: string[] = ['foo']; + const spaceSolution = 'chat'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + // merges the spaceDisabledFeatures with the disabledFeatureKeysFromSolution + expect(result).toEqual(['feature1', 'feature2', 'feature3']); // "foo" from the spaceDisabledFeatures should not be removed }); }); }); diff --git a/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.ts b/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.ts index 2682daf3a1c5..993bef31f5c4 100644 --- a/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.ts +++ b/x-pack/platform/plugins/shared/spaces/server/lib/utils/space_solution_disabled_features.ts @@ -11,13 +11,17 @@ import type { SolutionView } from '../../../common'; const getFeatureIdsForCategories = ( features: KibanaFeature[], - categories: Array<'observability' | 'enterpriseSearch' | 'securitySolution'> + categories: Array<'observability' | 'enterpriseSearch' | 'securitySolution' | 'chat'> ) => { return features .filter((feature) => feature.category ? categories.includes( - feature.category.id as 'observability' | 'enterpriseSearch' | 'securitySolution' + feature.category.id as + | 'observability' + | 'enterpriseSearch' + | 'securitySolution' + | 'chat' ) : false ) @@ -32,6 +36,7 @@ const enabledFeaturesPerSolution: Record = { es: ['observabilityAIAssistant'], oblt: [], security: [], + chat: [], }; /** @@ -59,16 +64,25 @@ export function withSpaceSolutionDisabledFeatures( disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ 'observability', 'securitySolution', + 'chat', ]).filter((featureId) => !enabledFeaturesPerSolution.es.includes(featureId)); } else if (spaceSolution === 'oblt') { disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ 'securitySolution', + 'chat', ]).filter((featureId) => !enabledFeaturesPerSolution.oblt.includes(featureId)); } else if (spaceSolution === 'security') { disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ 'observability', 'enterpriseSearch', + 'chat', ]).filter((featureId) => !enabledFeaturesPerSolution.security.includes(featureId)); + } else if (spaceSolution === 'chat') { + disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ + 'observability', + 'securitySolution', + 'enterpriseSearch', + ]).filter((featureId) => !enabledFeaturesPerSolution.chat.includes(featureId)); } return Array.from(new Set([...disabledFeatureKeysFromSolution])); diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/README.md b/x-pack/solutions/chat/packages/wc-genai-utils/README.md new file mode 100644 index 000000000000..8c18192bb2e6 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/README.md @@ -0,0 +1,3 @@ +# @kbn/wc-genai-utils + +Empty package generated by @kbn/generate diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/index.ts b/x-pack/solutions/chat/packages/wc-genai-utils/index.ts new file mode 100644 index 000000000000..62e27fb72cc1 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getConnectorList, getDefaultConnector } from './src/connectors'; diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/jest.config.js b/x-pack/solutions/chat/packages/wc-genai-utils/jest.config.js new file mode 100644 index 000000000000..992ac0dafd4f --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/packages/wc-genai-utils'], +}; diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/kibana.jsonc b/x-pack/solutions/chat/packages/wc-genai-utils/kibana.jsonc new file mode 100644 index 000000000000..7bb854810a95 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/wc-genai-utils", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private" +} diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/package.json b/x-pack/solutions/chat/packages/wc-genai-utils/package.json new file mode 100644 index 000000000000..112b06928ea7 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/wc-genai-utils", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_connector_list.ts b/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_connector_list.ts new file mode 100644 index 000000000000..4cead7bbd4c3 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_connector_list.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest } from '@kbn/core/server'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; +import { + isSupportedConnector, + connectorToInference, + InferenceConnector, +} from '@kbn/inference-common'; + +export const getConnectorList = async ({ + actions, + request, +}: { + actions: ActionsPluginStart; + request: KibanaRequest; +}): Promise => { + const actionClient = await actions.getActionsClientWithRequest(request); + + const allConnectors = await actionClient.getAll({ + includeSystemActions: false, + }); + + return allConnectors + .filter((connector) => isSupportedConnector(connector)) + .map(connectorToInference); +}; diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_default_connector.ts b/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_default_connector.ts new file mode 100644 index 000000000000..ed24b5eee8f3 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/get_default_connector.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { InferenceConnector, InferenceConnectorType } from '@kbn/inference-common'; + +/** + * Naive utility function to consistently return the "best" connector for workchat features. + * + * In practice, mostly useful for development, as for production there should always be a single connector + */ +export const getDefaultConnector = ({ connectors }: { connectors: InferenceConnector[] }) => { + // + const inferenceConnector = connectors.find( + (connector) => connector.type === InferenceConnectorType.Inference + ); + if (inferenceConnector) { + return inferenceConnector; + } + + const openAIConnector = connectors.find( + (connector) => connector.type === InferenceConnectorType.OpenAI + ); + if (openAIConnector) { + return openAIConnector; + } + + return connectors[0]; +}; diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/index.ts b/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/index.ts new file mode 100644 index 000000000000..9158cd6cc1f1 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/src/connectors/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getConnectorList } from './get_connector_list'; +export { getDefaultConnector } from './get_default_connector'; diff --git a/x-pack/solutions/chat/packages/wc-genai-utils/tsconfig.json b/x-pack/solutions/chat/packages/wc-genai-utils/tsconfig.json new file mode 100644 index 000000000000..6801c155154d --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-genai-utils/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/actions-plugin", + "@kbn/inference-common", + ] +} diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/README.md b/x-pack/solutions/chat/packages/wc-index-schema-builder/README.md new file mode 100644 index 000000000000..b16f5abfa018 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/README.md @@ -0,0 +1,3 @@ +# @kbn/wc-index-schema-builder + +Empty package generated by @kbn/generate diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/index.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/index.ts new file mode 100644 index 000000000000..23ad7ad649d8 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { buildSchema } from './src/build_schema'; diff --git a/x-pack/solutions/chat/plugins/serverless_chat/server/jest.config.js b/x-pack/solutions/chat/packages/wc-index-schema-builder/jest.config.js similarity index 71% rename from x-pack/solutions/chat/plugins/serverless_chat/server/jest.config.js rename to x-pack/solutions/chat/packages/wc-index-schema-builder/jest.config.js index 95ad8fab9e66..c35480251f8f 100644 --- a/x-pack/solutions/chat/plugins/serverless_chat/server/jest.config.js +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/jest.config.js @@ -5,9 +5,8 @@ * 2.0. */ -/** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - preset: '@kbn/test', - rootDir: '../../../../../..', - roots: ['/x-pack/solutions/chat/plugins/serverless_chat/server/'], + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/packages/wc-index-schema-builder'], }; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/kibana.jsonc b/x-pack/solutions/chat/packages/wc-index-schema-builder/kibana.jsonc new file mode 100644 index 000000000000..b6261c69adee --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/wc-index-schema-builder", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private" +} diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/package.json b/x-pack/solutions/chat/packages/wc-index-schema-builder/package.json new file mode 100644 index 000000000000..e9f9f2927f34 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/wc-index-schema-builder", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/build_schema.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/build_schema.ts new file mode 100644 index 000000000000..f9ca4e993f7f --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/build_schema.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { InferenceChatModel } from '@kbn/inference-langchain'; +import type { IndexSourceDefinition } from '@kbn/wci-common'; +import { createSchemaGraph } from './workflows/build_index_schema'; + +export const buildSchema = async ({ + indexName, + esClient, + logger, + chatModel, +}: { + indexName: string; + logger: Logger; + chatModel: InferenceChatModel; + esClient: ElasticsearchClient; +}): Promise => { + const graph = await createSchemaGraph({ chatModel, esClient, logger }); + + const output = await graph.invoke({ indexName }); + + return output.generatedDefinition as IndexSourceDefinition; +}; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_index_information.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_index_information.ts new file mode 100644 index 000000000000..b78135d76a48 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_index_information.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient } from '@kbn/core/server'; + +export interface IndexInformation { + mappings: MappingTypeMapping; +} + +export const getIndexInformation = async ({ + indexName, + esClient, +}: { + indexName: string; + esClient: ElasticsearchClient; +}): Promise => { + const response = await esClient.indices.getMapping({ index: indexName }); + + const mappings = response[indexName]!.mappings; + + return { + mappings, + }; +}; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.test.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.test.ts new file mode 100644 index 000000000000..d68c26daf33d --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { getLeafFields, MappingField } from './get_leaf_fields'; + +describe('getLeafFields', () => { + test('should return empty array when mappings has no properties', () => { + const mappings: MappingTypeMapping = { + properties: {}, + }; + + const result = getLeafFields({ mappings }); + expect(result).toEqual([]); + }); + + test('should extract leaf fields at root level', () => { + const mappings: MappingTypeMapping = { + properties: { + title: { type: 'text' }, + age: { type: 'integer' }, + enabled: { type: 'boolean' }, + }, + }; + + const expected: MappingField[] = [ + { path: 'title', type: 'text' }, + { path: 'age', type: 'integer' }, + { path: 'enabled', type: 'boolean' }, + ]; + + const result = getLeafFields({ mappings }); + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toBe(expected.length); + }); + + test('should extract nested fields with correct paths', () => { + const mappings: MappingTypeMapping = { + properties: { + user: { + properties: { + firstName: { type: 'text' }, + lastName: { type: 'text' }, + address: { + properties: { + city: { type: 'keyword' }, + zipCode: { type: 'keyword' }, + }, + }, + }, + }, + }, + }; + + const expected: MappingField[] = [ + { path: 'user.firstName', type: 'text' }, + { path: 'user.lastName', type: 'text' }, + { path: 'user.address.city', type: 'keyword' }, + { path: 'user.address.zipCode', type: 'keyword' }, + ]; + + const result = getLeafFields({ mappings }); + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toBe(expected.length); + }); + + test('should handle a mix of leaf fields and nested objects', () => { + const mappings: MappingTypeMapping = { + properties: { + id: { type: 'keyword' }, + content: { type: 'text' }, + metadata: { + properties: { + created: { type: 'date' }, + author: { + properties: { + id: { type: 'keyword' }, + name: { type: 'text' }, + }, + }, + }, + }, + tags: { type: 'keyword' }, + }, + }; + + const expected: MappingField[] = [ + { path: 'id', type: 'keyword' }, + { path: 'content', type: 'text' }, + { path: 'metadata.created', type: 'date' }, + { path: 'metadata.author.id', type: 'keyword' }, + { path: 'metadata.author.name', type: 'text' }, + { path: 'tags', type: 'keyword' }, + ]; + + const result = getLeafFields({ mappings }); + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toBe(expected.length); + }); + + test('should handle mappings with undefined properties', () => { + const mappings: MappingTypeMapping = {}; + + const result = getLeafFields({ mappings }); + expect(result).toEqual([]); + }); +}); diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.ts new file mode 100644 index 000000000000..214c75f4e415 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_leaf_fields.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MappingTypeMapping, MappingProperty } from '@elastic/elasticsearch/lib/api/types'; + +export type FieldType = Extract['type']; + +export interface MappingField { + path: string; + type: FieldType; +} + +export interface MappingProperties { + [key: string]: { + type?: string; // Leaf field (e.g., "text", "keyword", etc.) + properties?: MappingProperties; // Nested object fields + }; +} + +export const getLeafFields = ({ mappings }: { mappings: MappingTypeMapping }): MappingField[] => { + const properties: MappingProperties = mappings.properties ?? {}; + + function extractFields(obj: MappingProperties, prefix = ''): MappingField[] { + let fields: MappingField[] = []; + + for (const [key, value] of Object.entries(obj)) { + const fieldPath = prefix ? `${prefix}.${key}` : key; + + if (value.type) { + // If it's a leaf field, add it + fields.push({ + type: value.type as FieldType, + path: fieldPath, + }); + } else if (value.properties) { + // If it's an object, go deeper + fields = fields.concat(extractFields(value.properties, fieldPath)); + } + } + + return fields; + } + + return extractFields(properties); +}; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_sample_documents.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_sample_documents.ts new file mode 100644 index 000000000000..b7732f533b9d --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/get_sample_documents.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from '@kbn/core/server'; + +export type SampleDocument = Record; + +export const getSampleDocuments = async ({ + indexName, + esClient, + maxSamples = 5, +}: { + indexName: string; + esClient: ElasticsearchClient; + maxSamples?: number; +}): Promise<{ samples: SampleDocument[] }> => { + const response = await esClient.search({ + index: indexName, + size: maxSamples, + }); + + const documents = response.hits.hits.map((hit) => hit._source! as Record); + + return { samples: documents }; +}; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/index.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/index.ts new file mode 100644 index 000000000000..a7308ca7ed38 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/utils/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getIndexInformation, type IndexInformation } from './get_index_information'; +export { getLeafFields } from './get_leaf_fields'; +export { getSampleDocuments, type SampleDocument } from './get_sample_documents'; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/build_index_schema.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/build_index_schema.ts new file mode 100644 index 000000000000..da1cdf3aba05 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/build_index_schema.ts @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; +import { StateGraph, Annotation, Send } from '@langchain/langgraph'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { InferenceChatModel } from '@kbn/inference-langchain'; +import { getFieldTypeByPath, getFieldsTopValues } from '@kbn/wc-integration-utils'; +import type { + IndexSourceDefinition, + IndexSourceFilter, + IndexSourceQueryFields, +} from '@kbn/wci-common'; +import { + getIndexInformation, + getSampleDocuments, + getLeafFields, + type IndexInformation, + type SampleDocument, +} from '../utils'; +import { + generateDescriptionPrompt, + pickFilterFieldsPrompt, + pickQueryFieldsPrompt, + pickContentFieldsPrompt, + generateFilterPrompt, +} from './prompts'; + +export const createSchemaGraph = async ({ + chatModel, + esClient, +}: { + chatModel: InferenceChatModel; + esClient: ElasticsearchClient; + logger: Logger; +}) => { + const StateAnnotation = Annotation.Root({ + indexName: Annotation(), + indexInfo: Annotation, + sampleDocuments: Annotation, + fieldTopValues: Annotation>, + // temporary + filterFields: Annotation, + queryFields: Annotation, + contentFields: Annotation, + description: Annotation(), + // output + generatedDefinition: Annotation>({ + reducer: (a, b) => ({ + ...a, + ...b, + filterFields: [...(a.filterFields ?? []), ...(b.filterFields ?? [])], + }), + default: () => ({}), + }), + }); + + type StateType = typeof StateAnnotation.State; + type BuildFilterStateType = StateType & { + fieldName: string; + }; + + const gatherIndexInfo = async (state: StateType) => { + const indexInfo = await getIndexInformation({ + indexName: state.indexName, + esClient, + }); + const sampleDocuments = await getSampleDocuments({ + indexName: state.indexName, + esClient, + maxSamples: 3, + }); + + const leafFields = getLeafFields({ mappings: indexInfo.mappings }); + + const fieldTopValues = await getFieldsTopValues({ + indexName: state.indexName, + esClient, + maxSize: 20, + fieldNames: leafFields.filter((field) => field.type === 'keyword').map((field) => field.path), + }); + + return { + indexInfo, + sampleDocuments, + fieldTopValues, + generatedDefinition: { index: state.indexName }, + }; + }; + + const pickFilterFields = async (state: StateType) => { + const structuredModel = chatModel.withStructuredOutput( + z.object({ + fields: z.array(z.string()).describe('The list of fields to use as filter fields'), + }) + ); + + const response = await structuredModel.invoke( + pickFilterFieldsPrompt({ + indexName: state.indexName, + indexInfo: state.indexInfo, + fieldTopValues: state.fieldTopValues, + sampleDocuments: state.sampleDocuments, + }) + ); + + return { filterFields: response.fields }; + }; + + const dispatchFilterFields = async (state: StateType) => { + return state.filterFields.map((filterField) => { + return new Send('build_filter_field', { ...state, fieldName: filterField }); + }); + }; + + const buildFilterField = async (state: BuildFilterStateType) => { + const structuredModel = chatModel.withStructuredOutput( + z.object({ + description: z + .string() + .describe('the description for the filter. Please refer to the instruction'), + asEnum: z + .boolean() + .describe('The asEnum value for the filter. Please refer to the instructions'), + }) + ); + + const fieldName = state.fieldName; + const fieldType = getFieldTypeByPath({ + fieldPath: state.fieldName, + mappings: state.indexInfo.mappings, + }); + const fieldTopValues = state.fieldTopValues[fieldName]; + + const response = await structuredModel.invoke( + generateFilterPrompt({ + indexName: state.indexName, + fieldName, + fieldType, + fieldTopValues, + sampleDocuments: state.sampleDocuments, + }) + ); + + const filterField: IndexSourceFilter = { + field: fieldName, + type: fieldType, + description: response.description ?? '', + asEnum: response.asEnum ?? false, + }; + + return { + generatedDefinition: { + filterFields: [filterField], + }, + }; + }; + + const pickQueryFields = async (state: StateType) => { + const structuredModel = chatModel.withStructuredOutput( + z.object({ + fields: z.array(z.string()).describe('The list of fields to use as fulltext fields'), + }) + ); + + const response = await structuredModel.invoke( + pickQueryFieldsPrompt({ + indexName: state.indexName, + indexInfo: state.indexInfo, + sampleDocuments: state.sampleDocuments, + }) + ); + + return { queryFields: response.fields }; + }; + + const buildQueryFields = async (state: StateType) => { + const { + indexInfo: { mappings }, + } = state; + + const queryFields: IndexSourceQueryFields[] = state.queryFields.map((field) => { + return { + field, + type: getFieldTypeByPath({ fieldPath: field, mappings })!, + }; + }); + + return { + generatedDefinition: { + queryFields, + }, + }; + }; + + /////// + const pickContentFields = async (state: StateType) => { + const structuredModel = chatModel.withStructuredOutput( + z.object({ + fields: z.array(z.string()).describe('The list of fields to use as content fields'), + }) + ); + + const response = await structuredModel.invoke( + pickContentFieldsPrompt({ + indexName: state.indexName, + indexInfo: state.indexInfo, + sampleDocuments: state.sampleDocuments, + }) + ); + + return { contentFields: response.fields }; + }; + + const buildContentFields = async (state: StateType) => { + const { + indexInfo: { mappings }, + } = state; + + const contentFields: IndexSourceQueryFields[] = state.contentFields.map((field) => { + return { + field, + type: getFieldTypeByPath({ fieldPath: field, mappings })!, + }; + }); + + return { + generatedDefinition: { + contentFields, + }, + }; + }; + /////// + + const generateDescription = async (state: StateType) => { + const structuredModel = chatModel.withStructuredOutput( + z.object({ + description: z.string().describe('The description for the tool'), + }) + ); + + const response = await structuredModel.invoke( + generateDescriptionPrompt({ + sourceDefinition: state.generatedDefinition, + indexName: state.indexName, + indexInfo: state.indexInfo, + sampleDocuments: state.sampleDocuments, + }) + ); + return { generatedDefinition: { description: response.description } }; + }; + + const graph = new StateGraph(StateAnnotation) + // nodes + .addNode('gather_index_info', gatherIndexInfo) + .addNode('pick_filter_fields', pickFilterFields) + .addNode('pick_query_fields', pickQueryFields) + .addNode('build_query_fields', buildQueryFields) + .addNode('build_filter_field', buildFilterField) + .addNode('generate_description', generateDescription) + .addNode('pick_content_fields', pickContentFields) + .addNode('build_content_fields', buildContentFields) + + // transitions + .addEdge('__start__', 'gather_index_info') + .addEdge('gather_index_info', 'pick_filter_fields') + .addEdge('gather_index_info', 'pick_query_fields') + .addEdge('gather_index_info', 'pick_content_fields') + .addEdge('pick_query_fields', 'build_query_fields') + .addEdge('build_query_fields', 'generate_description') + .addEdge('pick_content_fields', 'build_content_fields') + .addEdge('build_content_fields', 'generate_description') + .addEdge('generate_description', '__end__') + .addConditionalEdges('pick_filter_fields', dispatchFilterFields, { + build_filter_field: 'build_filter_field', + }) + .addEdge('build_filter_field', 'generate_description') + // done + .compile(); + + return graph; +}; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/prompts.ts b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/prompts.ts new file mode 100644 index 000000000000..9ef5a7098952 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/src/workflows/prompts.ts @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BaseMessageLike } from '@langchain/core/messages'; +import type { IndexSourceDefinition } from '@kbn/wci-common'; +import { type IndexInformation, type SampleDocument } from '../utils'; + +export const generateDescriptionPrompt = ({ + indexName, + indexInfo, + sampleDocuments, + sourceDefinition, +}: { + indexName: string; + indexInfo: IndexInformation; + sampleDocuments: SampleDocument[]; + sourceDefinition: Partial; +}): BaseMessageLike[] => { + return [ + [ + 'system', + `You are an helpful AI assistant, with expert knowledge of Elasticsearch and the Elastic stack. + Your current job is to generate schemas to describe Elasticsearch indices, following a predefined format. + `, + ], + [ + 'human', + ` + ## Task description + + You are building a schema representing a tool that can be used by an LLM to query an Elasticsearch index. + You previously generated the information about which field should be used for full text search (query fields), + and which ones should be used for filtering (filter fields). + + Your current task is to generate a description for the tool. The description will be used for the LLM + to know what the tool can be used for. + + - Please keep the description relatively short - ideally not more than a few lines + - Describe *what the tool can be used to query*, not the index + + E.g + + "This tool can be used to query the [logs] index, which contains log entries from a web application. + most of the logs are access logs from NGInx." + + ### Base information: + + - index name: ${indexName} + + ### Tool definition: + + ${JSON.stringify(sourceDefinition)} + + ### Mappings: + + ${JSON.stringify(indexInfo.mappings)} + + ### Sample documents: + + ${JSON.stringify(sampleDocuments)} + + `, + ], + ]; +}; + +export const pickFilterFieldsPrompt = ({ + indexName, + indexInfo, + sampleDocuments, + fieldTopValues, + maxFilters = 5, +}: { + indexName: string; + indexInfo: IndexInformation; + sampleDocuments: SampleDocument[]; + fieldTopValues: Record; + maxFilters?: number; +}): BaseMessageLike[] => { + return [ + [ + 'system', + `You are an helpful AI assistant, with expert knowledge of Elasticsearch and the Elastic stack. + Your current job is to generate schemas to describe Elasticsearch indices, following a predefined format. + `, + ], + [ + 'human', + ` + + ## Task description + + We want to generate a search schema for the index ${indexName}. For that purpose, + we want to select the fields will be defined as "filters" in the schema. + + "Filter" fields are basically fields that the user will be able to use in the UI to create search filter. + E.g. if the "category" field is a filter, then the user will be able to search for "category: CAT". + + ## Additional directives + + - "meta" fields that don't represent anything concrete are irrelevant, and shouldn't be picked + - e.g 'inference_id' or '_run_inference' + + - please pick no more than *${maxFilters}* fields. + + ## Index information + + Here are some information to help you in your decision: + + ### Base information: + + - index name: ${indexName} + + ### Mappings: + + ${JSON.stringify(indexInfo.mappings)} + + ### Sample documents: + + ${JSON.stringify(sampleDocuments)} + + ### Fields top values: + + ${Object.entries(fieldTopValues) + .map(([key, values]) => { + return `- field ${key}: ${values.join(', ')}`; + }) + .join('\n')} + + Given the previous information, please list the fields that you think would make the most sense to be used as filter. + `, + ], + ]; +}; + +export const generateFilterPrompt = ({ + indexName, + fieldName, + fieldType, + sampleDocuments, + fieldTopValues, +}: { + indexName: string; + fieldName: string; + fieldType: string; + + sampleDocuments: SampleDocument[]; + fieldTopValues?: string[]; +}): BaseMessageLike[] => { + return [ + [ + 'system', + `You are an helpful AI assistant, with expert knowledge of Elasticsearch and the Elastic stack. + Your current job is to generate schemas to describe Elasticsearch indices, following a predefined format. + `, + ], + [ + 'human', + ` + + ## Task description + + We previously selected a list of fields from the index's mappings that will be used as filter in the schema. + + "Filter" fields are basically fields that the user will be able to use in the UI to create search filter. + E.g. if the "category" field is a filter, then the user will be able to search for "category: CAT". + + We now want to generate the full filter definition for the "${fieldName}" field, which is of type "${fieldType}". + + The filter definition is composed of: + - description: (string) a short description for what the field/filter can be used for + - asEnum: (boolean) if true, the filter will behave as an enum: the field's top values will be fetched at query time and + listed in the description, and using the filter will be limited to doing it against those values. + + ## Additional directives + + - 'asEnum' can only be true if the field type is 'keyword' or 'boolean' + + ## Context information + + Here are some information to help you in your decision: + + ### Base information: + + - index name: ${indexName} + - filter field name: ${fieldName} + - filter field type: ${fieldType} + + ### Sample documents: + + ${JSON.stringify(sampleDocuments)} + + ### Fields top values: + + ${fieldTopValues?.map((value) => `- ${value}`).join('\n') ?? 'No top values for that field type'} + + Given the previous information, generate the filter definition. + `, + ], + ]; +}; + +export const pickQueryFieldsPrompt = ({ + indexName, + indexInfo, + sampleDocuments, + maxFields = 2, +}: { + indexName: string; + indexInfo: IndexInformation; + sampleDocuments: SampleDocument[]; + maxFields?: number; +}): BaseMessageLike[] => { + return [ + [ + 'system', + `You are an helpful AI assistant, with expert knowledge of Elasticsearch and the Elastic stack. + Your current job is to generate schemas to describe Elasticsearch indices, following a predefined format. + `, + ], + [ + 'human', + ` + + ## Task description + + We want to generate a search schema for the index ${indexName}. For that purpose, + we want to select the fields will be defined as "fulltext search" fields in the schema. + + "fulltext search" fields are fields that text queries will be performed against. + E.g. when the user search for "red balloon CATEGORY:RED", we will perform a text search for "red balloon" against the fields + defined as "fulltext search" field in the schema. + + ## Additional directives + + - fulltext search fields can only be of type 'text' or 'semantic_text' + - only includes the fields that are the most likely to contains "content" text + - please pick no more than *${maxFields}* fields. + + ## Index information + + Here are some information to help you in your decision: + + ### Base information: + + - index name: ${indexName} + + ### Mappings: + + ${JSON.stringify(indexInfo.mappings)} + + ### Sample documents: + + ${JSON.stringify(sampleDocuments)} + + Given the previous information, please list the fields that you think would make the most sense to be used as full text fields. + `, + ], + ]; +}; + +export const pickContentFieldsPrompt = ({ + indexName, + indexInfo, + sampleDocuments, + maxFields = 10, +}: { + indexName: string; + indexInfo: IndexInformation; + sampleDocuments: SampleDocument[]; + maxFields?: number; +}): BaseMessageLike[] => { + return [ + [ + 'system', + `You are an helpful AI assistant, with expert knowledge of Elasticsearch and the Elastic stack. + Your current job is to generate schemas to describe Elasticsearch indices, following a predefined format. + `, + ], + [ + 'human', + ` + + ## Task description + + We want to generate a schema for a tool that will then be used by a LLM to query the index ${indexName}. For that purpose, + you current task is to define the "content" fields, fields that will be returned by the tool as content for the LLM to use. + + ## Additional directives + + - do not include "meta" fields such as _inference_id or similar without real value + - please pick the fields that you think would be the most useful for the + - please pick no more than *${maxFields}* fields. + + ## Index information + + Here are some information to help you in your decision: + + ### Base information: + + - index name: ${indexName} + + ### Mappings: + + ${JSON.stringify(indexInfo.mappings)} + + ### Sample documents: + + ${JSON.stringify(sampleDocuments)} + + Given the previous information, please list the fields that you think would make the most sense to be used as content fields. + `, + ], + ]; +}; diff --git a/x-pack/solutions/chat/packages/wc-index-schema-builder/tsconfig.json b/x-pack/solutions/chat/packages/wc-index-schema-builder/tsconfig.json new file mode 100644 index 000000000000..d26b60688fc4 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-index-schema-builder/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/inference-langchain", + "@kbn/wci-common", + "@kbn/zod", + "@kbn/wc-integration-utils", + ] +} diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/README.md b/x-pack/solutions/chat/packages/wc-integration-utils/README.md new file mode 100644 index 000000000000..b28f3568ac57 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/README.md @@ -0,0 +1,3 @@ +# @kbn/wc-integration-utils + +Empty package generated by @kbn/generate diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/index.ts b/x-pack/solutions/chat/packages/wc-integration-utils/index.ts new file mode 100644 index 000000000000..42dd656267d2 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getFieldsTopValues, getFieldTypeByPath } from './src/elasticsearch'; +export { + generateSearchSchema, + type SearchFilter, + createFilterClauses, + hitToContent, +} from './src/tools'; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/jest.config.js b/x-pack/solutions/chat/packages/wc-integration-utils/jest.config.js new file mode 100644 index 000000000000..7ee05826587a --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/packages/wc-integration-utils'], +}; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/kibana.jsonc b/x-pack/solutions/chat/packages/wc-integration-utils/kibana.jsonc new file mode 100644 index 000000000000..4c02a4cf1c6b --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/wc-integration-utils", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private" +} diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/package.json b/x-pack/solutions/chat/packages/wc-integration-utils/package.json new file mode 100644 index 000000000000..b7a7b47ae1cc --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/wc-integration-utils", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.test.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.test.ts new file mode 100644 index 000000000000..9ff45a4c9ae2 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { getFieldTypeByPath } from './get_field_type_by_path'; + +describe('getFieldTypeByPath', () => { + it('returns the type for a top level field', () => { + const mappings: MappingTypeMapping = { + properties: { + content: { + type: 'text', + }, + category: { + type: 'keyword', + }, + }, + }; + + const type = getFieldTypeByPath({ fieldPath: 'content', mappings }); + + expect(type).toEqual('text'); + }); + + it('returns the type for a nested field', () => { + const mappings: MappingTypeMapping = { + properties: { + nested: { + type: 'object', + properties: { + category: { type: 'keyword' }, + }, + }, + category: { + type: 'keyword', + }, + }, + }; + + const type = getFieldTypeByPath({ fieldPath: 'nested.category', mappings }); + + expect(type).toEqual('keyword'); + }); + + it('throw an error for fields not present in the mappings', () => { + const mappings: MappingTypeMapping = { + properties: { + content: { + type: 'text', + }, + }, + }; + + expect(() => + getFieldTypeByPath({ fieldPath: 'missing', mappings }) + ).toThrowErrorMatchingInlineSnapshot(`"Field 'missing' not found in mappings"`); + }); +}); diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.ts new file mode 100644 index 000000000000..382d57cc02c0 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_field_type_by_path.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; + +interface MappingProperties { + [key: string]: { + type?: string; + properties?: MappingProperties; + }; +} + +/** + * Resolves the type of a given field from its path in the provided mappings. + */ +export const getFieldTypeByPath = ({ + fieldPath, + mappings, +}: { + fieldPath: string; + mappings: MappingTypeMapping; +}): string => { + let properties: MappingProperties = mappings.properties ?? {}; + + const paths = fieldPath.split('.'); + for (let i = 0; i < paths.length; i++) { + const path = paths[i]; + const isLast = i === paths.length - 1; + if (isLast) { + if (properties[path]?.type) { + return properties[path]?.type!; + } else { + throw Error(`Field '${fieldPath}' not found in mappings`); + } + } else { + if (properties[path]?.properties) { + properties = properties[path]!.properties!; + } else { + throw Error(`Field '${fieldPath}' not found in mappings`); + } + } + } + throw Error(`Exited loop without return (should never happen)`); +}; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_fields_top_values.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_fields_top_values.ts new file mode 100644 index 000000000000..43ee9eb45f01 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/get_fields_top_values.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AggregationsStringTermsAggregate } from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient } from '@kbn/core/server'; + +export const getFieldsTopValues = async ({ + indexName, + fieldNames, + esClient, + maxSize = 20, +}: { + indexName: string; + fieldNames: string[]; + esClient: ElasticsearchClient; + maxSize?: number; +}): Promise> => { + const aggResult = await esClient.search({ + index: indexName, + size: 0, + aggs: Object.fromEntries( + fieldNames.map((field) => [ + field, + { + terms: { + field, + size: maxSize, + }, + }, + ]) + ), + }); + + const aggregations = aggResult.aggregations!; + + const topValues = fieldNames.reduce((map, fieldName) => { + const aggr = aggregations[fieldName] as AggregationsStringTermsAggregate; + + if (aggr.buckets && Array.isArray(aggr.buckets)) { + // key | doc_count + const values = aggr.buckets.map((bucket) => bucket.key as string); + map[fieldName] = values; + } + + // aggr.buckets[0] + + return map; + }, {} as Record); + + return topValues; +}; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/index.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/index.ts new file mode 100644 index 000000000000..34a36d593c10 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/elasticsearch/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getFieldsTopValues } from './get_fields_top_values'; +export { getFieldTypeByPath } from './get_field_type_by_path'; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.test.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.test.ts new file mode 100644 index 000000000000..671943fc2a59 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createFilterClauses } from './create_filter_clauses'; +import type { SearchFilter } from './generate_search_schema'; + +describe('createFilterClauses', () => { + it('generate the correct clauses for a keyword filter', () => { + const filter: SearchFilter = { + type: 'keyword', + field: 'foo', + description: 'foo filter', + }; + + const values = { + foo: 'bar', + hello: 'dolly', + }; + + const output = createFilterClauses({ filters: [filter], values }); + + expect(output).toEqual([{ term: { foo: 'bar' } }]); + }); + + it('generate the correct clauses for a boolean filter', () => { + const filter: SearchFilter = { + type: 'boolean', + field: 'isActive', + description: 'active status filter', + }; + + const values = { + isActive: true, + otherField: 'ignored', + }; + + const output = createFilterClauses({ filters: [filter], values }); + + expect(output).toEqual([{ term: { isActive: true } }]); + }); + + it('handles multiple filters correctly', () => { + const filters: SearchFilter[] = [ + { + type: 'keyword', + field: 'foo', + description: 'foo filter', + }, + { + type: 'boolean', + field: 'isActive', + description: 'active status filter', + }, + ]; + + const values = { + foo: 'bar', + isActive: true, + ignored: 'value', + }; + + const output = createFilterClauses({ filters, values }); + + expect(output).toEqual([{ term: { foo: 'bar' } }, { term: { isActive: true } }]); + }); + + it('returns empty array when no values are provided', () => { + const filters: SearchFilter[] = [ + { + type: 'keyword', + field: 'foo', + description: 'foo filter', + }, + ]; + + const values = {}; + + const output = createFilterClauses({ filters, values }); + + expect(output).toEqual([]); + }); + + it('ignores values that do not have matching filters', () => { + const filters: SearchFilter[] = [ + { + type: 'keyword', + field: 'foo', + description: 'foo filter', + }, + ]; + + const values = { + foo: 'bar', + unmatched: 'value', + anotherUnmatched: 123, + }; + + const output = createFilterClauses({ filters, values }); + + expect(output).toEqual([{ term: { foo: 'bar' } }]); + }); +}); diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.ts new file mode 100644 index 000000000000..48d064ba8213 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/create_filter_clauses.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import type { SearchFilter } from './generate_search_schema'; + +export const createFilterClauses = ({ + filters, + values, +}: { + filters: SearchFilter[]; + values: Record; +}): QueryDslQueryContainer[] => { + const clauses: QueryDslQueryContainer[] = []; + + Object.entries(values).forEach(([field, value]) => { + const filter = filters.find((f) => f.field === field); + if (filter) { + if (filter.type === 'keyword' || filter.type === 'boolean') { + clauses.push({ + term: { [field]: value }, + }); + } + // TODO: handle other field types, date mostly + } + }); + + return clauses; +}; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.test.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.test.ts new file mode 100644 index 000000000000..fe5061afe56e --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; +import { generateSearchSchema, type SearchFilter } from './generate_search_schema'; + +describe('generateSearchSchema', () => { + function getTypeName(schema: z.Schema): string | undefined { + schema = unwrap(schema); + + const typeName = + 'typeName' in schema._def && typeof schema._def.typeName === 'string' + ? schema._def.typeName + : undefined; + + return typeName; + } + + function unwrap(schema: z.Schema) { + if (schema.isOptional()) { + return (schema as z.ZodOptional).unwrap(); + } + return schema; + } + + it('should generate a schema with query field by default', () => { + const schema = generateSearchSchema({ filters: [] }); + expect(schema.query).toBeDefined(); + expect(getTypeName(schema.query)).toBe('ZodString'); + expect(schema.query.isOptional()).toBe(true); + }); + + it('should generate schema for keyword filter without predefined values', () => { + const filters: SearchFilter[] = [ + { + field: 'status', + type: 'keyword', + description: 'Filter by status', + }, + ]; + const schema = generateSearchSchema({ filters }); + + expect(schema.status).toBeDefined(); + expect(getTypeName(schema.status)).toBe('ZodString'); + expect(schema.status.isOptional()).toBe(true); + }); + + it('should generate schema for keyword filter with predefined values', () => { + const filters: SearchFilter[] = [ + { + field: 'status', + type: 'keyword', + description: 'Filter by status', + values: ['active', 'inactive', 'pending'], + }, + ]; + const schema = generateSearchSchema({ filters }); + + expect(schema.status).toBeDefined(); + expect(getTypeName(schema.status)).toBe('ZodEnum'); + expect((unwrap(schema.status) as z.ZodEnum).options).toEqual([ + 'active', + 'inactive', + 'pending', + ]); + expect(schema.status.isOptional()).toBe(true); + }); + + it('should generate schema for date filter', () => { + const filters: SearchFilter[] = [ + { + field: 'created_at', + type: 'date', + description: 'Filter by creation date', + }, + ]; + const schema = generateSearchSchema({ filters }); + + expect(schema.created_at).toBeDefined(); + expect(getTypeName(schema.created_at)).toBe('ZodString'); + expect(schema.created_at.isOptional()).toBe(true); + }); + + it('should generate schema for boolean filter', () => { + const filters: SearchFilter[] = [ + { + field: 'is_active', + type: 'boolean', + description: 'Filter by active status', + }, + ]; + const schema = generateSearchSchema({ filters }); + + expect(schema.is_active).toBeDefined(); + expect(getTypeName(schema.is_active)).toBe('ZodBoolean'); + expect(schema.is_active.isOptional()).toBe(true); + }); + + it('should combine multiple filters in the schema', () => { + const filters: SearchFilter[] = [ + { + field: 'status', + type: 'keyword', + description: 'Filter by status', + values: ['active', 'inactive'], + }, + { + field: 'created_at', + type: 'date', + description: 'Filter by creation date', + }, + { + field: 'is_active', + type: 'boolean', + description: 'Filter by active status', + }, + ]; + const schema = generateSearchSchema({ filters }); + + expect(schema.query).toBeDefined(); + expect(schema.status).toBeDefined(); + expect(schema.created_at).toBeDefined(); + expect(schema.is_active).toBeDefined(); + }); + + it('should handle empty values array for keyword filter', () => { + const filters: SearchFilter[] = [ + { + field: 'status', + type: 'keyword', + description: 'Filter by status', + values: [], + }, + ]; + const schema = generateSearchSchema({ filters }); + + expect(schema.status).toBeDefined(); + expect(getTypeName(schema.status)).toBe('ZodString'); + expect(schema.status.isOptional()).toBe(true); + }); +}); diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.ts new file mode 100644 index 000000000000..2d14e00e573a --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/generate_search_schema.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; + +export interface SearchFilter { + field: string; + type: 'keyword' | 'date' | 'boolean'; + description: string; + values?: string[]; +} + +export const generateSearchSchema = ({ filters }: { filters: SearchFilter[] }) => { + return filters.reduce>( + (schema, filter) => { + return { + ...schema, + ...generateFilterSchema({ filter }), + }; + }, + { + query: z.string().describe('A query to use for fulltext search').optional(), + } + ); +}; + +export const generateFilterSchema = ({ + filter, +}: { + filter: SearchFilter; +}): Record => { + switch (filter.type) { + case 'keyword': + if (filter.values && filter.values.length > 0) { + return { + [filter.field]: z + .enum(filter.values as [string, ...string[]]) + .describe(filter.description) + .optional(), + }; + } else { + return { + [filter.field]: z.string().describe(filter.description).optional(), + }; + } + case 'date': + return { + [filter.field]: z + .string() + .datetime({ offset: true }) + .describe(`${filter.description} - use ISO 8601 format`) + .optional(), + }; + case 'boolean': + return { [filter.field]: z.boolean().describe(filter.description).optional() }; + default: + return {}; + } +}; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.test.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.test.ts new file mode 100644 index 000000000000..aa66df06a62f --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import { hitToContent } from './hit_to_content'; + +const createSearchHit = (overrides: Partial = {}): SearchHit => ({ + _index: 'test-index', + _id: 'test-id', + _score: 1, + _source: {}, + ...overrides, +}); + +describe('hitToContent', () => { + it('should return highlighted fields when highlights are present', () => { + const hit = createSearchHit({ + _source: { + title: 'Original title', + content: 'Original content', + }, + highlight: { + title: ['Highlighted title'], + content: ['Highlighted content'], + }, + }); + + const result = hitToContent({ + hit, + fields: ['title', 'content'], + }); + + expect(result).toEqual({ + title: ['Highlighted title'], + content: ['Highlighted content'], + }); + }); + + it('should fall back to _source when highlights are not present', () => { + const hit = createSearchHit({ + _source: { + title: 'Original title', + content: 'Original content', + }, + }); + + const result = hitToContent({ + hit, + fields: ['title', 'content'], + }); + + expect(result).toEqual({ + title: 'Original title', + content: 'Original content', + }); + }); + + it('should handle undefined _source', () => { + const hit = createSearchHit({ + _source: undefined, + highlight: { + title: ['Highlighted title'], + }, + }); + + const result = hitToContent({ + hit, + fields: ['title', 'content'], + }); + + expect(result).toEqual({ + title: ['Highlighted title'], + content: undefined, + }); + }); + + it('should handle non-existent fields', () => { + const hit = createSearchHit({ + _source: { + title: 'Original title', + }, + }); + + const result = hitToContent({ + hit, + fields: ['title', 'nonExistentField'], + }); + + expect(result).toEqual({ + title: 'Original title', + nonExistentField: undefined, + }); + }); +}); diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.ts new file mode 100644 index 000000000000..0466291ae49c --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/hit_to_content.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash'; +import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; + +export const hitToContent = ({ + hit, + fields, +}: { + hit: SearchHit; + fields: string[]; +}): Record => { + const content: Record = {}; + + fields.forEach((field) => { + if (hit.highlight?.[field]) { + content[field] = hit.highlight?.[field]; + } else { + content[field] = get(hit._source ?? {}, field); + } + }); + + return content; +}; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/index.ts b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/index.ts new file mode 100644 index 000000000000..94571fdb06b2 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/src/tools/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { generateSearchSchema, type SearchFilter } from './generate_search_schema'; +export { createFilterClauses } from './create_filter_clauses'; +export { hitToContent } from './hit_to_content'; diff --git a/x-pack/solutions/chat/packages/wc-integration-utils/tsconfig.json b/x-pack/solutions/chat/packages/wc-integration-utils/tsconfig.json new file mode 100644 index 000000000000..7b18438736c3 --- /dev/null +++ b/x-pack/solutions/chat/packages/wc-integration-utils/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/zod", + ] +} diff --git a/x-pack/solutions/chat/packages/wci-browser/README.md b/x-pack/solutions/chat/packages/wci-browser/README.md new file mode 100644 index 000000000000..458895802bb9 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-browser/README.md @@ -0,0 +1,3 @@ +# @kbn/wci-browser + +Empty package generated by @kbn/generate diff --git a/x-pack/solutions/chat/packages/wci-browser/index.ts b/x-pack/solutions/chat/packages/wci-browser/index.ts new file mode 100644 index 000000000000..d4a5f835e940 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-browser/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { + IntegrationComponentDescriptor, + IntegrationToolComponentProps, + IntegrationConfigurationFormProps, +} from './src/integration_ui_descriptor'; diff --git a/x-pack/solutions/chat/packages/wci-browser/jest.config.js b/x-pack/solutions/chat/packages/wci-browser/jest.config.js new file mode 100644 index 000000000000..973b12460bf9 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-browser/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/packages/wci-browser'], +}; diff --git a/x-pack/solutions/chat/packages/wci-browser/kibana.jsonc b/x-pack/solutions/chat/packages/wci-browser/kibana.jsonc new file mode 100644 index 000000000000..af691b38faf7 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-browser/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-browser", + "id": "@kbn/wci-browser", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private" +} diff --git a/x-pack/solutions/chat/packages/wci-browser/package.json b/x-pack/solutions/chat/packages/wci-browser/package.json new file mode 100644 index 000000000000..9f87e96f49ac --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-browser/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/wci-browser", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wci-browser/src/integration_ui_descriptor.ts b/x-pack/solutions/chat/packages/wci-browser/src/integration_ui_descriptor.ts new file mode 100644 index 000000000000..1e8ce4065104 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-browser/src/integration_ui_descriptor.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type React from 'react'; +import type { UseFormReturn } from 'react-hook-form'; +import { IntegrationType, Integration, ToolCall } from '@kbn/wci-common'; + +export interface IntegrationComponentDescriptor { + getType: () => IntegrationType; + getConfigurationForm: () => React.ComponentType; + getToolCallComponent: (toolName: string) => React.ComponentType; +} + +/** + * Props that will be passed to the tool call component + */ +export interface IntegrationToolComponentProps { + /** + * The integration the call was made with + */ + integration: Integration; + /** + * The tool call to render + */ + toolCall: ToolCall; + /** + * If tool call is complete, will contain the string result of the call + */ + toolResult?: string; +} + +export interface IntegrationConfigurationFormProps { + // TODO: fix this + // shouldn't need this and use the useFormContext vs passing down as prop + form: UseFormReturn; +} diff --git a/x-pack/solutions/chat/packages/wci-browser/tsconfig.json b/x-pack/solutions/chat/packages/wci-browser/tsconfig.json new file mode 100644 index 000000000000..0f97a5d7832c --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-browser/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/wci-common", + ] +} diff --git a/x-pack/solutions/chat/packages/wci-common/README.md b/x-pack/solutions/chat/packages/wci-common/README.md new file mode 100644 index 000000000000..2b3e5c206a3d --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/README.md @@ -0,0 +1,3 @@ +# @kbn/wci-common + +Contains shared types and constants for WorkChat integrations. For server-side implementation, see the `@kbn/wci-server` package. \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wci-common/index.ts b/x-pack/solutions/chat/packages/wci-common/index.ts new file mode 100644 index 000000000000..74df1a444870 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { IntegrationType } from './src/constants'; +export type { Integration, IntegrationConfiguration } from './src/integrations'; +export type { ToolCall } from './src/tool_calls'; +export { + buildToolName, + parseToolName, + type ToolNameAndIntegrationId, +} from './src/integration_tools'; +export type { + IndexSourceDefinition, + IndexSourceFilter, + IndexSourceQueryFields, +} from './src/index_source'; diff --git a/x-pack/solutions/chat/packages/wci-common/jest.config.js b/x-pack/solutions/chat/packages/wci-common/jest.config.js new file mode 100644 index 000000000000..f1cb7bada26c --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/packages/wci-common'], +}; diff --git a/x-pack/solutions/chat/packages/wci-common/kibana.jsonc b/x-pack/solutions/chat/packages/wci-common/kibana.jsonc new file mode 100644 index 000000000000..d048a6158347 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/kibana.jsonc @@ -0,0 +1,9 @@ +{ + "type": "shared-common", + "id": "@kbn/wci-common", + "owner": [ + "@elastic/search-kibana" + ], + "group": "chat", + "visibility": "private" +} diff --git a/x-pack/solutions/chat/packages/wci-common/package.json b/x-pack/solutions/chat/packages/wci-common/package.json new file mode 100644 index 000000000000..1557db26eb5f --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/wci-common", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wci-common/src/constants.ts b/x-pack/solutions/chat/packages/wci-common/src/constants.ts new file mode 100644 index 000000000000..c30ba9c66932 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/src/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum IntegrationType { + salesforce = 'salesforce', + index_source = 'index_source', + external_server = 'external_server', +} diff --git a/x-pack/solutions/chat/packages/wci-common/src/index_source.ts b/x-pack/solutions/chat/packages/wci-common/src/index_source.ts new file mode 100644 index 000000000000..e639d7050fc7 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/src/index_source.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Represents a definition for an index source. + * + * The definition contains all what's necessary for the system to build + * the MCP tool that will then be used by the LLM to query the data. + */ +export interface IndexSourceDefinition { + /** + * ID of the index that is going to be used for this index source + */ + index: string; + /** + * A short description of what the index contains + */ + description: string; + /** + * List of fields that will be used for fulltext search. + */ + queryFields: IndexSourceQueryFields[]; + /** + * List of possible filters when querying for the data + */ + filterFields: IndexSourceFilter[]; + /** + * List of fields that will be returned as content by the tool + */ + contentFields: IndexSourceContentFields[]; +} + +export interface IndexSourceFilter { + /** + * The name / path to the field + * E.g. `content` or `reference.id` + */ + field: string; + /** + * The type of field. Should be the same type as defined in the mappings + */ + type: string; + /** + * A human-readable description for this filter. + */ + description: string; + /** + * If true, the field's top values will be fetched at query time, + * and added to the description. The parameter will also be restricted + * to only allow those values + */ + asEnum: boolean; +} + +/** + * Represents a field that will be used for full-text search. + */ +export interface IndexSourceQueryFields { + /** + * The name / path to the field + * E.g. `content` or `reference.id` + */ + field: string; + /** + * The type of field. Should be the same type as defined in the mappings + */ + type: string; +} + +/** + * Represents a field that will be used for full-text search. + */ +export interface IndexSourceContentFields { + /** + * The name / path to the field + * E.g. `content` or `reference.id` + */ + field: string; + /** + * The type of field. Should be the same type as defined in the mappings + */ + type: string; +} diff --git a/x-pack/solutions/chat/packages/wci-common/src/integration_tools.test.ts b/x-pack/solutions/chat/packages/wci-common/src/integration_tools.test.ts new file mode 100644 index 000000000000..1a2d12b5e5e4 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/src/integration_tools.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildToolName, parseToolName, ToolNameAndIntegrationId } from './integration_tools'; + +describe('integration_tools', () => { + describe('buildToolName', () => { + it('should correctly concatenate toolName and integrationId', () => { + const input: ToolNameAndIntegrationId = { + toolName: 'searchTool', + integrationId: 'elastic-search-123', + }; + expect(buildToolName(input)).toBe('searchTool___elastic-search-123'); + }); + + it('should handle special characters in input', () => { + const input: ToolNameAndIntegrationId = { + toolName: 'special@#$!%', + integrationId: 'integration&^*()', + }; + expect(buildToolName(input)).toBe('special@#$!%___integration&^*()'); + }); + }); + + describe('parseToolName', () => { + it('should correctly parse a valid tool name', () => { + const fullToolName = 'searchTool___elastic-search-123'; + expect(parseToolName(fullToolName)).toEqual({ + toolName: 'searchTool', + integrationId: 'elastic-search-123', + }); + }); + + it('should throw an error for invalid tool name format', () => { + expect(() => parseToolName('invalidToolName')).toThrow( + 'Invalid tool name format : "invalidToolName"' + ); + }); + + it('should throw an error when there are too many separators', () => { + expect(() => parseToolName('tool___integration___extra')).toThrow( + 'Invalid tool name format : "tool___integration___extra"' + ); + }); + }); +}); diff --git a/x-pack/solutions/chat/packages/wci-common/src/integration_tools.ts b/x-pack/solutions/chat/packages/wci-common/src/integration_tools.ts new file mode 100644 index 000000000000..2a4c93111d11 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/src/integration_tools.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const TOOL_NAME_SEPARATOR = '___'; + +export interface ToolNameAndIntegrationId { + integrationId: string; + toolName: string; +} + +/** + * Generates a unique tool name based on the integrationId and base tool name. + * This is used by the orchestration layer to generate "uuids" for each integration/tool tuples. + */ +export const buildToolName = ({ integrationId, toolName }: ToolNameAndIntegrationId) => { + return `${toolName}${TOOL_NAME_SEPARATOR}${integrationId}`; +}; + +export const parseToolName = (fullToolName: string): ToolNameAndIntegrationId => { + const splits = fullToolName.split(TOOL_NAME_SEPARATOR); + if (splits.length !== 2) { + throw new Error(`Invalid tool name format : "${fullToolName}"`); + } + return { + toolName: splits[0], + integrationId: splits[1], + }; +}; diff --git a/x-pack/solutions/chat/packages/wci-common/src/integrations.ts b/x-pack/solutions/chat/packages/wci-common/src/integrations.ts new file mode 100644 index 000000000000..2fafeeaf0df0 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/src/integrations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType } from './constants'; + +export interface Integration { + id: string; + name: string; + type: IntegrationType; + description: string; + configuration: IntegrationConfiguration; + createdAt: string; + updatedAt: string; + createdBy: string; +} + +export type IntegrationConfiguration = Record; diff --git a/x-pack/solutions/chat/packages/wci-common/src/tool_calls.ts b/x-pack/solutions/chat/packages/wci-common/src/tool_calls.ts new file mode 100644 index 000000000000..76625aff8a18 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/src/tool_calls.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Represents a tool call that was requested by the assistant + */ +export interface ToolCall { + /** + * The id the of tool call + */ + toolCallId: string; + /** + * The complete tool name (containing the integrationId) + */ + toolName: string; + /** + * Arguments that were used to call the tool + */ + args: Record; +} diff --git a/x-pack/solutions/chat/packages/wci-common/tsconfig.json b/x-pack/solutions/chat/packages/wci-common/tsconfig.json new file mode 100644 index 000000000000..f2017abc0b91 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-common/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "index.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + ] +} diff --git a/x-pack/solutions/chat/packages/wci-server/README.md b/x-pack/solutions/chat/packages/wci-server/README.md new file mode 100644 index 000000000000..3ef0825859be --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/README.md @@ -0,0 +1,3 @@ +# @kbn/wci-server + +Contains server-side implementation for WorkChat integrations. Uses types from `@kbn/wci-common`. \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wci-server/index.ts b/x-pack/solutions/chat/packages/wci-server/index.ts new file mode 100644 index 000000000000..12d6fb52fe71 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + type McpTool, + type McpClient, + type McpProvider, + type McpClientFactoryFn, + toolResult, +} from './src/mcp'; +export { + getConnectToInternalServer, + getConnectToExternalServer, + createMcpServer, +} from './src/utils'; +export type { + IntegrationContext, + WorkChatIntegration, + WorkchatIntegrationDefinition, +} from './src/integration'; diff --git a/x-pack/solutions/chat/packages/wci-server/jest.config.js b/x-pack/solutions/chat/packages/wci-server/jest.config.js new file mode 100644 index 000000000000..0e576b0c1f45 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/packages/wci-server'], +}; diff --git a/x-pack/solutions/chat/packages/wci-server/kibana.jsonc b/x-pack/solutions/chat/packages/wci-server/kibana.jsonc new file mode 100644 index 000000000000..a1e801b7a337 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/kibana.jsonc @@ -0,0 +1,9 @@ +{ + "type": "shared-server", + "id": "@kbn/wci-server", + "owner": [ + "@elastic/search-kibana" + ], + "group": "chat", + "visibility": "private" +} diff --git a/x-pack/solutions/chat/packages/wci-server/package.json b/x-pack/solutions/chat/packages/wci-server/package.json new file mode 100644 index 000000000000..e77d1965ffdd --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/wci-server", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/x-pack/solutions/chat/packages/wci-server/src/integration.ts b/x-pack/solutions/chat/packages/wci-server/src/integration.ts new file mode 100644 index 000000000000..e455ca045ffe --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/integration.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MaybePromise } from '@kbn/utility-types'; +import type { KibanaRequest } from '@kbn/core/server'; +import type { IntegrationType, IntegrationConfiguration } from '@kbn/wci-common'; +import type { McpProvider } from './mcp'; +/** + * Represents the definition of a type of integration for WorkChat. + * + * This is the top level entity for integration, which is the source + * of all things related to this integration type, such as being + * able to create an actual integration instance. + */ +export interface WorkchatIntegrationDefinition< + T extends IntegrationConfiguration = IntegrationConfiguration +> { + /** + * Returns the type of integration. + */ + getType(): IntegrationType; + + /** + * Creates an integration instance based on the provided context + */ + createIntegration(context: IntegrationContext): MaybePromise; +} + +export interface IntegrationContext { + request: KibanaRequest; + description: string; + integrationId: string; + configuration: T; +} + +/** + * Represents an instance of an integration type, bound to a specific context + */ +export interface WorkChatIntegration { + /** connect to the MCP client */ + connect: McpProvider['connect']; +} diff --git a/x-pack/solutions/chat/packages/wci-server/src/mcp.ts b/x-pack/solutions/chat/packages/wci-server/src/mcp.ts new file mode 100644 index 000000000000..87ad8ad7ef9c --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/mcp.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z, ZodRawShape, ZodTypeAny } from '@kbn/zod'; +import type { Client as McpBaseClient } from '@modelcontextprotocol/sdk/client/index'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { MaybePromise } from '@kbn/utility-types'; + +export interface McpTool { + name: string; + description: string; + schema: RunInput; + execute: (args: z.objectOutputType) => MaybePromise; +} + +/** + * Wrapper on top of the MCP client implementation to avoid leaking internals + * and to control which APIs are supported. + */ +export type McpClient = Pick & { + /** + * Disconnect the client. Note that once disconnected, it can't + * be connected again. + */ + disconnect: () => Promise; +}; + +export type McpClientFactoryFn = () => MaybePromise; + +export interface McpProvider { + id: string; + connect: McpClientFactoryFn; + meta?: Record; +} + +/** + * Utility factory to generate MCP call tool results + */ +export const toolResult = { + text: (text: string): CallToolResult => { + return { + content: [ + { + type: 'text', + text, + }, + ], + }; + }, + error: (message: string): CallToolResult => { + return { + content: [ + { + type: 'text', + text: `Error during tool execution: ${message}`, + }, + ], + isError: true, + }; + }, +}; diff --git a/x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.test.ts b/x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.test.ts new file mode 100644 index 000000000000..750843ec2215 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { getConnectToExternalServer } from './create_external_client'; + +jest.mock('@modelcontextprotocol/sdk/client/index.js'); +jest.mock('@modelcontextprotocol/sdk/client/sse.js'); + +describe('getConnectToExternalServer', () => { + const mockUrl = 'http://test-server.com/mcp'; + + const setupMocks = () => { + const mockTransport = { + close: jest.fn(), + }; + + const mockClient = { + connect: jest.fn(), + close: jest.fn(), + }; + + (SSEClientTransport as jest.Mock).mockImplementation(() => mockTransport); + (Client as jest.Mock).mockImplementation(() => mockClient); + + return { mockTransport, mockClient }; + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a client and connect successfully', async () => { + const { mockTransport, mockClient } = setupMocks(); + + const connectFn = getConnectToExternalServer({ + serverUrl: mockUrl, + clientName: 'test-client', + }); + + await connectFn(); + + expect(SSEClientTransport).toHaveBeenCalledWith(expect.any(URL)); + expect(Client).toHaveBeenCalledWith( + { + name: 'test-client', + version: '1.0.0', + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + }, + } + ); + expect(mockClient.connect).toHaveBeenCalledWith(mockTransport); + }); + + it('should use correct URL when creating transport', async () => { + setupMocks(); + + const connectFn = getConnectToExternalServer({ + serverUrl: mockUrl, + }); + + await connectFn(); + + expect(SSEClientTransport).toHaveBeenCalledWith(new URL(mockUrl)); + }); + + it('should disconnect properly', async () => { + const { mockClient } = setupMocks(); + + const connectFn = getConnectToExternalServer({ + serverUrl: mockUrl, + }); + + const client = await connectFn(); + await client.disconnect(); + + expect(mockClient.close).toHaveBeenCalled(); + }); + + it('should throw error when connecting an already connected client', async () => { + setupMocks(); + + const connectFn = getConnectToExternalServer({ + serverUrl: mockUrl, + }); + + await connectFn(); + await expect(connectFn()).rejects.toThrow('Client already connected'); + }); + + it('should handle disconnection errors gracefully', async () => { + const { mockClient } = setupMocks(); + mockClient.close.mockRejectedValueOnce(new Error('Disconnect failed')); + + const connectFn = getConnectToExternalServer({ + serverUrl: mockUrl, + }); + + const client = await connectFn(); + await expect(client.disconnect()).rejects.toThrow('Disconnect failed'); + }); +}); diff --git a/x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.ts b/x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.ts new file mode 100644 index 000000000000..2cfb7122fe5d --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/utils/create_external_client.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import type { McpClient } from '../mcp'; + +export const getConnectToExternalServer = ({ + serverUrl, + clientName = 'unknown', +}: { + serverUrl: string; + clientName?: string; +}): (() => Promise) => { + let connected = false; + + return async function connect() { + if (connected) { + throw new Error('Client already connected'); + } + connected = true; + + const transport = new SSEClientTransport(new URL(serverUrl)); + + const client = new Client( + { + name: clientName, + version: '1.0.0', + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + }, + } + ); + + await client.connect(transport); + + const disconnect = async () => { + await client.close(); + }; + + return Object.assign(client, { + disconnect, + }); + }; +}; diff --git a/x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.test.ts b/x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.test.ts new file mode 100644 index 000000000000..1ea8bb800a42 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { getConnectToInternalServer } from './create_internal_client'; + +jest.mock('@modelcontextprotocol/sdk/inMemory.js'); +jest.mock('@modelcontextprotocol/sdk/client/index.js'); + +describe('getConnectToInternalServer', () => { + const createStubServer = (): jest.Mocked => { + return { + connect: jest.fn(), + close: jest.fn(), + } as unknown as jest.Mocked; + }; + + const createStubTransport = () => ({ + close: jest.fn(), + }); + + const setupMocks = () => { + const clientTransport = createStubTransport(); + const serverTransport = createStubTransport(); + const mockClient = { + connect: jest.fn(), + close: jest.fn(), + }; + + (InMemoryTransport.createLinkedPair as jest.Mock).mockReturnValue([ + clientTransport, + serverTransport, + ]); + (Client as jest.Mock).mockImplementation(() => mockClient); + + return { clientTransport, serverTransport, mockClient }; + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a client and connect successfully', async () => { + const server = createStubServer(); + const { clientTransport, serverTransport, mockClient } = setupMocks(); + + const connectFn = getConnectToInternalServer({ + server, + clientName: 'test-client', + }); + + await connectFn(); + + expect(Client).toHaveBeenCalledWith({ + name: 'test-client', + version: '1.0.0', + }); + expect(server.connect).toHaveBeenCalledWith(clientTransport); + expect(mockClient.connect).toHaveBeenCalledWith(serverTransport); + }); + + it('should disconnect properly', async () => { + const server = createStubServer(); + const { mockClient } = setupMocks(); + + const connectFn = getConnectToInternalServer({ + server, + }); + + const client = await connectFn(); + await client.disconnect(); + + expect(mockClient.close).toHaveBeenCalled(); + expect(server.close).toHaveBeenCalled(); + }); + + it('should throw error when connecting an already connected client', async () => { + const server = createStubServer(); + setupMocks(); + + const connectFn = getConnectToInternalServer({ + server, + }); + + await connectFn(); + await expect(connectFn()).rejects.toThrow('Client already connected'); + }); + + it('should handle disconnection errors gracefully', async () => { + const server = createStubServer(); + const { mockClient } = setupMocks(); + mockClient.close.mockRejectedValueOnce(new Error('Disconnect failed')); + + const connectFn = getConnectToInternalServer({ + server, + }); + + const client = await connectFn(); + await expect(client.disconnect()).rejects.toThrow('Disconnect failed'); + }); +}); diff --git a/x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.ts b/x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.ts new file mode 100644 index 000000000000..f90049e90aba --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/utils/create_internal_client.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { McpClient } from '../mcp'; + +/** + * Returns a {@link McpProviderFn} that will run the provided server in memory + * and connect to it. + */ +export const getConnectToInternalServer = ({ + server, + clientName = 'unknown', +}: { + server: McpServer; + clientName?: string; +}): (() => Promise) => { + let connected = false; + + return async function connect() { + if (connected) { + throw new Error('Client already connected'); + } + connected = true; + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ + name: clientName, + version: '1.0.0', + }); + + await server.connect(clientTransport); + await client.connect(serverTransport); + + const disconnect = async () => { + await client.close(); + await server.close(); + }; + + return Object.assign(client, { + disconnect, + }); + }; +}; diff --git a/x-pack/solutions/chat/packages/wci-server/src/utils/create_mcp_server.ts b/x-pack/solutions/chat/packages/wci-server/src/utils/create_mcp_server.ts new file mode 100644 index 000000000000..a38e9175194d --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/utils/create_mcp_server.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { McpTool } from '../mcp'; + +export const createMcpServer = ({ + name, + version = '1.0.0', + tools, +}: { + name: string; + version?: string; + tools: McpTool[]; +}): McpServer => { + const server = new McpServer({ + name, + version, + }); + + tools.forEach((tool) => { + server.tool(tool.name, tool.description, tool.schema, async (params) => { + return tool.execute(params); + }); + }); + + return server; +}; diff --git a/x-pack/solutions/chat/packages/wci-server/src/utils/index.ts b/x-pack/solutions/chat/packages/wci-server/src/utils/index.ts new file mode 100644 index 000000000000..647b4d3d9958 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/src/utils/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getConnectToInternalServer } from './create_internal_client'; +export { getConnectToExternalServer } from './create_external_client'; +export { createMcpServer } from './create_mcp_server'; diff --git a/x-pack/solutions/chat/packages/wci-server/tsconfig.json b/x-pack/solutions/chat/packages/wci-server/tsconfig.json new file mode 100644 index 000000000000..b98d1700ef33 --- /dev/null +++ b/x-pack/solutions/chat/packages/wci-server/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "index.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/wci-common", + "@kbn/utility-types", + "@kbn/core", + "@kbn/zod" + ] +} diff --git a/x-pack/solutions/chat/plugins/serverless_chat/jest.config.dev.js b/x-pack/solutions/chat/plugins/serverless_chat/jest.config.dev.js index 0e7e2e974a3f..6dce1abe9b21 100644 --- a/x-pack/solutions/chat/plugins/serverless_chat/jest.config.dev.js +++ b/x-pack/solutions/chat/plugins/serverless_chat/jest.config.dev.js @@ -9,8 +9,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../../../', - projects: [ - '/x-pack/solutions/chat/plugins/chat_serverless/server/*/jest.config.js', - '/x-pack/solutions/chat/plugins/chat_serverless/public/*/jest.config.js', - ], + roots: ['/x-pack/solutions/chat/plugins/serverless_chat'], }; diff --git a/x-pack/solutions/chat/plugins/serverless_chat/public/jest.config.js b/x-pack/solutions/chat/plugins/serverless_chat/public/jest.config.js deleted file mode 100644 index b1e6452c0610..000000000000 --- a/x-pack/solutions/chat/plugins/serverless_chat/public/jest.config.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - preset: '@kbn/test', - rootDir: '../../../../../..', - /** all nested directories have their own Jest config file */ - testMatch: [ - '/x-pack/solutions/chat/plugins/serverless_chat/public/**/*.test.{js,mjs,ts,tsx}', - ], - roots: ['/x-pack/solutions/chat/plugins/serverless_chat/public'], - coverageDirectory: - '/target/kibana-coverage/jest/x-pack/solutions/chat/plugins/serverless_chat/public', - coverageReporters: ['text', 'html'], - collectCoverageFrom: [ - '/x-pack/solutions/chat/plugins/serverless_chat/public/**/*.{ts,tsx}', - '!/x-pack/solutions/chat/plugins/serverless_chat/public/*.test.{ts,tsx}', - '!/x-pack/solutions/chat/plugins/serverless_chat/public/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', - '!/x-pack/solutions/chat/plugins/serverless_chat/public/*mock*.{ts,tsx}', - '!/x-pack/solutions/chat/plugins/serverless_chat/public/*.test.{ts,tsx}', - '!/x-pack/solutions/chat/plugins/serverless_chat/public/*.d.ts', - '!/x-pack/solutions/chat/plugins/serverless_chat/public/*.config.ts', - '!/x-pack/solutions/chat/plugins/serverless_chat/public/index.{js,ts,tsx}', - ], -}; diff --git a/x-pack/solutions/chat/plugins/serverless_chat/public/navigation_tree.ts b/x-pack/solutions/chat/plugins/serverless_chat/public/navigation_tree.ts new file mode 100644 index 000000000000..39fd27eee11d --- /dev/null +++ b/x-pack/solutions/chat/plugins/serverless_chat/public/navigation_tree.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser'; + +export const createNavigationTree = (): NavigationTreeDefinition => { + return { + body: [ + { type: 'recentlyAccessed' }, + { + type: 'navGroup', + id: 'workchat_project_nav', + title: 'Workchat', + icon: 'logoElasticsearch', + defaultIsCollapsed: false, + isCollapsible: false, + breadcrumbStatus: 'hidden', + children: [ + { + link: 'workchat', + title: 'Home', + }, + { + link: 'workchat:agents', + }, + { + link: 'workchat:integrations', + }, + ], + }, + ], + footer: [ + { + type: 'navItem', + id: 'devTools', + title: i18n.translate('xpack.serverlessObservability.nav.devTools', { + defaultMessage: 'Developer tools', + }), + link: 'dev_tools', + icon: 'editorCodeBlock', + }, + { + type: 'navGroup', + id: 'project_settings_project_nav', + title: i18n.translate('xpack.serverlessObservability.nav.projectSettings', { + defaultMessage: 'Project settings', + }), + icon: 'gear', + breadcrumbStatus: 'hidden', + children: [ + { + id: 'management', + title: i18n.translate('xpack.serverlessObservability.nav.mngt', { + defaultMessage: 'Management', + }), + spaceBefore: null, + renderAs: 'panelOpener', + children: [ + { + title: i18n.translate('xpack.serverlessObservability.nav.mngt.data', { + defaultMessage: 'Data', + }), + breadcrumbStatus: 'hidden', + children: [ + { link: 'management:index_management', breadcrumbStatus: 'hidden' }, + { link: 'management:transform', breadcrumbStatus: 'hidden' }, + { link: 'management:ingest_pipelines', breadcrumbStatus: 'hidden' }, + { link: 'management:dataViews', breadcrumbStatus: 'hidden' }, + { link: 'management:jobsListLink', breadcrumbStatus: 'hidden' }, + { link: 'management:pipelines', breadcrumbStatus: 'hidden' }, + { link: 'management:data_quality', breadcrumbStatus: 'hidden' }, + { link: 'management:data_usage', breadcrumbStatus: 'hidden' }, + ], + }, + { + title: i18n.translate('xpack.serverlessObservability.nav.mngt.access', { + defaultMessage: 'Access', + }), + breadcrumbStatus: 'hidden', + children: [{ link: 'management:api_keys', breadcrumbStatus: 'hidden' }], + }, + { + title: i18n.translate('xpack.serverlessObservability.nav.mngt.alertsAndInsights', { + defaultMessage: 'Alerts and insights', + }), + breadcrumbStatus: 'hidden', + children: [ + { link: 'management:triggersActionsConnectors', breadcrumbStatus: 'hidden' }, + { link: 'management:maintenanceWindows', breadcrumbStatus: 'hidden' }, + ], + }, + { + title: i18n.translate('xpack.serverlessObservability.nav.mngt.content', { + defaultMessage: 'Content', + }), + breadcrumbStatus: 'hidden', + children: [ + { link: 'management:spaces', breadcrumbStatus: 'hidden' }, + { link: 'management:objects', breadcrumbStatus: 'hidden' }, + { link: 'management:filesManagement', breadcrumbStatus: 'hidden' }, + { link: 'management:reporting', breadcrumbStatus: 'hidden' }, + { link: 'management:tags', breadcrumbStatus: 'hidden' }, + ], + }, + { + title: i18n.translate('xpack.serverlessObservability.nav.mngt.other', { + defaultMessage: 'Other', + }), + breadcrumbStatus: 'hidden', + children: [ + { link: 'management:settings', breadcrumbStatus: 'hidden' }, + { + link: 'management:observabilityAiAssistantManagement', + breadcrumbStatus: 'hidden', + }, + ], + }, + ], + }, + { + id: 'cloudLinkUserAndRoles', + cloudLink: 'userAndRoles', + }, + { + id: 'cloudLinkBilling', + cloudLink: 'billingAndSub', + }, + ], + }, + ], + }; +}; diff --git a/x-pack/solutions/chat/plugins/serverless_chat/public/plugin.ts b/x-pack/solutions/chat/plugins/serverless_chat/public/plugin.ts index 8e99c88665a2..86b2513902d1 100644 --- a/x-pack/solutions/chat/plugins/serverless_chat/public/plugin.ts +++ b/x-pack/solutions/chat/plugins/serverless_chat/public/plugin.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { of } from 'rxjs'; import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import type { @@ -13,6 +14,7 @@ import type { ChatServerlessPluginSetupDeps, ChatServerlessPluginStartDeps, } from './types'; +import { createNavigationTree } from './navigation_tree'; export class ChatServerlessPlugin implements @@ -34,8 +36,12 @@ export class ChatServerlessPlugin public start( core: CoreStart, - startDeps: ChatServerlessPluginStartDeps + { serverless }: ChatServerlessPluginStartDeps ): ChatServerlessPluginStart { + const navigationTree$ = of(createNavigationTree()); + + serverless.initNavigation('chat', navigationTree$); + return {}; } diff --git a/x-pack/solutions/chat/plugins/serverless_chat/tsconfig.json b/x-pack/solutions/chat/plugins/serverless_chat/tsconfig.json index 4ad7cf6fc5b2..c4aa105a9e96 100644 --- a/x-pack/solutions/chat/plugins/serverless_chat/tsconfig.json +++ b/x-pack/solutions/chat/plugins/serverless_chat/tsconfig.json @@ -19,5 +19,7 @@ "@kbn/serverless", "@kbn/config-schema", "@kbn/serverless-chat-settings", + "@kbn/i18n", + "@kbn/core-chrome-browser", ] } diff --git a/x-pack/solutions/chat/plugins/wci-external-server/README.md b/x-pack/solutions/chat/plugins/wci-external-server/README.md new file mode 100644 index 000000000000..35039d969f5b --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/README.md @@ -0,0 +1,3 @@ +# WorkChat External Server Integration + +WorkChat External Server integration plugin that provides functionality to connect to external servers for the WorkChat application. \ No newline at end of file diff --git a/x-pack/solutions/chat/plugins/wci-external-server/common/index.ts b/x-pack/solutions/chat/plugins/wci-external-server/common/index.ts new file mode 100644 index 000000000000..c8a5e9309dc7 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const PLUGIN_ID = 'wciExternalServer'; +export const PLUGIN_NAME = 'wciExternalServer'; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/common/types.ts b/x-pack/solutions/chat/plugins/wci-external-server/common/types.ts new file mode 100644 index 000000000000..3c533a1decae --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/common/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationConfiguration } from '@kbn/wci-common'; + +export interface WCIExternalServerConfiguration extends IntegrationConfiguration { + url: string; + description: string; +} diff --git a/x-pack/solutions/chat/plugins/wci-external-server/jest.config.js b/x-pack/solutions/chat/plugins/wci-external-server/jest.config.js new file mode 100644 index 000000000000..84dc57d96ee2 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/plugins/wci-external-server'], + testMatch: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/kibana.jsonc b/x-pack/solutions/chat/plugins/wci-external-server/kibana.jsonc new file mode 100644 index 000000000000..342fbcbfaaf7 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/kibana.jsonc @@ -0,0 +1,17 @@ +{ + "type": "plugin", + "id": "@kbn/wci-external-server", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private", + "description": "WorkChat External Server Integration plugin", + "plugin": { + "id": "wciExternalServer", + "server": true, + "browser": true, + "configPath": ["xpack", "wciExternalServer"], + "requiredPlugins": ["workchatApp"], + "optionalPlugins": [], + "requiredBundles": [] + } +} diff --git a/x-pack/solutions/chat/plugins/wci-external-server/package.json b/x-pack/solutions/chat/plugins/wci-external-server/package.json new file mode 100644 index 000000000000..3a77d9fa169e --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/wci-external-server", + "version": "1.0.0", + "license": "Elastic License 2.0", + "private": true, + "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../../../../scripts/plugin_helpers", + "kbn": "node ../../../../../scripts/kbn" + } +} \ No newline at end of file diff --git a/x-pack/solutions/chat/plugins/wci-external-server/public/index.ts b/x-pack/solutions/chat/plugins/wci-external-server/public/index.ts new file mode 100644 index 000000000000..933f79f45af8 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/public/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { WCIExternalServerPlugin } from './plugin'; +import { + WCIExternalServerPluginSetup, + WCIExternalServerPluginStart, + WCIExternalServerPluginStartDependencies, +} from './types'; + +export const plugin: PluginInitializer< + WCIExternalServerPluginSetup, + WCIExternalServerPluginStart, + {}, + WCIExternalServerPluginStartDependencies +> = (context: PluginInitializerContext) => { + return new WCIExternalServerPlugin(context); +}; + +export type { WCIExternalServerPluginSetup, WCIExternalServerPluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/public/integration/configuration.tsx b/x-pack/solutions/chat/plugins/wci-external-server/public/integration/configuration.tsx new file mode 100644 index 000000000000..ccd9938a9713 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/public/integration/configuration.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Controller } from 'react-hook-form'; +import { + EuiDescribedFormGroup, + EuiFieldText, + EuiFormRow, + EuiSpacer, + EuiTextArea, +} from '@elastic/eui'; +import type { IntegrationConfigurationFormProps } from '@kbn/wci-browser'; + +export const ExternalServerConfigurationForm: React.FC = ({ + form, +}) => { + const { control } = form; + + return ( + External Server Configuration} + description="Configure the external server details" + > + + ( + + )} + /> + + + + + + ( + + )} + /> + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/public/integration/external_server_integration.ts b/x-pack/solutions/chat/plugins/wci-external-server/public/integration/external_server_integration.ts new file mode 100644 index 000000000000..8551b30cd3a1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/public/integration/external_server_integration.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType } from '@kbn/wci-common'; +import type { IntegrationComponentDescriptor } from '@kbn/wci-browser'; +import { ExternalServerTool } from './tool'; +import { ExternalServerConfigurationForm } from './configuration'; + +export function getExternalServerIntegrationComponents(): IntegrationComponentDescriptor { + return { + getType: () => IntegrationType.external_server, + getToolCallComponent: (toolName) => ExternalServerTool, + getConfigurationForm: () => ExternalServerConfigurationForm, + }; +} diff --git a/x-pack/solutions/chat/plugins/wci-external-server/public/integration/tool.tsx b/x-pack/solutions/chat/plugins/wci-external-server/public/integration/tool.tsx new file mode 100644 index 000000000000..feed3548c26c --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/public/integration/tool.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { IntegrationToolComponentProps } from '@kbn/wci-browser'; + +const bold = css` + font-weight: bold; +`; + +const italic = css` + font-style: italic; +`; + +export const ExternalServerTool: React.FC = ({ + toolCall, + toolResult, +}) => { + const toolNode = ( + + {toolCall.toolName} + + ); + const argsNode = ( + + {JSON.stringify(toolCall.args)} + + ); + + if (toolResult) { + return ( + + + + ); + } else { + return ( + + + + ); + } +}; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/public/plugin.tsx b/x-pack/solutions/chat/plugins/wci-external-server/public/plugin.tsx new file mode 100644 index 000000000000..405de541435a --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/public/plugin.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import type { + WCIExternalServerPluginSetup, + WCIExternalServerPluginStart, + WCIExternalServerPluginSetupDependencies, + WCIExternalServerPluginStartDependencies, +} from './types'; +import { getExternalServerIntegrationComponents } from './integration/external_server_integration'; + +export class WCIExternalServerPlugin + implements + Plugin< + WCIExternalServerPluginSetup, + WCIExternalServerPluginStart, + WCIExternalServerPluginSetupDependencies, + WCIExternalServerPluginStartDependencies + > +{ + constructor(context: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + { workchatApp }: WCIExternalServerPluginSetupDependencies + ): WCIExternalServerPluginSetup { + workchatApp.integrations.register(getExternalServerIntegrationComponents()); + + return {}; + } + + public start( + coreStart: CoreStart, + pluginsStart: WCIExternalServerPluginStartDependencies + ): WCIExternalServerPluginStart { + return {}; + } + + public stop() {} +} diff --git a/x-pack/solutions/chat/plugins/wci-external-server/public/types.ts b/x-pack/solutions/chat/plugins/wci-external-server/public/types.ts new file mode 100644 index 000000000000..43f570cca810 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/public/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { WorkChatAppPluginSetup } from '@kbn/workchat-app/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIExternalServerPluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIExternalServerPluginStart {} + +export interface WCIExternalServerPluginSetupDependencies { + workchatApp: WorkChatAppPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIExternalServerPluginStartDependencies {} diff --git a/x-pack/solutions/chat/plugins/wci-external-server/server/config.ts b/x-pack/solutions/chat/plugins/wci-external-server/server/config.ts new file mode 100644 index 000000000000..8e10453f48b1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/server/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type WCIExternalServerConfig = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: {}, + schema: configSchema, +}; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/server/index.ts b/x-pack/solutions/chat/plugins/wci-external-server/server/index.ts new file mode 100644 index 000000000000..5be02dde069a --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { WCIExternalServerPlugin } from './plugin'; + +export { config } from './config'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new WCIExternalServerPlugin(initializerContext); +}; + +export type { WCIExternalServerPluginSetup, WCIExternalServerPluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/server/integration/external_server_integration.ts b/x-pack/solutions/chat/plugins/wci-external-server/server/integration/external_server_integration.ts new file mode 100644 index 000000000000..536d60e0ad66 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/server/integration/external_server_integration.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, Logger } from '@kbn/core/server'; +import { IntegrationType } from '@kbn/wci-common'; +import { + getConnectToExternalServer, + WorkchatIntegrationDefinition, + WorkChatIntegration, +} from '@kbn/wci-server'; +import { WCIExternalServerConfiguration } from '../../common/types'; + +export const getExternalServerIntegrationDefinition = ({ + core, + logger, +}: { + core: CoreSetup; + logger: Logger; +}): WorkchatIntegrationDefinition => { + return { + getType: () => IntegrationType.external_server, + createIntegration: async ({ configuration }): Promise => { + return { + connect: getConnectToExternalServer({ + serverUrl: configuration.url, + }), + }; + }, + }; +}; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/server/integration/index.ts b/x-pack/solutions/chat/plugins/wci-external-server/server/integration/index.ts new file mode 100644 index 000000000000..f69a68ba0726 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/server/integration/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getExternalServerIntegrationDefinition } from './external_server_integration'; diff --git a/x-pack/solutions/chat/plugins/wci-external-server/server/plugin.ts b/x-pack/solutions/chat/plugins/wci-external-server/server/plugin.ts new file mode 100644 index 000000000000..de4da8c8f45e --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/server/plugin.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoreStart, + CoreSetup, + Plugin, + PluginInitializerContext, + Logger, +} from '@kbn/core/server'; +import type { + WCIExternalServerPluginStart, + WCIExternalServerPluginSetup, + WCIExternalServerPluginSetupDependencies, + WCIExternalServerPluginStartDependencies, +} from './types'; +import { getExternalServerIntegrationDefinition } from './integration'; + +export class WCIExternalServerPlugin + implements + Plugin< + WCIExternalServerPluginSetup, + WCIExternalServerPluginStart, + WCIExternalServerPluginSetupDependencies, + WCIExternalServerPluginStartDependencies + > +{ + private readonly logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + + public setup( + core: CoreSetup, + { workchatApp }: WCIExternalServerPluginSetupDependencies + ): WCIExternalServerPluginSetup { + workchatApp.integrations.register( + getExternalServerIntegrationDefinition({ + core, + logger: this.logger, + }) + ); + + return {}; + } + + public start( + core: CoreStart, + pluginsDependencies: WCIExternalServerPluginStartDependencies + ): WCIExternalServerPluginStart { + return {}; + } +} diff --git a/x-pack/solutions/chat/plugins/wci-external-server/server/types.ts b/x-pack/solutions/chat/plugins/wci-external-server/server/types.ts new file mode 100644 index 000000000000..92ce1c62ac1e --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/server/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { WorkChatAppPluginSetup } from '@kbn/workchat-app/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIExternalServerPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIExternalServerPluginStart {} + +export interface WCIExternalServerPluginSetupDependencies { + workchatApp: WorkChatAppPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIExternalServerPluginStartDependencies {} diff --git a/x-pack/solutions/chat/plugins/wci-external-server/tsconfig.json b/x-pack/solutions/chat/plugins/wci-external-server/tsconfig.json new file mode 100644 index 000000000000..0a0f2b080131 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-external-server/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../typings/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/config-schema", + "@kbn/wci-common", + "@kbn/wci-server", + "@kbn/workchat-app", + "@kbn/wci-browser", + "@kbn/i18n-react" + ], + "exclude": [ + "target/**/*" + ] +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/README.md b/x-pack/solutions/chat/plugins/wci-index-source/README.md new file mode 100644 index 000000000000..ef154fe7f5b6 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/README.md @@ -0,0 +1,3 @@ +# WorkChat Index Source Integration + +WorkChat Index Source integration plugin that provides functionality to search Elasticsearch indices for the WorkChat application. \ No newline at end of file diff --git a/x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts b/x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts new file mode 100644 index 000000000000..02e53ff34690 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IndexSourceDefinition } from '@kbn/wci-common'; + +export interface GenerateConfigurationResponse { + definition: IndexSourceDefinition; +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/common/index.ts b/x-pack/solutions/chat/plugins/wci-index-source/common/index.ts new file mode 100644 index 000000000000..26b411c63d77 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const PLUGIN_ID = 'wciIndexSource'; +export const PLUGIN_NAME = 'wciIndexSource'; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/common/types.ts b/x-pack/solutions/chat/plugins/wci-index-source/common/types.ts new file mode 100644 index 000000000000..085963e55e30 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/common/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationConfiguration } from '@kbn/wci-common'; + +export interface WCIIndexSourceConfiguration extends IntegrationConfiguration { + index: string; + description: string; + fields: { + filterFields: WCIIndexSourceFilterField[]; + contextFields: WCIIndexSourceContextField[]; + }; + queryTemplate: string; +} + +export interface WCIIndexSourceFilterField { + field: string; + type: string; + getValues: boolean; + description: string; +} + +export interface WCIIndexSourceContextField { + field: string; + description: string; + type?: 'semantic'; +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/jest.config.js b/x-pack/solutions/chat/plugins/wci-index-source/jest.config.js new file mode 100644 index 000000000000..33c86e177c31 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/plugins/wci-index-source'], + testMatch: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/kibana.jsonc b/x-pack/solutions/chat/plugins/wci-index-source/kibana.jsonc new file mode 100644 index 000000000000..fc20487267ce --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/kibana.jsonc @@ -0,0 +1,17 @@ +{ + "type": "plugin", + "id": "@kbn/wci-index-source", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private", + "description": "WorkChat Index Source Integration plugin", + "plugin": { + "id": "wciIndexSource", + "server": true, + "browser": true, + "configPath": ["xpack", "wciIndexSource"], + "requiredPlugins": ["workchatApp", "inference", "actions"], + "optionalPlugins": [], + "requiredBundles": ["kibanaReact"] + } +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/package.json b/x-pack/solutions/chat/plugins/wci-index-source/package.json new file mode 100644 index 000000000000..0b56e60f82f7 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/wci-index-source", + "version": "1.0.0", + "license": "Elastic License 2.0", + "private": true, + "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../../../../scripts/plugin_helpers", + "kbn": "node ../../../../../scripts/kbn" + } +} \ No newline at end of file diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/hooks/use_generate_schema.ts b/x-pack/solutions/chat/plugins/wci-index-source/public/hooks/use_generate_schema.ts new file mode 100644 index 000000000000..412b40e18d55 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/hooks/use_generate_schema.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation } from '@tanstack/react-query'; +import type { CoreStart } from '@kbn/core/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { GenerateConfigurationResponse } from '../../common/http_api/configuration'; + +export const useGenerateSchema = () => { + const { + services: { http, notifications }, + } = useKibana(); + const { mutate, isLoading } = useMutation({ + mutationFn: async ({ indexName }: { indexName: string }) => { + const response = await http.post( + '/internal/wci-index-source/configuration/generate', + { + body: JSON.stringify({ indexName }), + } + ); + return response.definition; + }, + onError: (err: any) => { + notifications.toasts.addError(err, { title: 'Error generating schema' }); + }, + }); + + return { + isLoading, + generateSchema: mutate, + }; +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/index.ts b/x-pack/solutions/chat/plugins/wci-index-source/public/index.ts new file mode 100644 index 000000000000..31362b27130a --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { WCIIndexSourcePlugin } from './plugin'; +import { + WCIIndexSourcePluginSetup, + WCIIndexSourcePluginStart, + WCIIndexSourcePluginStartDependencies, +} from './types'; + +export const plugin: PluginInitializer< + WCIIndexSourcePluginSetup, + WCIIndexSourcePluginStart, + {}, + WCIIndexSourcePluginStartDependencies +> = (context: PluginInitializerContext) => { + return new WCIIndexSourcePlugin(context); +}; + +export type { WCIIndexSourcePluginSetup, WCIIndexSourcePluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx new file mode 100644 index 000000000000..b2af23682862 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx @@ -0,0 +1,334 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { useFieldArray, Controller } from 'react-hook-form'; +import { + EuiTextArea, + EuiFormRow, + EuiDescribedFormGroup, + EuiFieldText, + EuiSelect, + EuiSwitch, + EuiButton, + EuiButtonIcon, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiButtonEmpty, +} from '@elastic/eui'; +import type { IndexSourceDefinition } from '@kbn/wci-common'; +import { IntegrationConfigurationFormProps } from '@kbn/wci-browser'; +import type { WCIIndexSourceFilterField, WCIIndexSourceContextField } from '../../common/types'; +import { useGenerateSchema } from '../hooks/use_generate_schema'; + +export const IndexSourceConfigurationForm: React.FC = ({ + form, +}) => { + const { control, setValue } = form; + const filterFieldsArray = useFieldArray({ + control, + name: 'configuration.fields.filterFields', + }); + + const contextFieldsArray = useFieldArray({ + control, + name: 'configuration.fields.contextFields', + }); + + const fieldTypeOptions = [ + { value: 'keyword', text: 'Keyword' }, + { value: 'text', text: 'Text' }, + { value: 'date', text: 'Date' }, + { value: 'number', text: 'Number' }, + { value: 'boolean', text: 'Boolean' }, + ]; + + const { generateSchema } = useGenerateSchema(); + + const onSchemaGenerated = useCallback( + (definition: IndexSourceDefinition) => { + setValue('configuration.description', definition.description); + setValue( + 'configuration.fields.filterFields', + definition.filterFields.map((field) => { + return { + field: field.field, + type: field.type, + description: field.description, + getValues: field.asEnum, + }; + }) + ); + + const queryClauses = definition.queryFields.map((field) => { + if (field.type === 'semantic_text') { + return { + semantic: { + field: field.field, + query: '{query}', + }, + }; + } else { + return { + match: { + [field.field]: '{query}', + }, + }; + } + }); + const queryTemplate = { + bool: { + should: queryClauses, + }, + }; + + setValue('configuration.queryTemplate', JSON.stringify(queryTemplate, undefined, 2)); + + setValue( + 'configuration.fields.contextFields', + definition.contentFields.map((field) => { + return { + field: field.field, + description: '', + type: field.type === 'semantic_text' ? 'semantic' : undefined, + }; + }) + ); + }, + [setValue] + ); + + return ( + <> + Index Source Configuration} + description="Configure the index source details" + > + + ( + { + generateSchema({ indexName: field.value }, { onSuccess: onSchemaGenerated }); + }} + > + Generate configuration + + } + /> + )} + /> + + + + ( + + )} + /> + + + + ( + + )} + /> + + + + Filter Fields} + description="Fields that can be used for filtering documents" + > + {filterFieldsArray.fields.length === 0 ? ( + + +

Add filter fields to allow filtering on specific document fields

+
+
+ ) : ( + filterFieldsArray.fields.map((filterField, index) => ( + + + + + } + /> + + + + + + } + /> + + + + + + ( + onChange(e.target.checked)} + {...rest} + /> + )} + /> + + + + + + filterFieldsArray.remove(index)} + /> + + + + + + ( + + )} + /> + + + )) + )} + + + + filterFieldsArray.append({ + field: '', + type: 'keyword', + getValues: true, + description: '', + }) + } + > + Add filter field + + +
+ + Context Fields} + description="Fields that provide additional context for the AI" + > + {contextFieldsArray.fields.length === 0 ? ( + + +

Add context fields to provide additional information for the AI

+
+
+ ) : ( + contextFieldsArray.fields.map((contextField, index) => ( + + + + + } + /> + + + + + + ( + onChange(e.target.checked ? 'semantic' : undefined)} + {...rest} + /> + )} + /> + + + + + + contextFieldsArray.remove(index)} + /> + + + + + )) + )} + + + contextFieldsArray.append({ field: '', description: '' })} + > + Add context field + + +
+ + ); +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/integration/index_source_integration.ts b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/index_source_integration.ts new file mode 100644 index 000000000000..d82c8e27e004 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/index_source_integration.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType } from '@kbn/wci-common'; +import type { IntegrationComponentDescriptor } from '@kbn/wci-browser'; +import { IndexSourceTool } from './tool'; +import { IndexSourceConfigurationForm } from './configuration'; + +export function indexSourceIntegrationComponents(): IntegrationComponentDescriptor { + return { + getType: () => IntegrationType.index_source, + getToolCallComponent: (toolName) => IndexSourceTool, + getConfigurationForm: () => IndexSourceConfigurationForm, + }; +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/integration/tool.tsx b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/tool.tsx new file mode 100644 index 000000000000..68beef27f5f5 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/tool.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { css } from '@emotion/css'; +import { EuiText, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { IntegrationToolComponentProps } from '@kbn/wci-browser'; + +const bold = css` + font-weight: bold; + font-style: normal; +`; + +const italic = css` + font-style: italic; +`; + +export const IndexSourceTool: React.FC = ({ + integration, + toolCall, + toolResult, +}) => { + const integrationNode = ( + + {integration.name} + + ); + const argsNode = ( + + "{toolCall.args.query}" + + ); + + if (toolResult) { + return ( + + + + ); + } else { + return ( + + + + ); + } +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/plugin.tsx b/x-pack/solutions/chat/plugins/wci-index-source/public/plugin.tsx new file mode 100644 index 000000000000..59a32bdfa988 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/plugin.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import type { + WCIIndexSourcePluginSetup, + WCIIndexSourcePluginStart, + WCIIndexSourcePluginSetupDependencies, + WCIIndexSourcePluginStartDependencies, +} from './types'; +import { indexSourceIntegrationComponents } from './integration/index_source_integration'; + +export class WCIIndexSourcePlugin + implements + Plugin< + WCIIndexSourcePluginSetup, + WCIIndexSourcePluginStart, + WCIIndexSourcePluginSetupDependencies, + WCIIndexSourcePluginStartDependencies + > +{ + constructor(context: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + { workchatApp }: WCIIndexSourcePluginSetupDependencies + ): WCIIndexSourcePluginSetup { + workchatApp.integrations.register(indexSourceIntegrationComponents()); + + return {}; + } + + public start( + coreStart: CoreStart, + pluginsStart: WCIIndexSourcePluginStartDependencies + ): WCIIndexSourcePluginStart { + return {}; + } + + public stop() {} +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/types.ts b/x-pack/solutions/chat/plugins/wci-index-source/public/types.ts new file mode 100644 index 000000000000..a1af2439d423 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { WorkChatAppPluginSetup } from '@kbn/workchat-app/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIIndexSourcePluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIIndexSourcePluginStart {} + +export interface WCIIndexSourcePluginSetupDependencies { + workchatApp: WorkChatAppPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIIndexSourcePluginStartDependencies {} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/config.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/config.ts new file mode 100644 index 000000000000..6d744845466f --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type WCIIndexSourceConfig = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: {}, + schema: configSchema, +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/index.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/index.ts new file mode 100644 index 000000000000..af673ed5cd38 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { WCIIndexSourcePlugin } from './plugin'; + +export { config } from './config'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new WCIIndexSourcePlugin(initializerContext); +}; + +export type { WCIIndexSourcePluginSetup, WCIIndexSourcePluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/integration/index.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/integration/index.ts new file mode 100644 index 000000000000..25994c8eec92 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/integration/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getIndexSourceIntegrationDefinition } from './index_source_integration'; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/integration/index_source_integration.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/integration/index_source_integration.ts new file mode 100644 index 000000000000..1452056f4067 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/integration/index_source_integration.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, Logger } from '@kbn/core/server'; +import { IntegrationType } from '@kbn/wci-common'; +import { + getConnectToInternalServer, + type WorkchatIntegrationDefinition, + type WorkChatIntegration, +} from '@kbn/wci-server'; +import { createMcpServer } from './mcp_server'; +import { WCIIndexSourceConfiguration } from '../../common/types'; + +export const getIndexSourceIntegrationDefinition = ({ + core, + logger, +}: { + core: CoreSetup; + logger: Logger; +}): WorkchatIntegrationDefinition => { + return { + getType: () => IntegrationType.index_source, + createIntegration: async ({ + request, + description, + configuration, + }): Promise => { + const [coreStart] = await core.getStartServices(); + const elasticsearchClient = coreStart.elasticsearch.client.asScoped(request).asCurrentUser; + + const mcpServer = await createMcpServer({ + configuration, + description, + elasticsearchClient, + logger, + }); + + return { + connect: getConnectToInternalServer({ server: mcpServer }), + }; + }, + }; +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/integration/mcp_server.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/integration/mcp_server.ts new file mode 100644 index 000000000000..33c2ed97353f --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/integration/mcp_server.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { + getFieldsTopValues, + generateSearchSchema, + createFilterClauses, + hitToContent, + type SearchFilter, +} from '@kbn/wc-integration-utils'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { WCIIndexSourceConfiguration } from '../../common/types'; + +type SearchResult = any; // TODO: fix this + +export async function createMcpServer({ + configuration, + description, + elasticsearchClient, + logger, +}: { + configuration: WCIIndexSourceConfiguration; + description: string; + elasticsearchClient: ElasticsearchClient; + logger: Logger; +}): Promise { + const server = new McpServer({ + name: 'wci-index-source', + version: '1.0.0', + }); + + const { index, fields } = configuration; + + const searchFilters = fields.filterFields.map((field) => ({ + field: field.field, + description: field.description, + values: [], + // filters are only a subset of field types + type: field.type as SearchFilter['type'], + })); + + const aggFields = fields.filterFields + .filter((field) => field.getValues) + .map((field) => field.field); + if (aggFields.length > 0) { + const topValues = await getFieldsTopValues({ + indexName: index, + fieldNames: aggFields.map((field) => field), + esClient: elasticsearchClient, + }); + + searchFilters.forEach((fieldValue) => { + fieldValue.values = topValues[fieldValue.field]; + }); + } + + const toolSchema = generateSearchSchema({ filters: searchFilters }); + + server.tool('search', description, toolSchema, async ({ query, ...filterValues }) => { + logger.info( + () => + `Searching for "${query}" in index "${index} with filters: ${JSON.stringify(filterValues)}"` + ); + + let result = null; + + const esFilters = createFilterClauses({ filters: searchFilters, values: filterValues }); + const contentFields = fields.contextFields.map((field) => field.field); + + try { + const queryClause = + query && configuration.queryTemplate + ? JSON.parse(configuration.queryTemplate.replace('{query}', query)) + : { + match_all: {}, + }; + + result = await elasticsearchClient.search({ + index, + query: { + bool: { + must: [queryClause], + ...(esFilters.length > 0 ? { filter: esFilters } : {}), + }, + }, + _source: { + includes: contentFields, + }, + highlight: { + fields: fields.contextFields.reduce((acc, field) => { + if (field.type === 'semantic') { + acc[field.field] = { + type: 'semantic', + }; + } + return acc; + }, {} as Record), + }, + }); + } catch (error) { + logger.error(`Failed to search: ${error}`); + } + + if (!result) { + return { + content: [], + }; + } + + logger.info(`Found ${result.hits.hits.length} hits`); + + const documents = result.hits.hits.map((hit) => { + return { + type: 'text' as const, + text: JSON.stringify(hitToContent({ hit, fields: contentFields })), + }; + }); + + return { + content: documents, + }; + }); + + return server; +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/plugin.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/plugin.ts new file mode 100644 index 000000000000..1b1d20fed81f --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/plugin.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoreStart, + CoreSetup, + Plugin, + PluginInitializerContext, + Logger, +} from '@kbn/core/server'; +import type { + WCIIndexSourcePluginStart, + WCIIndexSourcePluginSetup, + WCIIndexSourcePluginSetupDependencies, + WCIIndexSourcePluginStartDependencies, +} from './types'; +import { registerRoutes } from './routes'; +import { getIndexSourceIntegrationDefinition } from './integration'; + +export class WCIIndexSourcePlugin + implements + Plugin< + WCIIndexSourcePluginSetup, + WCIIndexSourcePluginStart, + WCIIndexSourcePluginSetupDependencies, + WCIIndexSourcePluginStartDependencies + > +{ + private readonly logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + + public setup( + core: CoreSetup, + { workchatApp }: WCIIndexSourcePluginSetupDependencies + ): WCIIndexSourcePluginSetup { + const router = core.http.createRouter(); + registerRoutes({ + core, + logger: this.logger, + router, + }); + + workchatApp.integrations.register( + getIndexSourceIntegrationDefinition({ + core, + logger: this.logger, + }) + ); + + return {}; + } + + public start( + core: CoreStart, + pluginsDependencies: WCIIndexSourcePluginStartDependencies + ): WCIIndexSourcePluginStart { + return {}; + } +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts new file mode 100644 index 000000000000..442737b344ce --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { apiCapabilities } from '@kbn/workchat-app/common/features'; +import { buildSchema } from '@kbn/wc-index-schema-builder'; +import { getConnectorList, getDefaultConnector } from '@kbn/wc-genai-utils'; +import type { GenerateConfigurationResponse } from '../../common/http_api/configuration'; +import type { RouteDependencies } from './types'; + +export const registerConfigurationRoutes = ({ router, core, logger }: RouteDependencies) => { + // generate the index integration schema for a given index + router.post( + { + path: '/internal/wci-index-source/configuration/generate', + security: { + authz: { + requiredPrivileges: [apiCapabilities.manageWorkchat], + }, + }, + validate: { + body: schema.object({ + indexName: schema.string(), + }), + }, + }, + async (ctx, request, res) => { + try { + const [, { actions, inference }] = await core.getStartServices(); + const { elasticsearch } = await ctx.core; + + const connectors = await getConnectorList({ actions, request }); + const connector = getDefaultConnector({ connectors }); + + const chatModel = await inference.getChatModel({ + request, + connectorId: connector.connectorId, + chatModelOptions: {}, + }); + + const { indexName } = request.body; + const definition = await buildSchema({ + indexName, + chatModel, + esClient: elasticsearch.client.asCurrentUser, + logger, + }); + + return res.ok({ + body: { + definition, + }, + }); + } catch (e) { + logger.error(e); + throw e; + } + } + ); +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/routes/index.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/index.ts new file mode 100644 index 000000000000..147987370d43 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RouteDependencies } from './types'; +import { registerConfigurationRoutes } from './configuration'; + +export const registerRoutes = (dependencies: RouteDependencies) => { + registerConfigurationRoutes(dependencies); +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/routes/types.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/types.ts new file mode 100644 index 000000000000..0758baaf9574 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IRouter, Logger, CoreSetup } from '@kbn/core/server'; +import type { WCIIndexSourcePluginStartDependencies } from '../types'; + +export interface RouteDependencies { + core: CoreSetup; + router: IRouter; + logger: Logger; +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/types.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/types.ts new file mode 100644 index 000000000000..e7a3281d1ab1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InferenceServerStart } from '@kbn/inference-plugin/server'; +import type { WorkChatAppPluginSetup } from '@kbn/workchat-app/server'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIIndexSourcePluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCIIndexSourcePluginStart {} + +export interface WCIIndexSourcePluginSetupDependencies { + workchatApp: WorkChatAppPluginSetup; +} + +export interface WCIIndexSourcePluginStartDependencies { + inference: InferenceServerStart; + actions: ActionsPluginStart; +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/tsconfig.json b/x-pack/solutions/chat/plugins/wci-index-source/tsconfig.json new file mode 100644 index 000000000000..4cb78c021bc4 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../typings/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/config-schema", + "@kbn/wci-common", + "@kbn/wci-server", + "@kbn/workchat-app", + "@kbn/wci-browser", + "@kbn/i18n-react", + "@kbn/kibana-react-plugin", + "@kbn/wc-integration-utils", + "@kbn/wc-index-schema-builder", + "@kbn/wc-genai-utils", + "@kbn/inference-plugin", + "@kbn/actions-plugin" + ], + "exclude": [ + "target/**/*" + ] +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/README.md b/x-pack/solutions/chat/plugins/wci-salesforce/README.md new file mode 100755 index 000000000000..85feae0bf70e --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/README.md @@ -0,0 +1,3 @@ +# WorkChat Salesforce Integration + +WorkChat Salesforce integration plugin that provides Salesforce-specific functionality for the WorkChat application. diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/common/index.ts b/x-pack/solutions/chat/plugins/wci-salesforce/common/index.ts new file mode 100644 index 000000000000..e5781a9a94fb --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const PLUGIN_ID = 'wciSalesforce'; +export const PLUGIN_NAME = 'wciSalesforce'; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/common/types.ts b/x-pack/solutions/chat/plugins/wci-salesforce/common/types.ts new file mode 100644 index 000000000000..26ae085da83b --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/common/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationConfiguration } from '@kbn/wci-common'; + +export type SalesforceConfiguration = IntegrationConfiguration; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/jest.config.js b/x-pack/solutions/chat/plugins/wci-salesforce/jest.config.js new file mode 100644 index 000000000000..064c463078dd --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/plugins/wci-salesforce'], + testMatch: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/kibana.jsonc b/x-pack/solutions/chat/plugins/wci-salesforce/kibana.jsonc new file mode 100644 index 000000000000..c5fa70dc2de5 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/kibana.jsonc @@ -0,0 +1,17 @@ +{ + "type": "plugin", + "id": "@kbn/wci-salesforce", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private", + "description": "WorkChat Salesforce Integration plugin", + "plugin": { + "id": "wciSalesforce", + "server": true, + "browser": true, + "configPath": ["xpack", "wciSalesforce"], + "requiredPlugins": ["inference", "workchatApp"], + "optionalPlugins": [], + "requiredBundles": [] + } +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/package.json b/x-pack/solutions/chat/plugins/wci-salesforce/package.json new file mode 100644 index 000000000000..7b9c4853f437 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/wci-salesforce", + "version": "1.0.0", + "license": "Elastic License 2.0", + "private": true, + "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../../../../scripts/plugin_helpers", + "kbn": "node ../../../../../scripts/kbn" + } +} \ No newline at end of file diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/public/index.ts b/x-pack/solutions/chat/plugins/wci-salesforce/public/index.ts new file mode 100644 index 000000000000..c44213697df0 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/public/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { WCISalesforcePlugin } from './plugin'; +import { + WCISalesforcePluginSetup, + WCISalesforcePluginStart, + WCISalesforcePluginStartDependencies, +} from './types'; + +export const plugin: PluginInitializer< + WCISalesforcePluginSetup, + WCISalesforcePluginStart, + {}, + WCISalesforcePluginStartDependencies +> = (context: PluginInitializerContext) => { + return new WCISalesforcePlugin(context); +}; + +export type { WCISalesforcePluginSetup, WCISalesforcePluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/configuration.tsx b/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/configuration.tsx new file mode 100644 index 000000000000..268237f62792 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/configuration.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { IntegrationConfigurationFormProps } from '@kbn/wci-browser'; +import { EuiDescribedFormGroup, EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { Controller } from 'react-hook-form'; + +export const SalesforceConfigurationForm: React.FC = ({ + form, +}) => { + const { control } = form; + + return ( + Salesforce Configuration} + description="Configure the salesforce details" + > + + ( + + )} + /> + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/salesforce_integration.ts b/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/salesforce_integration.ts new file mode 100644 index 000000000000..4d556fa0a700 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/salesforce_integration.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType } from '@kbn/wci-common'; +import type { IntegrationComponentDescriptor } from '@kbn/wci-browser'; +import { SalesforceTool } from './tool'; +import { SalesforceConfigurationForm } from './configuration'; + +export function getSalesforceIntegrationComponents(): IntegrationComponentDescriptor { + return { + getType: () => IntegrationType.salesforce, + getToolCallComponent: (name) => SalesforceTool, + getConfigurationForm: () => SalesforceConfigurationForm, + }; +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/tool.tsx b/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/tool.tsx new file mode 100644 index 000000000000..62b3d5bc4b40 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/public/integration/tool.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { IntegrationToolComponentProps } from '@kbn/wci-browser'; +import { EuiText } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const bold = css` + font-weight: bold; +`; + +const italic = css` + font-style: italic; +`; + +export const SalesforceTool: React.FC = ({ + toolCall, + toolResult, +}) => { + const toolNode = ( + + {toolCall.toolName} + + ); + const argsNode = ( + + {JSON.stringify(toolCall.args)} + + ); + + if (toolResult) { + return ( + + + + ); + } else { + return ( + + + + ); + } +}; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/public/plugin.tsx b/x-pack/solutions/chat/plugins/wci-salesforce/public/plugin.tsx new file mode 100644 index 000000000000..78101692ea85 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/public/plugin.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import type { + WCISalesforcePluginSetup, + WCISalesforcePluginStart, + WCISalesforcePluginSetupDependencies, + WCISalesforcePluginStartDependencies, +} from './types'; +import { getSalesforceIntegrationComponents } from './integration/salesforce_integration'; + +export class WCISalesforcePlugin + implements + Plugin< + WCISalesforcePluginSetup, + WCISalesforcePluginStart, + WCISalesforcePluginSetupDependencies, + WCISalesforcePluginStartDependencies + > +{ + constructor(context: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + { workchatApp }: WCISalesforcePluginSetupDependencies + ): WCISalesforcePluginSetup { + workchatApp.integrations.register(getSalesforceIntegrationComponents()); + return {}; + } + + public start( + coreStart: CoreStart, + pluginsStart: WCISalesforcePluginStartDependencies + ): WCISalesforcePluginStart { + return {}; + } + + public stop() {} +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/public/types.ts b/x-pack/solutions/chat/plugins/wci-salesforce/public/types.ts new file mode 100644 index 000000000000..e2923cb477c5 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/public/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { WorkChatAppPluginSetup } from '@kbn/workchat-app/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCISalesforcePluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCISalesforcePluginStart {} + +export interface WCISalesforcePluginSetupDependencies { + workchatApp: WorkChatAppPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCISalesforcePluginStartDependencies {} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/config.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/config.ts new file mode 100644 index 000000000000..cd546cf435bd --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type WCISalesforceConfig = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: {}, + schema: configSchema, +}; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/index.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/index.ts new file mode 100644 index 000000000000..b5407f44407e --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { WCISalesforcePlugin } from './plugin'; + +export { config } from './config'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new WCISalesforcePlugin(initializerContext); +}; + +export type { WCISalesforcePluginSetup, WCISalesforcePluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/index.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/index.ts new file mode 100644 index 000000000000..eb52c752dc84 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getSalesforceIntegrationDefinition } from './salesforce_integration'; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/mcp_server.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/mcp_server.ts new file mode 100644 index 000000000000..1aa557645877 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/mcp_server.ts @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { z } from '@kbn/zod'; +import { retrieveCases } from './tools'; + +// Define enum field structure upfront +interface Field { + field: string; + description: string; + path?: string; // Optional path for nested fields +} + +// Define enum value structure +interface FieldWithValues extends Field { + values: string[]; +} + +export async function createMcpServer({ + configuration, + elasticsearchClient, + logger, +}: { + configuration: Record; + elasticsearchClient: ElasticsearchClient; + logger: Logger; +}): Promise { + const server = new McpServer({ + name: 'wci-salesforce', + version: '1.0.0', + }); + + const { index } = configuration; + + const enumFields: Field[] = [ + { field: 'priority', description: 'Case priority level', path: 'metadata.priority' }, + { + field: 'status', + description: 'Current status of the case', + path: 'metadata.status', + }, + ]; + + // Extract field mappings for sorting parameters + const sortableFields = await getSortableFields(elasticsearchClient, logger, index); + logger.debug('Salesforce Integration Sortable Fields ' + JSON.stringify(sortableFields)); + + const enumFieldValues = await getFieldValues(elasticsearchClient, logger, index, enumFields); + // Extract specific values for tool parameters + const priorityValues = enumFieldValues.find((f) => f.field === 'priority')?.values || []; + const statusValues = enumFieldValues.find((f) => f.field === 'status')?.values || []; + + // Create enum types for validation + const priorityEnum = z.enum( + priorityValues.length ? (priorityValues as [string, ...string[]]) : [''] + ); + const statusEnum = z.enum(statusValues.length ? (statusValues as [string, ...string[]]) : ['']); + + server.tool( + 'retrieve_cases', + `Retrieves Salesforce support cases with flexible filtering options`, + { + caseNumber: z + .array(z.string()) + .optional() + .describe('Salesforce case number identifiers (preferred lookup method)'), + id: z + .array(z.string()) + .optional() + .describe( + 'Salesforce internal IDs of the support cases (use only when specifically requested)' + ), + size: z.number().int().positive().default(10).describe('Maximum number of cases to return'), + sortField: z + .string() + .optional() + .describe(`Field to sort results by. Can only be one of these ${sortableFields}`), + sortOrder: z + .string() + .optional() + .describe( + `Sorting order. Can only be 'desc' meaning sort in descending order or 'asc' meaning sort in ascending order` + ), + ownerEmail: z + .array(z.string()) + .optional() + .describe('Emails of case owners/assignees to filter results'), + priority: z + .array(priorityEnum) + .optional() + .describe( + `Case priority levels${ + priorityValues.length ? ` (values from: ${priorityValues.join(', ')})` : '' + }` + ), + status: z + .array(statusEnum) + .optional() + .describe( + `Current statuses of the cases${ + statusValues.length ? ` (values from: ${statusValues.join(', ')})` : '' + }` + ), + closed: z.boolean().optional().describe('Filter by case closure status (true/false)'), + createdAfter: z + .string() + .optional() + .describe('Return cases created after this date (format: YYYY-MM-DD)'), + createdBefore: z + .string() + .optional() + .describe('Return cases created before this date (format: YYYY-MM-DD)'), + semanticQuery: z + .string() + .optional() + .describe('Natural language query to search case content semantically'), + updatedAfter: z + .string() + .optional() + .describe('Return cases updated after this date (format: YYYY-MM-DD)'), + updatedBefore: z + .string() + .optional() + .describe('Return cases updated before this date (format: YYYY-MM-DD)'), + }, + async ({ + id, + size = 10, + sortField, + sortOrder, + priority, + closed, + caseNumber, + createdAfter, + createdBefore, + semanticQuery, + status, + updatedAfter, + updatedBefore, + }) => { + const caseContent = await retrieveCases(elasticsearchClient, logger, index, { + id, + size, + sortField, + sortOrder, + priority, + closed, + caseNumber, + createdAfter, + createdBefore, + semanticQuery, + status, + updatedAfter, + updatedBefore, + }); + + logger.info(`Retrieved ${caseContent.length} support cases`); + + return { + content: caseContent, + }; + } + ); + + return server; +} + +/** + * Retrieves possible values for enum fields from Elasticsearch using _terms_enum API + */ +async function getFieldValues( + elasticsearchClient: ElasticsearchClient, + logger: Logger, + index: string, + enumFields: Field[] +): Promise { + const fieldValues: FieldWithValues[] = enumFields.map((field) => ({ + ...field, + values: [], + })); + + try { + for (let i = 0; i < enumFields.length; i++) { + const field = enumFields[i]; + const fieldPath = field.path || field.field; + + const response = await elasticsearchClient.termsEnum({ + index, + field: fieldPath, + }); + + if (response.terms && response.terms.length) { + fieldValues[i] = { + ...fieldValues[i], + values: response.terms, + }; + } + } + } catch (error) { + logger.error(`Failed to get terms enum for fields: ${error}`); + } + + return fieldValues; +} +/** + * Retrieves index field properties for sorting + */ +async function getSortableFields( + client: ElasticsearchClient, + logger: Logger, + indexName: string +): Promise> { + const sortableFieldTypes = ['keyword', 'date', 'boolean', 'integer', 'long', 'double', 'float']; + const sortableFields = []; + try { + const response = await client.indices.getMapping({ + index: indexName, + }); + + if (response) { + const properties = response[indexName].mappings.properties as Record; + + for (const [fieldName, fieldConfig] of Object.entries(properties)) { + if (sortableFieldTypes.includes(fieldConfig.type)) { + const sortableField = { field: fieldName, type: fieldConfig.type }; + sortableFields.push(sortableField); + } + } + return sortableFields; + } else { + throw new Error('Could not find mappings in response'); + } + } catch (error) { + logger.error(`Error getting field mappings for index ${indexName}:`, error); + throw error; + } +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/salesforce_integration.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/salesforce_integration.ts new file mode 100644 index 000000000000..f5f935881c8e --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/salesforce_integration.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, Logger } from '@kbn/core/server'; +import { IntegrationType } from '@kbn/wci-common'; +import { + getConnectToInternalServer, + type WorkchatIntegrationDefinition, + type WorkChatIntegration, +} from '@kbn/wci-server'; +import type { WCISalesforceConfiguration } from '../types'; +import { createMcpServer } from './mcp_server'; + +export const getSalesforceIntegrationDefinition = ({ + core, + logger, +}: { + core: CoreSetup; + logger: Logger; +}): WorkchatIntegrationDefinition => { + return { + getType: () => IntegrationType.salesforce, + createIntegration: async ({ request, configuration }): Promise => { + const [coreStart] = await core.getStartServices(); + const elasticsearchClient = coreStart.elasticsearch.client.asScoped(request).asCurrentUser; + + const mcpServer = await createMcpServer({ configuration, elasticsearchClient, logger }); + + return { + connect: getConnectToInternalServer({ server: mcpServer }), + }; + }, + }; +}; diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/tools.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/tools.ts new file mode 100644 index 000000000000..9c3ccbece98d --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/tools.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SearchRequest, + SearchResponse, + SortOrder, +} from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { SupportCase } from './types'; + +interface CaseRetrievalParams { + id?: string[]; + size?: number; + sortField?: string; + sortOrder?: string; + ownerEmail?: string[]; + priority?: string[]; + closed?: boolean; + caseNumber?: string[]; + createdAfter?: string; + createdBefore?: string; + semanticQuery?: string; + status?: string[]; + updatedAfter?: string; + updatedBefore?: string; +} + +/** + * Retrieves Salesforce cases + * + * @param esClient - Elasticsearch client + * @param logger - Logger instance + * @param indexName - Index name to query + * @param params - Search parameters including optional sorting configuration + */ +export async function retrieveCases( + esClient: ElasticsearchClient, + logger: Logger, + indexName: string, + params: CaseRetrievalParams = {} +): Promise> { + const size = params.size || 10; + const sort = params.sortField + ? [{ [params.sortField as string]: { order: params.sortOrder as SortOrder } }] + : []; + + try { + const query = buildQuery(params); + + const searchRequest: SearchRequest = { + index: indexName, + query, + sort, + size, + }; + + logger.info( + `Retrieving cases from ${indexName} with search request: ${JSON.stringify(searchRequest)}` + ); + + const response = await esClient.search>(searchRequest); + + // Let's keep this here for now + const contextFields = [ + { field: 'title', type: 'keyword' }, + { field: 'description', type: 'text' }, + { field: 'content', type: 'text' }, + { field: 'metadata.case_number', type: 'keyword' }, + { field: 'metadata.priority', type: 'keyword' }, + { field: 'metadata.status', type: 'keyword' }, + { field: 'owner.email', type: 'keyword' }, + { field: 'owner.name', type: 'keyword' }, + ]; + + const contentFragments = response.hits.hits.map((hit) => { + const source = hit._source as SupportCase; + + // Helper function to safely get nested values + const getNestedValue = (obj: any, path: string[]): string => { + return ( + path + .reduce((prev, curr) => { + return prev && typeof prev === 'object' && curr in prev ? prev[curr] : ''; + }, obj) + ?.toString() || '' + ); + }; + + return { + type: 'text' as const, + text: contextFields + .map(({ field }) => { + const fieldPath = field.split('.'); + let value = ''; + + // Use the helper function for both nested and non-nested fields + value = + fieldPath.length > 1 + ? getNestedValue(source, fieldPath) + : (source[field as keyof SupportCase] || '').toString(); + + return `${field}: ${value}`; + }) + .join('\n'), + }; + }); + + return contentFragments; + } catch (error) { + logger.error(`Search failed: ${error}`); + + return [ + { + type: 'text' as const, + text: `Error: Search failed: ${error}`, + }, + ]; + } +} + +function buildQuery(params: CaseRetrievalParams): any { + const mustClauses: any[] = [{ term: { object_type: 'support_case' } }]; + + if (params.id && params.id.length > 0) mustClauses.push({ terms: { id: params.id } }); + if (params.caseNumber && params.caseNumber.length > 0) + mustClauses.push({ terms: { 'metadata.case_number': params.caseNumber } }); + if (params.ownerEmail && params.ownerEmail.length > 0) + mustClauses.push({ terms: { 'owner.emailaddress': params.ownerEmail } }); + if (params.priority && params.priority.length > 0) + mustClauses.push({ terms: { 'metadata.priority': params.priority } }); + if (params.status && params.status.length > 0) + mustClauses.push({ terms: { 'metadata.status': params.status } }); + if (params.closed !== undefined) mustClauses.push({ term: { 'metadata.closed': params.closed } }); + + if (params.createdAfter || params.createdBefore) { + const range: any = { range: { created_at: {} } }; + if (params.createdAfter) range.range.created_at.gte = params.createdAfter; + if (params.createdBefore) range.range.created_at.lte = params.createdBefore; + mustClauses.push(range); + } + + if (params.updatedAfter || params.updatedBefore) { + const range: any = { range: { updated_at: {} } }; + if (params.updatedAfter) range.range.updated_at.gte = params.updatedAfter; + if (params.updatedBefore) range.range.updated_at.lte = params.updatedBefore; + mustClauses.push(range); + } + + if (params.semanticQuery) { + mustClauses.push({ + semantic: { field: 'content_semantic', query: params.semanticQuery, boost: 2.0 }, + }); + } + + return { bool: { must: mustClauses } }; +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/types.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/types.ts new file mode 100644 index 000000000000..19afb3f7ab10 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/integration/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface SupportCase { + id?: string; + title?: string; + content?: string; + content_semantic?: string; + url?: string; + object_type?: string; + owner?: { + name?: string; + emailaddress?: string; + }; + created_at?: string | Date; + updated_at?: string | Date; + metadata?: { + case_number?: string; + priority?: string; + status?: string; + closed?: boolean; + deleted?: boolean; + }; + comments?: Array<{ + id?: string; + author?: { + email?: string; + name?: string; + }; + content?: { + text?: string; + }; + created_at?: string | Date; + updated_at?: string | Date; + }>; +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/plugin.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/plugin.ts new file mode 100644 index 000000000000..8376ea66cf2d --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/plugin.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoreStart, + CoreSetup, + Plugin, + PluginInitializerContext, + Logger, +} from '@kbn/core/server'; +import type { + WCISalesforcePluginStart, + WCISalesforcePluginSetup, + WCISalesforcePluginSetupDependencies, + WCISalesforcePluginStartDependencies, +} from './types'; +import { getSalesforceIntegrationDefinition } from './integration'; + +export class WCISalesforcePlugin + implements + Plugin< + WCISalesforcePluginSetup, + WCISalesforcePluginStart, + WCISalesforcePluginSetupDependencies, + WCISalesforcePluginStartDependencies + > +{ + private readonly logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + + public setup( + core: CoreSetup, + { workchatApp }: WCISalesforcePluginSetupDependencies + ): WCISalesforcePluginSetup { + workchatApp.integrations.register( + getSalesforceIntegrationDefinition({ + core, + logger: this.logger, + }) + ); + + return {}; + } + + public start( + core: CoreStart, + pluginsDependencies: WCISalesforcePluginStartDependencies + ): WCISalesforcePluginStart { + return {}; + } +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/server/types.ts b/x-pack/solutions/chat/plugins/wci-salesforce/server/types.ts new file mode 100644 index 000000000000..4853d3b599c1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/server/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationConfiguration } from '@kbn/wci-common'; +import type { WorkChatAppPluginSetup } from '@kbn/workchat-app/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCISalesforcePluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCISalesforcePluginStart {} + +export interface WCISalesforcePluginSetupDependencies { + workchatApp: WorkChatAppPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WCISalesforcePluginStartDependencies {} + +export interface WCISalesforceConfiguration extends IntegrationConfiguration { + index: string; +} diff --git a/x-pack/solutions/chat/plugins/wci-salesforce/tsconfig.json b/x-pack/solutions/chat/plugins/wci-salesforce/tsconfig.json new file mode 100644 index 000000000000..af8374532e09 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-salesforce/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../typings/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/config-schema", + "@kbn/wci-common", + "@kbn/wci-server", + "@kbn/zod", + "@kbn/workchat-app", + "@kbn/wci-browser", + "@kbn/i18n-react", + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/README.md b/x-pack/solutions/chat/plugins/workchat-app/README.md new file mode 100755 index 000000000000..85ea7140d9a2 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/README.md @@ -0,0 +1,3 @@ +# WorkChatApp + +WorkChat application plugin diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/agents.ts b/x-pack/solutions/chat/plugins/workchat-app/common/agents.ts new file mode 100644 index 000000000000..6b4db18353e6 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/agents.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UserNameAndId } from './shared'; + +/** + * Represents an agent + */ +export interface Agent { + id: string; + name: string; + description: string; + lastUpdated: string; + user: UserNameAndId; + public: boolean; + configuration: Record; +} + +export type AgentCreateRequest = Pick< + Agent, + 'name' | 'description' | 'configuration' | 'public' +> & { + id?: string; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/chat_events.test.ts b/x-pack/solutions/chat/plugins/workchat-app/common/chat_events.test.ts new file mode 100644 index 000000000000..30e80f2df474 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/chat_events.test.ts @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createUserMessage, createAssistantMessage } from './conversation_events'; +import { + isMessageEvent, + isToolResultEvent, + isChunkEvent, + isConversationCreatedEvent, + isConversationUpdatedEvent, + conversationCreatedEvent, + conversationUpdatedEvent, + chunkEvent, + messageEvent, + toolResultEvent, +} from './chat_events'; + +describe('chat_events', () => { + describe('isMessageEvent', () => { + it('should return true for message events', () => { + const userMessage = createUserMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + expect(isMessageEvent(messageEvent({ message: userMessage }))).toBe(true); + }); + + it('should return false for non-message events', () => { + expect( + isMessageEvent( + chunkEvent({ + contentChunk: 'Hello', + messageId: 'msg-1', + }) + ) + ).toBe(false); + }); + }); + + describe('isChunkEvent', () => { + it('should return true for chunk events', () => { + expect( + isChunkEvent( + chunkEvent({ + contentChunk: 'Hello', + messageId: 'msg-1', + }) + ) + ).toBe(true); + }); + + it('should return false for non-chunk events', () => { + const assistantMessage = createAssistantMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + toolCalls: [], + }); + expect(isChunkEvent(messageEvent({ message: assistantMessage }))).toBe(false); + }); + }); + + describe('isToolResultEvent', () => { + it('should return true for tool result events', () => { + expect( + isToolResultEvent( + toolResultEvent({ + callId: 'call-1', + result: 'Result from tool', + }) + ) + ).toBe(true); + }); + + it('should return false for non-tool result events', () => { + const userMessage = createUserMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + expect(isToolResultEvent(messageEvent({ message: userMessage }))).toBe(false); + }); + }); + + describe('isConversationCreatedEvent', () => { + it('should return true for conversation created events', () => { + expect( + isConversationCreatedEvent( + conversationCreatedEvent({ + id: 'conv-1', + title: 'Test Conversation', + }) + ) + ).toBe(true); + }); + + it('should return false for non-conversation created events', () => { + const userMessage = createUserMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + expect(isConversationCreatedEvent(messageEvent({ message: userMessage }))).toBe(false); + }); + }); + + describe('isConversationUpdatedEvent', () => { + it('should return true for conversation updated events', () => { + expect( + isConversationUpdatedEvent( + conversationUpdatedEvent({ + id: 'conv-1', + title: 'Updated Test Conversation', + }) + ) + ).toBe(true); + }); + + it('should return false for non-conversation updated events', () => { + const userMessage = createUserMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + expect(isConversationUpdatedEvent(messageEvent({ message: userMessage }))).toBe(false); + }); + }); + + describe('conversationCreatedEvent', () => { + it('should create a conversation created event', () => { + expect( + conversationCreatedEvent({ + id: 'conv-1', + title: 'Test Conversation', + }) + ).toEqual({ + type: 'conversation_created', + conversation: { + id: 'conv-1', + title: 'Test Conversation', + }, + }); + }); + }); + + describe('conversationUpdatedEvent', () => { + it('should create a conversation updated event', () => { + expect( + conversationUpdatedEvent({ + id: 'conv-1', + title: 'Updated Test Conversation', + }) + ).toEqual({ + type: 'conversation_updated', + conversation: { + id: 'conv-1', + title: 'Updated Test Conversation', + }, + }); + }); + }); + + describe('chunkEvent', () => { + it('should create a chunk event', () => { + expect( + chunkEvent({ + contentChunk: 'partial response', + messageId: 'msg-1', + }) + ).toEqual({ + type: 'message_chunk', + content_chunk: 'partial response', + message_id: 'msg-1', + }); + }); + }); + + describe('messageEvent', () => { + it('should create a message event with a user message', () => { + const userMessage = createUserMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect( + messageEvent({ + message: userMessage, + }) + ).toEqual({ + type: 'message', + message: userMessage, + }); + }); + + it('should create a message event with an assistant message', () => { + const assistantMessage = createAssistantMessage({ + id: 'msg-2', + content: 'Hello there', + createdAt: '2023-01-01T00:00:00.000Z', + toolCalls: [], + }); + + expect( + messageEvent({ + message: assistantMessage, + }) + ).toEqual({ + type: 'message', + message: assistantMessage, + }); + }); + }); + + describe('toolResultEvent', () => { + it('should create a tool result event', () => { + expect( + toolResultEvent({ + callId: 'call-1', + result: 'Result from tool execution', + }) + ).toEqual({ + type: 'tool_result', + toolResult: { + callId: 'call-1', + result: 'Result from tool execution', + }, + }); + }); + }); +}); diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/chat_events.ts b/x-pack/solutions/chat/plugins/workchat-app/common/chat_events.ts new file mode 100644 index 000000000000..60da7d0a0a2c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/chat_events.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UserMessage, AssistantMessage } from './conversation_events'; + +export interface ChatEventBase { + type: string; +} + +/** + * Emitted when a message chunk is emitted by the LLM. + */ +export interface ChunkEvent extends ChatEventBase { + type: 'message_chunk'; + content_chunk: string; + message_id: string; +} + +export interface ToolResultEvent extends ChatEventBase { + type: 'tool_result'; + toolResult: { + callId: string; + result: string; + }; +} + +/** + * Emitted when a conversation was created. + * + * Can be used to update the UI with the new information. + */ +export interface ConversationCreatedEvent extends ChatEventBase { + type: 'conversation_created'; + conversation: ConversationEventChanges; +} + +/** + * Emitted when a conversation was updated. + * + * Can be used to update the UI with the new information. + */ +export interface ConversationUpdatedEvent extends ChatEventBase { + type: 'conversation_updated'; + conversation: ConversationEventChanges; +} + +/** + * Emitted when a full message was generated. + */ +export interface MessageEvent extends ChatEventBase { + type: 'message'; + message: UserMessage | AssistantMessage; +} + +export interface ConversationEventChanges { + id: string; + title: string; +} + +export type ChatEvent = + | ChunkEvent + | MessageEvent + | ConversationCreatedEvent + | ConversationUpdatedEvent + | ToolResultEvent; + +export const conversationCreatedEvent = ({ + id, + title, +}: { + id: string; + title: string; +}): ConversationCreatedEvent => { + return { + type: 'conversation_created', + conversation: { id, title }, + }; +}; + +export const conversationUpdatedEvent = ({ + id, + title, +}: { + id: string; + title: string; +}): ConversationUpdatedEvent => { + return { + type: 'conversation_updated', + conversation: { id, title }, + }; +}; + +/** + * Creates a chunk event to represent partial content from an LLM response. + */ +export const chunkEvent = ({ + contentChunk, + messageId, +}: { + contentChunk: string; + messageId: string; +}): ChunkEvent => { + return { + type: 'message_chunk', + content_chunk: contentChunk, + message_id: messageId, + }; +}; + +/** + * Creates a message event to represent a complete message from either a user or assistant. + */ +export const messageEvent = ({ + message, +}: { + message: UserMessage | AssistantMessage; +}): MessageEvent => { + return { + type: 'message', + message, + }; +}; + +/** + * Creates a tool result event to represent the result of a tool execution. + */ +export const toolResultEvent = ({ + callId, + result, +}: { + callId: string; + result: string; +}): ToolResultEvent => { + return { + type: 'tool_result', + toolResult: { + callId, + result, + }, + }; +}; + +export const isMessageEvent = (event: ChatEvent): event is MessageEvent => { + return event.type === 'message'; +}; + +export const isChunkEvent = (event: ChatEvent): event is ChunkEvent => { + return event.type === 'message_chunk'; +}; + +export const isToolResultEvent = (event: ChatEvent): event is ToolResultEvent => { + return event.type === 'tool_result'; +}; + +export const isConversationCreatedEvent = (event: ChatEvent): event is ConversationCreatedEvent => { + return event.type === 'conversation_created'; +}; + +export const isConversationUpdatedEvent = (event: ChatEvent): event is ConversationUpdatedEvent => { + return event.type === 'conversation_updated'; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/constants.ts b/x-pack/solutions/chat/plugins/workchat-app/common/constants.ts new file mode 100644 index 000000000000..624b3975fe04 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const baseToolsProviderId = 'base_tools'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.test.ts b/x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.test.ts new file mode 100644 index 000000000000..d355a2a7d858 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.test.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ConversationEventType, + createUserMessage, + createAssistantMessage, + createToolResult, + isUserMessage, + isAssistantMessage, + isToolResult, +} from './conversation_events'; + +describe('conversation_events', () => { + describe('createUserMessage', () => { + it('should create a user message with the provided content', () => { + const message = createUserMessage({ + content: 'Hello', + }); + + expect(message.type).toBe(ConversationEventType.userMessage); + expect(message.content).toBe('Hello'); + expect(message.id).toBeDefined(); + expect(message.createdAt).toBeDefined(); + }); + + it('should use provided id and createdAt if available', () => { + const message = createUserMessage({ + id: 'custom-id', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(message.id).toBe('custom-id'); + expect(message.createdAt).toBe('2023-01-01T00:00:00.000Z'); + }); + }); + + describe('createAssistantMessage', () => { + it('should create an assistant message with default values', () => { + const message = createAssistantMessage({}); + + expect(message.type).toBe(ConversationEventType.assistantMessage); + expect(message.content).toBe(''); + expect(message.toolCalls).toEqual([]); + expect(message.id).toBeDefined(); + expect(message.createdAt).toBeDefined(); + }); + + it('should use provided values if available', () => { + const toolCalls = [ + { + toolCallId: 'tool-1', + toolName: 'calculator', + args: { expression: '2+2' }, + }, + ]; + + const message = createAssistantMessage({ + id: 'custom-id', + content: 'I calculated that for you', + toolCalls, + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(message.id).toBe('custom-id'); + expect(message.content).toBe('I calculated that for you'); + expect(message.toolCalls).toEqual(toolCalls); + expect(message.createdAt).toBe('2023-01-01T00:00:00.000Z'); + }); + }); + + describe('createToolResult', () => { + it('should create a tool result with required values', () => { + const toolResult = createToolResult({ + toolCallId: 'tool-1', + toolResult: '4', + }); + + expect(toolResult.type).toBe(ConversationEventType.toolResult); + expect(toolResult.toolCallId).toBe('tool-1'); + expect(toolResult.toolResult).toBe('4'); + expect(toolResult.id).toBeDefined(); + expect(toolResult.createdAt).toBeDefined(); + }); + + it('should use provided id and createdAt if available', () => { + const toolResult = createToolResult({ + id: 'custom-id', + toolCallId: 'tool-1', + toolResult: '4', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(toolResult.id).toBe('custom-id'); + expect(toolResult.createdAt).toBe('2023-01-01T00:00:00.000Z'); + }); + }); + + describe('isUserMessage', () => { + it('should return true for user messages', () => { + const userMessage = createUserMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isUserMessage(userMessage)).toBe(true); + }); + + it('should return false for non-user messages', () => { + const assistantMessage = createAssistantMessage({ + id: 'msg-2', + content: 'Hello there', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isUserMessage(assistantMessage)).toBe(false); + + const toolResult = createToolResult({ + id: 'result-1', + toolCallId: 'tool-1', + toolResult: '4', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isUserMessage(toolResult)).toBe(false); + }); + }); + + describe('isAssistantMessage', () => { + it('should return true for assistant messages', () => { + const assistantMessage = createAssistantMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isAssistantMessage(assistantMessage)).toBe(true); + }); + + it('should return false for non-assistant messages', () => { + const userMessage = createUserMessage({ + id: 'msg-2', + content: 'Hello there', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isAssistantMessage(userMessage)).toBe(false); + + const toolResult = createToolResult({ + id: 'result-1', + toolCallId: 'tool-1', + toolResult: '4', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isAssistantMessage(toolResult)).toBe(false); + }); + }); + + describe('isToolResult', () => { + it('should return true for tool results', () => { + const toolResult = createToolResult({ + id: 'result-1', + toolCallId: 'tool-1', + toolResult: '4', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isToolResult(toolResult)).toBe(true); + }); + + it('should return false for non-tool results', () => { + const userMessage = createUserMessage({ + id: 'msg-1', + content: 'Hello', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isToolResult(userMessage)).toBe(false); + + const assistantMessage = createAssistantMessage({ + id: 'msg-2', + content: 'Hello there', + createdAt: '2023-01-01T00:00:00.000Z', + }); + + expect(isToolResult(assistantMessage)).toBe(false); + }); + }); +}); diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.ts b/x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.ts new file mode 100644 index 000000000000..e50256ef5829 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/conversation_events.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 as uuidv4 } from 'uuid'; + +export enum ConversationEventType { + userMessage = 'user_message', + assistantMessage = 'assistant_message', + toolResult = 'tool_result', +} + +/** + * Base interface for conversation events + */ +export interface ConversationEventBase { + /** + * Type of the event. + */ + type: T; + /** + * ID for this event. + * Must be unique across all events of a given conversation. + */ + id: string; + /** + * Creation date for this event, ISO format. + */ + createdAt: string; +} + +/** + * Represents a message from the user. + */ +export interface UserMessage extends ConversationEventBase { + /** + * The text content of the message + */ + content: string; +} + +/** + * Represents a message from the assistant + */ +export interface AssistantMessage + extends ConversationEventBase { + /** + * The text content of the message. + * Can be blank if toolCalls is not empty. + */ + content: string; + /** + * + */ + toolCalls: ToolCall[]; +} + +/** + * Represents a tool being executed + */ +export interface ToolResult extends ConversationEventBase { + toolCallId: string; + toolResult: string; +} + +/** + * Composite of all possible conversation event types + */ +export type ConversationEvent = UserMessage | AssistantMessage | ToolResult; + +/** + * Represents a tool call that was requested by the assistant + */ +export interface ToolCall { + toolCallId: string; + toolName: string; + args: Record; +} + +export const createUserMessage = ( + parts: Partial> & Pick +): UserMessage => { + return { + type: ConversationEventType.userMessage, + id: parts.id ?? uuidv4(), + content: parts.content, + createdAt: parts.createdAt ?? new Date().toISOString(), + }; +}; + +export const createAssistantMessage = ( + parts: Partial> +): AssistantMessage => { + return { + type: ConversationEventType.assistantMessage, + id: parts.id ?? uuidv4(), + content: parts.content ?? '', + toolCalls: parts.toolCalls ?? [], + createdAt: parts.createdAt ?? new Date().toISOString(), + }; +}; + +export const createToolResult = ( + parts: Partial> & Pick +): ToolResult => { + return { + type: ConversationEventType.toolResult, + id: parts.id ?? uuidv4(), + toolCallId: parts.toolCallId, + toolResult: parts.toolResult, + createdAt: parts.createdAt ?? new Date().toISOString(), + }; +}; + +export const isUserMessage = (event: ConversationEvent): event is UserMessage => { + return event.type === ConversationEventType.userMessage; +}; + +export const isAssistantMessage = (event: ConversationEvent): event is AssistantMessage => { + return event.type === ConversationEventType.assistantMessage; +}; + +export const isToolResult = (event: ConversationEvent): event is ToolResult => { + return event.type === ConversationEventType.toolResult; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/conversations.ts b/x-pack/solutions/chat/plugins/workchat-app/common/conversations.ts new file mode 100644 index 000000000000..1b0f8ea95a25 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/conversations.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UserNameAndId } from './shared'; +import type { ConversationEvent } from './conversation_events'; + +export interface Conversation { + id: string; + agentId: string; + title: string; + lastUpdated: string; + user: UserNameAndId; + events: ConversationEvent[]; +} + +/** + * Summary of conversation, e.g. for the conversation listing + */ +export interface ConversationSummary { + id: string; + agentId: string; + title: string; + lastUpdated: string; +} + +export type ConversationCreateRequest = Omit & { + id?: string; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/errors.ts b/x-pack/solutions/chat/plugins/workchat-app/common/errors.ts new file mode 100644 index 000000000000..dade8dfcc13b --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/errors.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServerSentEventError } from '@kbn/sse-utils'; + +export type ChatErrorCode = 'internalError' | 'connectorNotFound'; + +export interface ErrorEvent { + type: 'error'; + error: { + code: ChatErrorCode; + message: string; + meta: Record; + }; +} + +/** + * Represents an error that can be thrown by the chat API. + */ +export class ChatError extends ServerSentEventError> { + constructor(code: ChatErrorCode, message: string, meta: Record = {}) { + super(code, message, meta); + } +} + +export const isChatError = (error: unknown): error is ChatError => { + return error instanceof ChatError; +}; + +export const createChatError = ( + code: ChatErrorCode, + message: string, + meta: Record = {} +): ChatError => { + return new ChatError(code, message, meta); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/features.ts b/x-pack/solutions/chat/plugins/workchat-app/common/features.ts new file mode 100644 index 000000000000..d72bd4fd4c4b --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/features.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const WORKCHAT_FEATURE_ID = 'workchat'; +export const WORKCHAT_FEATURE_NAME = 'WorkChat'; +export const WORKCHAT_APP_ID = 'workchat'; + +export const uiCapabilities = { + show: 'show', + showManagement: 'showManagement', +}; + +export const apiCapabilities = { + useWorkchat: 'workchat_use', + manageWorkchat: 'workchat_manage', +}; + +// defining feature groups here because it's less error prone when adding new capabilities +export const capabilityGroups = { + ui: { + read: [uiCapabilities.show], + all: [uiCapabilities.show, uiCapabilities.showManagement], + }, + api: { + read: [apiCapabilities.useWorkchat], + all: [apiCapabilities.manageWorkchat, apiCapabilities.manageWorkchat], + }, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/http_api/agents.ts b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/agents.ts new file mode 100644 index 000000000000..1308b284adbf --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/agents.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Agent, AgentCreateRequest } from '../agents'; + +export interface ListAgentResponse { + agents: Agent[]; +} + +export type GetAgentResponse = Agent; + +export type CreateAgentPayload = Omit; + +export type CreateAgentResponse = { success: false } | { success: true; agent: Agent }; + +export type UpdateAgentPayload = Omit; + +export type UpdateAgentResponse = { success: false } | { success: true; agent: Agent }; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/http_api/connectors.ts b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/connectors.ts new file mode 100644 index 000000000000..835dd1dca259 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/connectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InferenceConnector } from '@kbn/inference-common'; + +export interface ListConnectorsResponse { + connectors: InferenceConnector[]; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/http_api/conversation.ts b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/conversation.ts new file mode 100644 index 000000000000..2a64901d3cb1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/conversation.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Conversation, ConversationSummary } from '../conversations'; + +export interface ListConversationRequest { + /** + * If specified, will only fetch conversations for this agent. + */ + agentId?: string; +} + +export interface ListConversationResponse { + conversations: ConversationSummary[]; +} + +export type GetConversationResponse = Conversation; diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/http_api/integrations.ts b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/integrations.ts new file mode 100644 index 000000000000..94d49d9de490 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/http_api/integrations.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IntegrationConfiguration } from '@kbn/wci-common'; +import type { Integration } from '../integrations'; + +export type IntegrationType = string; + +export interface ListIntegrationsResponse { + integrations: Integration[]; +} + +export interface CreateIntegrationPayload { + type: IntegrationType; + name: string; + description: string; + configuration: IntegrationConfiguration; +} + +export type CreateIntegrationResponse = Integration; + +export interface UpdateIntegrationPayload { + name?: string; + description?: string; + configuration?: IntegrationConfiguration; +} + +export type UpdateIntegrationResponse = Integration; + +export type GetIntegrationResponse = Integration; + +export interface DeleteIntegrationResponse { + success: boolean; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/integrations.ts b/x-pack/solutions/chat/plugins/workchat-app/common/integrations.ts new file mode 100644 index 000000000000..8ddbd9289836 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/integrations.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType, IntegrationConfiguration } from '@kbn/wci-common'; +export type { Integration } from '@kbn/wci-common'; + +export interface IntegrationCreateRequest { + id?: string; + name: string; + type: IntegrationType; + description: string; + configuration: IntegrationConfiguration; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/common/shared.ts b/x-pack/solutions/chat/plugins/workchat-app/common/shared.ts new file mode 100644 index 000000000000..a4f78d766347 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/common/shared.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface UserNameAndId { + /** id */ + id: string; + /** username */ + name: string; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/jest.config.js b/x-pack/solutions/chat/plugins/workchat-app/jest.config.js new file mode 100644 index 000000000000..b56cf8a7cdf9 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/x-pack/solutions/chat/plugins/workchat-app'], + testMatch: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/kibana.jsonc b/x-pack/solutions/chat/plugins/workchat-app/kibana.jsonc new file mode 100644 index 000000000000..f8c128e45d7c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/kibana.jsonc @@ -0,0 +1,17 @@ +{ + "type": "plugin", + "id": "@kbn/workchat-app", + "owner": "@elastic/search-kibana", + "group": "chat", + "visibility": "private", + "description": "WorkChat application", + "plugin": { + "id": "workchatApp", + "server": true, + "browser": true, + "configPath": ["xpack", "workchatApp"], + "requiredPlugins": ["inference", "actions", "features"], + "optionalPlugins": [], + "requiredBundles": ["kibanaReact"] + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/package.json b/x-pack/solutions/chat/plugins/workchat-app/package.json new file mode 100644 index 000000000000..0e72f9bc92bd --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/workchat-app", + "version": "1.0.0", + "license": "Elastic License 2.0", + "private": true, + "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../../../../scripts/plugin_helpers", + "kbn": "node ../../../../../scripts/kbn" + } +} \ No newline at end of file diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/app_paths.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/app_paths.ts new file mode 100644 index 000000000000..4fbb27c67a5c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/app_paths.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Single source of truth for all url building logic in the app. + */ +export const appPaths = { + home: '/', + + chat: { + new: ({ agentId }: { agentId: string }) => `/agents/${agentId}/chat`, + conversation: ({ agentId, conversationId }: { agentId: string; conversationId: string }) => + `/agents/${agentId}/chat/${conversationId}`, + }, + + agents: { + list: '/agents', + create: '/agents/create', + edit: ({ agentId }: { agentId: string }) => `/agents/${agentId}/edit`, + }, + + integrations: { + list: '/integrations', + create: '/integrations/create', + edit: ({ integrationId }: { integrationId: string }) => `/integrations/${integrationId}/edit`, + }, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/edition/agent_edit_view.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/edition/agent_edit_view.tsx new file mode 100644 index 000000000000..9e76d43e20b3 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/edition/agent_edit_view.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { + EuiButton, + EuiFieldText, + EuiTextArea, + EuiFlexGroup, + EuiPanel, + EuiFlexItem, + EuiSpacer, + EuiForm, + EuiFormRow, + EuiSelect, + EuiDescribedFormGroup, +} from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { useNavigation } from '../../../hooks/use_navigation'; +import { useKibana } from '../../../hooks/use_kibana'; +import { useBreadcrumb } from '../../../hooks/use_breadcrumbs'; +import { useAgentEdition } from '../../../hooks/use_agent_edition'; +import { appPaths } from '../../../app_paths'; +import { agentLabels } from '../i18n'; + +interface AgentEditViewProps { + agentId: string | undefined; +} + +export const AgentEditView: React.FC = ({ agentId }) => { + const { navigateToWorkchatUrl, createWorkchatUrl } = useNavigation(); + const { + services: { notifications }, + } = useKibana(); + + const breadcrumb = useMemo(() => { + return [ + { text: agentLabels.breadcrumb.agentsPill, href: createWorkchatUrl(appPaths.agents.list) }, + agentId + ? { text: agentLabels.breadcrumb.editAgentPill } + : { text: agentLabels.breadcrumb.createAgensPill }, + ]; + }, [agentId, createWorkchatUrl]); + + useBreadcrumb(breadcrumb); + + const handleCancel = useCallback(() => { + navigateToWorkchatUrl('/agents'); + }, [navigateToWorkchatUrl]); + + const onSaveSuccess = useCallback(() => { + notifications.toasts.addSuccess( + agentId + ? agentLabels.notifications.agentUpdatedToastText + : agentLabels.notifications.agentCreatedToastText + ); + navigateToWorkchatUrl('/agents'); + }, [agentId, navigateToWorkchatUrl, notifications]); + + const { editState, setFieldValue, submit, isSubmitting } = useAgentEdition({ + agentId, + onSaveSuccess, + }); + + const onSubmit = useCallback( + (event: React.FormEvent) => { + event.preventDefault(); + if (isSubmitting) { + return; + } + submit(); + }, + [submit, isSubmitting] + ); + + return ( + + + + + + + Base configuration} + description="Configure your agent" + > + + setFieldValue('name', e.target.value)} + /> + + + setFieldValue('description', e.target.value)} + /> + + + setFieldValue('public', e.target.value === 'public')} + /> + + + + + + Customization} + description="Optional parameters to customize the agent" + > + + setFieldValue('systemPrompt', e.target.value)} + /> + + + + + + + + + {agentLabels.editView.cancelButtonLabel} + + + + + {agentLabels.editView.saveButtonLabel} + + + + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/i18n.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/i18n.ts new file mode 100644 index 000000000000..a7ba059104a2 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/i18n.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const agentLabels = { + breadcrumb: { + agentsPill: i18n.translate('workchatApp.agents.breadcrumb.agents', { + defaultMessage: 'Agents', + }), + editAgentPill: i18n.translate('workchatApp.agents.breadcrumb.editAgent', { + defaultMessage: 'Edit agent', + }), + createAgensPill: i18n.translate('workchatApp.agents.breadcrumb.createAgent', { + defaultMessage: 'Create agent', + }), + }, + notifications: { + agentCreatedToastText: i18n.translate( + 'workchatApp.agents.notifications.agentCreatedToastText', + { + defaultMessage: 'Agent created', + } + ), + agentUpdatedToastText: i18n.translate( + 'workchatApp.agents.notifications.agentCreatedToastText', + { + defaultMessage: 'Agent updated', + } + ), + }, + editView: { + createAgentTitle: i18n.translate('workchatApp.agents.editView.createTitle', { + defaultMessage: 'Create a new agent', + }), + editAgentTitle: i18n.translate('workchatApp.agents.editView.editTitle', { + defaultMessage: 'Edit agent', + }), + cancelButtonLabel: i18n.translate('workchatApp.agents.editView.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + saveButtonLabel: i18n.translate('workchatApp.agents.editView.saveButtonLabel', { + defaultMessage: 'Save', + }), + }, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/listing/agent_list_view.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/listing/agent_list_view.tsx new file mode 100644 index 000000000000..3c885c27c2f4 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/agents/listing/agent_list_view.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiButton } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { Agent } from '../../../../../common/agents'; +import { useNavigation } from '../../../hooks/use_navigation'; +import { appPaths } from '../../../app_paths'; + +interface AgentListViewProps { + agents: Agent[]; +} + +export const AgentListView: React.FC = ({ agents }) => { + const { navigateToWorkchatUrl } = useNavigation(); + const columns: Array> = [ + { field: 'name', name: 'Name' }, + { field: 'description', name: 'Description' }, + { field: 'user.name', name: 'Created by' }, + { field: 'public', name: 'Public' }, + { + name: 'Actions', + actions: [ + { + name: 'Edit', + description: 'Edit this agent', + isPrimary: true, + icon: 'documentEdit', + type: 'icon', + onClick: ({ id }) => { + navigateToWorkchatUrl(appPaths.agents.edit({ agentId: id })); + }, + 'data-test-subj': 'agentListTable-edit-btn', + }, + ], + }, + ]; + + return ( + + + + + { + return navigateToWorkchatUrl('/agents/create'); + }} + > + Create new agent + + + + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat.tsx new file mode 100644 index 000000000000..96188c80006d --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useRef, useEffect } from 'react'; +import { css } from '@emotion/css'; +import { EuiFlexItem, EuiPanel, useEuiTheme, euiScrollBarStyles } from '@elastic/eui'; +import type { AuthenticatedUser } from '@kbn/core/public'; +import { ConversationEventChanges } from '../../../../common/chat_events'; +import { useConversation } from '../../hooks/use_conversation'; +import { useStickToBottom } from '../../hooks/use_stick_to_bottom'; +import { ChatInputForm } from './chat_input_form'; +import { ChatConversation } from './chat_conversation'; + +interface ChatProps { + agentId: string; + conversationId: string | undefined; + connectorId: string | undefined; + currentUser: AuthenticatedUser | undefined; + onConversationUpdate: (changes: ConversationEventChanges) => void; +} + +const fullHeightClassName = css` + height: 100%; +`; + +const panelClassName = css` + min-height: 100%; +`; + +const scrollContainerClassName = (scrollBarStyles: string) => css` + overflow-y: auto; + ${scrollBarStyles} +`; + +export const Chat: React.FC = ({ + agentId, + conversationId, + currentUser, + onConversationUpdate, + connectorId, +}) => { + const { sendMessage, conversationEvents, chatStatus } = useConversation({ + conversationId, + connectorId, + agentId, + onConversationUpdate, + }); + + const theme = useEuiTheme(); + const scrollBarStyles = euiScrollBarStyles(theme); + + const scrollContainerRef = useRef(null); + + const { setStickToBottom } = useStickToBottom({ + defaultState: true, + scrollContainer: scrollContainerRef.current, + }); + + useEffect(() => { + setStickToBottom(true); + }, [conversationId, setStickToBottom]); + + const onSubmit = useCallback( + (message: string) => { + setStickToBottom(true); + sendMessage(message); + }, + [sendMessage, setStickToBottom] + ); + + return ( + <> + +
+ + + +
+
+ + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation.tsx new file mode 100644 index 000000000000..ce585117cb4a --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import type { AuthenticatedUser } from '@kbn/core/public'; +import type { ConversationEvent } from '../../../../common/conversation_events'; +import { getChartConversationItems } from '../../utils/get_chart_conversation_items'; +import { ChatConversationItem } from './chat_conversation_item'; +import type { ChatStatus } from '../../hooks/use_chat'; + +interface ChatConversationProps { + conversationEvents: ConversationEvent[]; + chatStatus: ChatStatus; + currentUser: AuthenticatedUser | undefined; +} + +export const ChatConversation: React.FC = ({ + conversationEvents, + chatStatus, + currentUser, +}) => { + const conversationItems = useMemo(() => { + return getChartConversationItems({ conversationEvents, chatStatus }); + }, [conversationEvents, chatStatus]); + + return ( + + {conversationItems.map((conversationItem) => { + return ( + + ); + })} + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_item.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_item.tsx new file mode 100644 index 000000000000..ad1d12591a16 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_item.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { AuthenticatedUser } from '@kbn/core/public'; +import { + type ConversationItem, + isUserMessageItem, + isAssistantMessageItem, + isToolCallItem, +} from '../../utils/conversation_items'; +import { ChatConversationMessage } from './chat_conversation_message'; +import { ChatConversationToolCall } from './chat_conversation_tool_call'; + +interface ChatConversationItemProps { + item: ConversationItem; + currentUser: AuthenticatedUser | undefined; +} + +export const ChatConversationItem: React.FC = ({ + item, + currentUser, +}) => { + if (isUserMessageItem(item) || isAssistantMessageItem(item)) { + return ; + } + if (isToolCallItem(item)) { + return ; + } + return undefined; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_message.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_message.tsx new file mode 100644 index 000000000000..4130869e5580 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_message.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiComment, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { AuthenticatedUser } from '@kbn/core/public'; +import { ChatMessageText } from './chat_message_text'; +import { ChatMessageAvatar } from './chat_message_avatar'; +import { + type UserMessageConversationItem, + type AssistantMessageConversationItem, + isUserMessageItem, +} from '../../utils/conversation_items'; + +type UserOrAssistantMessageItem = UserMessageConversationItem | AssistantMessageConversationItem; + +interface ChatConversationMessageProps { + message: UserOrAssistantMessageItem; + currentUser: AuthenticatedUser | undefined; +} + +const getUserLabel = (item: UserOrAssistantMessageItem) => { + if (isUserMessageItem(item)) { + return i18n.translate('xpack.workchatApp.chat.messages.userLabel', { + defaultMessage: 'You', + }); + } + return i18n.translate('xpack.workchatApp.chat.messages.assistantLabel', { + defaultMessage: 'WorkChat', + }); +}; + +export const ChatConversationMessage: React.FC = ({ + message, + currentUser, +}) => { + const userMessage = useMemo(() => { + return isUserMessageItem(message); + }, [message]); + + const messageContent = useMemo(() => { + return message.message.content; + }, [message]); + + return ( + + } + event="" + eventColor={userMessage ? 'primary' : 'subdued'} + actions={<>} + > + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_tool_call.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_tool_call.tsx new file mode 100644 index 000000000000..4c427cd4809b --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_conversation_tool_call.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiComment } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { ToolCallConversationItem } from '../../utils/conversation_items'; +import { useIntegrationToolView } from '../../hooks/use_integration_tool_view'; +import { ChatMessageAvatar } from './chat_message_avatar'; + +interface ChatConversationMessageProps { + toolCall: ToolCallConversationItem; +} + +const assistantLabel = i18n.translate('xpack.workchatApp.chat.messages.assistantLabel', { + defaultMessage: 'WorkChat', +}); + +export const ChatConversationToolCall: React.FC = ({ toolCall }) => { + const ToolView = useIntegrationToolView(toolCall.toolCall.toolName); + + return ( + + } + event={} + eventColor="transparent" + actions={<>} + /> + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_default_tool_call.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_default_tool_call.tsx new file mode 100644 index 000000000000..452a8b4b891f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_default_tool_call.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { css } from '@emotion/css'; +import { EuiText, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { IntegrationToolComponentProps } from '@kbn/wci-browser'; + +const bold = css` + font-weight: bold; +`; + +const italic = css` + font-style: italic; +`; + +/** + * Component used as default rendered for the `useIntegrationToolView` hook. + */ +export const ChatDefaultToolCallRendered: React.FC< + Omit +> = ({ toolCall, toolResult }) => { + const toolNode = ( + + {toolCall.toolName} + + ); + const argsNode = ( + + {JSON.stringify(toolCall.args)} + + ); + + if (toolResult) { + return ( + + + + ); + } else { + return ( + + + + ); + } +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header.tsx new file mode 100644 index 000000000000..5728c8be0493 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiPanel } from '@elastic/eui'; +import type { Agent } from '../../../../common/agents'; +import { ChatHeaderConnectorSelector } from './chat_header_connector_selector'; + +interface ChatHeaderProps { + agent?: Agent; + connectorId: string | undefined; + onConnectorChange: (connectorId: string) => void; +} + +export const ChatHeader: React.FC = ({ + agent, + connectorId, + onConnectorChange, +}) => { + return ( + + + + + +

{agent?.name}

+
+
+ + + + + You know, for chat! + +
+
+
+ ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header_connector_selector.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header_connector_selector.tsx new file mode 100644 index 000000000000..ba54b0915b55 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_header_connector_selector.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useEffect, useCallback } from 'react'; +import { EuiSuperSelect, EuiText, useEuiTheme, EuiSuperSelectOption } from '@elastic/eui'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { css } from '@emotion/css'; +import { useConnectors } from '../../hooks/use_connectors'; + +const selectInputDisplayClassName = css` + margin-right: 8px; + overflow: hidden; + text-overflow: ellipsis; +`; + +interface ChatHeaderConnectorSelectorProps { + connectorId: string | undefined; + onConnectorChange: (connectorId: string) => void; +} + +export const ChatHeaderConnectorSelector: React.FC = ({ + connectorId, + onConnectorChange, +}) => { + const { connectors } = useConnectors(); + const { euiTheme } = useEuiTheme(); + const [lastSelectedConnectorId, setLastSelectedConnectorId] = useLocalStorage( + 'workchat.lastSelectedConnectorId' + ); + + useEffect(() => { + if (connectors.length && !connectorId) { + onConnectorChange(lastSelectedConnectorId ?? connectors[0].connectorId); + } + }, [connectorId, connectors, onConnectorChange, lastSelectedConnectorId]); + + const onValueChange = useCallback( + (newConnectorId: string) => { + onConnectorChange(newConnectorId); + setLastSelectedConnectorId(newConnectorId); + }, + [onConnectorChange, setLastSelectedConnectorId] + ); + + const options = useMemo(() => { + return connectors.map>((connector) => { + return { + value: connector.connectorId, + inputDisplay: ( + + {connector.name} + + ), + }; + }); + }, [connectors, euiTheme]); + + return ( + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_input_form.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_input_form.tsx new file mode 100644 index 000000000000..75e43043c73c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_input_form.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState, KeyboardEvent } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTextArea, + EuiPanel, + keys, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface ChatInputFormProps { + disabled: boolean; + loading: boolean; + onSubmit: (message: string) => void; +} + +export const ChatInputForm: React.FC = ({ disabled, loading, onSubmit }) => { + const [message, setMessage] = useState(''); + + const handleSubmit = useCallback(() => { + if (loading || !message.trim()) { + return; + } + + onSubmit(message); + setMessage(''); + }, [message, loading, onSubmit]); + + const handleChange = useCallback((event: React.ChangeEvent) => { + setMessage(event.currentTarget.value); + }, []); + + const handleTextAreaKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!event.shiftKey && event.key === keys.ENTER) { + event.preventDefault(); + handleSubmit(); + } + }, + [handleSubmit] + ); + + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_avatar.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_avatar.tsx new file mode 100644 index 000000000000..7ef95d46e8be --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_avatar.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiLoadingSpinner, EuiAvatar } from '@elastic/eui'; +import { UserAvatar } from '@kbn/user-profile-components'; +import type { AuthenticatedUser } from '@kbn/core/public'; +import { AssistantAvatar } from '@kbn/ai-assistant-icon'; + +interface ChatMessageAvatarProps { + eventType: 'user' | 'assistant' | 'tool'; + currentUser: Pick | undefined; + loading: boolean; +} + +export function ChatMessageAvatar({ eventType, currentUser, loading }: ChatMessageAvatarProps) { + if (loading) { + return ; + } + + switch (eventType) { + case 'user': + return ; + case 'assistant': + return ; + case 'tool': + return ; + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_text.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_text.tsx new file mode 100644 index 000000000000..cdb488645775 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_message_text.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import type { Code, InlineCode, Parent, Text } from 'mdast'; +import React, { useMemo } from 'react'; +import type { Node } from 'unist'; +import { + EuiCodeBlock, + EuiTable, + EuiTableRow, + EuiTableRowCell, + EuiTableHeaderCell, + EuiMarkdownFormat, + EuiSpacer, + EuiText, + getDefaultEuiMarkdownParsingPlugins, + getDefaultEuiMarkdownProcessingPlugins, +} from '@elastic/eui'; + +interface Props { + content: string; + loading: boolean; +} + +const cursorCss = css` + @keyframes blink { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + + animation: blink 1s infinite; + width: 10px; + height: 16px; + vertical-align: middle; + display: inline-block; + background: rgba(0, 0, 0, 0.25); +`; + +const Cursor = () => ; + +const CURSOR = ` ᠎  `; + +const loadingCursorPlugin = () => { + const visitor = (node: Node, parent?: Parent) => { + if ('children' in node) { + const nodeAsParent = node as Parent; + nodeAsParent.children.forEach((child) => { + visitor(child, nodeAsParent); + }); + } + + if (node.type !== 'text' && node.type !== 'inlineCode' && node.type !== 'code') { + return; + } + + const textNode = node as Text | InlineCode | Code; + + const indexOfCursor = textNode.value.indexOf(CURSOR); + if (indexOfCursor === -1) { + return; + } + + textNode.value = textNode.value.replace(CURSOR, ''); + + const indexOfNode = parent!.children.indexOf(textNode); + parent!.children.splice(indexOfNode + 1, 0, { + type: 'cursor' as Text['type'], + value: CURSOR, + }); + }; + + return (tree: Node) => { + visitor(tree); + }; +}; + +const esqlLanguagePlugin = () => { + const visitor = (node: Node, parent?: Parent) => { + if ('children' in node) { + const nodeAsParent = node as Parent; + nodeAsParent.children.forEach((child) => { + visitor(child, nodeAsParent); + }); + } + + if (node.type === 'code' && node.lang === 'esql') { + node.type = 'esql'; + } else if (node.type === 'code') { + // switch to type that allows us to control rendering + node.type = 'codeBlock'; + } + }; + + return (tree: Node) => { + visitor(tree); + }; +}; + +export function ChatMessageText({ loading, content }: Props) { + const containerClassName = css` + overflow-wrap: anywhere; + `; + + const { parsingPluginList, processingPluginList } = useMemo(() => { + const parsingPlugins = getDefaultEuiMarkdownParsingPlugins(); + const processingPlugins = getDefaultEuiMarkdownProcessingPlugins(); + + const { components } = processingPlugins[1][1]; + + processingPlugins[1][1].components = { + ...components, + cursor: Cursor, + codeBlock: (props) => { + return ( + <> + {props.value} + + + ); + }, + esql: (props) => { + return ( + <> + {props.value} + + + ); + }, + table: (props) => ( + <> + + + + ), + th: (props) => { + const { children, ...rest } = props; + return {children}; + }, + tr: (props) => , + td: (props) => { + const { children, ...rest } = props; + return ( + + {children} + + ); + }, + }; + + return { + parsingPluginList: [loadingCursorPlugin, esqlLanguagePlugin, ...parsingPlugins], + processingPluginList: processingPlugins, + }; + }, []); + + return ( + + + {`${content}${loading ? CURSOR : ''}`} + + + ); +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_view.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_view.tsx new file mode 100644 index 000000000000..5987ba8ee66d --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/chat_view.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { css } from '@emotion/css'; +import React, { useCallback, useState } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { ConversationEventChanges } from '../../../../common/chat_events'; +import { Chat } from './chat'; +import { ChatHeader } from './chat_header'; +import { ConversationList } from './conversation_list'; +import { useCurrentUser } from '../../hooks/use_current_user'; +import { useConversationList } from '../../hooks/use_conversation_list'; +import { useKibana } from '../../hooks/use_kibana'; +import { useAgent } from '../../hooks/use_agent'; + +const newConversationId = 'new'; + +const pageSectionContentClassName = css` + width: 100%; + display: flex; + flex-grow: 1; + padding-top: 0; + padding-bottom: 0; + height: 100%; + max-block-size: calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0))); +`; + +interface WorkchatChatViewProps { + agentId: string; + conversationId: string | undefined; +} + +export const WorkchatChatView: React.FC = ({ agentId, conversationId }) => { + const { + services: { application }, + } = useKibana(); + + const currentUser = useCurrentUser(); + + const { agent } = useAgent({ agentId }); + const { conversations, refresh: refreshConversations } = useConversationList({ agentId }); + + const onConversationUpdate = useCallback( + (changes: ConversationEventChanges) => { + if (!conversationId) { + application.navigateToApp('workchat', { path: `/agents/${agentId}/chat/${changes.id}` }); + } + refreshConversations(); + }, + [agentId, application, conversationId, refreshConversations] + ); + + const [connectorId, setConnectorId] = useState(); + + return ( + + + { + application.navigateToApp('workchat', { path: `/agents/${agentId}/chat/${newConvId}` }); + }} + onNewConversationSelect={() => { + application.navigateToApp('workchat', { + path: `/agents/${agentId}/chat/${newConversationId}`, + }); + }} + /> + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/conversation_list.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/conversation_list.tsx new file mode 100644 index 000000000000..2682b4b704bb --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/chat/conversation_list.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, type MouseEvent } from 'react'; +import { css } from '@emotion/css'; +import { + EuiText, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiListGroup, + EuiListGroupItem, + EuiButton, + useEuiTheme, + euiScrollBarStyles, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { ConversationSummary } from '../../../../common/conversations'; +import { sortAndGroupConversations } from '../../utils/sort_and_group_conversations'; + +interface ConversationListProps { + conversations: ConversationSummary[]; + activeConversationId?: string; + onConversationSelect?: (conversationId: string) => void; + onNewConversationSelect?: () => void; +} + +const scrollContainerClassName = (scrollBarStyles: string) => css` + overflow-y: auto; + ${scrollBarStyles} +`; + +const fullHeightClassName = css` + height: 100%; +`; + +const containerClassName = css` + height: 100%; + width: 100%; +`; + +const pageSectionContentClassName = css` + width: 100%; + display: flex; + flex-grow: 1; + height: 100%; + max-block-size: calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0))); +`; + +export const ConversationList: React.FC = ({ + conversations, + activeConversationId, + onConversationSelect, + onNewConversationSelect, +}) => { + const handleConversationClick = useCallback( + (e: MouseEvent | MouseEvent, conversationId: string) => { + if (onConversationSelect) { + e.preventDefault(); + onConversationSelect(conversationId); + } + }, + [onConversationSelect] + ); + + const theme = useEuiTheme(); + const scrollBarStyles = euiScrollBarStyles(theme); + + const titleClassName = css` + text-transform: uppercase; + font-weight: ${theme.euiTheme.font.weight.bold}; + `; + + const conversationGroups = useMemo(() => { + return sortAndGroupConversations(conversations); + }, [conversations]); + + return ( + + + + + {i18n.translate('xpack.workchatApp.conversationList.conversationTitle', { + defaultMessage: 'Conversations', + })} + + + + + + {conversationGroups.map(({ conversations: groupConversations, dateLabel }) => { + return ( + + + +

{dateLabel}

+
+
+ + {groupConversations.map((conversation) => ( + handleConversationClick(event, conversation.id)} + label={conversation.title} + size="s" + isActive={conversation.id === activeConversationId} + showToolTip + /> + ))} + + +
+ ); + })} +
+
+ + onNewConversationSelect && onNewConversationSelect()} + > + {i18n.translate('xpack.workchatApp.newConversationButtonLabel', { + defaultMessage: 'New conversation', + })} + + +
+
+ ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_agent_section.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_agent_section.tsx new file mode 100644 index 000000000000..b7dc2fb2a343 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_agent_section.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiLink, + EuiTitle, + EuiButton, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { Agent } from '../../../../common/agents'; +import { useNavigation } from '../../hooks/use_navigation'; +import { useAgentList } from '../../hooks/use_agent_list'; +import { useCapabilities } from '../../hooks/use_capabilities'; +import { appPaths } from '../../app_paths'; + +export const HomeAgentSection: React.FC<{}> = () => { + const { createWorkchatUrl, navigateToWorkchatUrl } = useNavigation(); + const { agents } = useAgentList(); + const { showManagement } = useCapabilities(); + + const columns: Array> = [ + { + field: 'name', + name: 'Name', + render: (value, agent) => { + return ( + + {value} + + ); + }, + }, + { field: 'description', name: 'Description' }, + { field: 'user.name', name: 'Created by' }, + ]; + + return ( + + +

Your agents

+
+ + + + {showManagement && ( + + + { + navigateToWorkchatUrl(appPaths.agents.list); + }} + > + Go to agent management + + + + )} +
+ ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_integration_section.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_integration_section.tsx new file mode 100644 index 000000000000..95ff0704b5da --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_integration_section.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiTitle, + EuiButton, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { Integration } from '../../../../common/integrations'; +import { useNavigation } from '../../hooks/use_navigation'; +import { useIntegrationList } from '../../hooks/use_integration_list'; +import { useCapabilities } from '../../hooks/use_capabilities'; +import { appPaths } from '../../app_paths'; + +export const HomeIntegrationSection: React.FC<{}> = () => { + const { navigateToWorkchatUrl } = useNavigation(); + const { integrations } = useIntegrationList(); + const { showManagement } = useCapabilities(); + + const columns: Array> = [ + { + field: 'name', + name: 'Name', + }, + { + field: 'type', + name: 'Type', + }, + { + field: 'description', + name: 'Description', + }, + ]; + + return ( + + +

Your integrations

+
+ + + + {showManagement && ( + + + { + navigateToWorkchatUrl(appPaths.integrations.list); + }} + > + Go to integration management + + + + )} +
+ ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_view.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_view.tsx new file mode 100644 index 000000000000..5ea012dd69e9 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/home/home_view.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { HomeAgentSection } from './home_agent_section'; +import { HomeIntegrationSection } from './home_integration_section'; + +export const WorkChatHomeView: React.FC<{}> = () => { + return ( + + + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/edit/integration_edit_view.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/edit/integration_edit_view.tsx new file mode 100644 index 000000000000..7339a042e169 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/edit/integration_edit_view.tsx @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback, useState } from 'react'; +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiPanel, + EuiFlexItem, + EuiSpacer, + EuiForm, + EuiFormRow, + EuiDescribedFormGroup, + EuiSelect, + EuiConfirmModal, + EuiButtonEmpty, +} from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { IntegrationType } from '@kbn/wci-common'; +import { FormProvider, useForm, Controller } from 'react-hook-form'; +import { useNavigation } from '../../../hooks/use_navigation'; +import { useKibana } from '../../../hooks/use_kibana'; +import { useBreadcrumb } from '../../../hooks/use_breadcrumbs'; +import { IntegrationEditState, useIntegrationEdit } from '../../../hooks/use_integration_edit'; +import { useIntegrationDelete } from '../../../hooks/use_integration_delete'; +import { useIntegrationConfigurationForm } from '../../../hooks/use_integration_configuration_form'; +import { appPaths } from '../../../app_paths'; +import { integrationLabels } from '../i18n'; +import { integrationTypeToLabel } from '../utils'; + +interface IntegrationEditViewProps { + integrationId: string | undefined; +} + +export const IntegrationEditView: React.FC = ({ integrationId }) => { + const { navigateToWorkchatUrl } = useNavigation(); + const { + services: { notifications }, + } = useKibana(); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + + const breadcrumb = useMemo(() => { + return [ + { text: integrationLabels.breadcrumb.integrationsPill, path: appPaths.integrations.list }, + integrationId + ? { text: integrationLabels.breadcrumb.editIntegrationPill } + : { text: integrationLabels.breadcrumb.createIntegrationPill }, + ]; + }, [integrationId]); + + useBreadcrumb(breadcrumb); + + const handleCancel = useCallback(() => { + navigateToWorkchatUrl(appPaths.integrations.list); + }, [navigateToWorkchatUrl]); + + const onSaveSuccess = useCallback(() => { + notifications.toasts.addSuccess( + integrationId + ? integrationLabels.notifications.integrationUpdatedToastText + : integrationLabels.notifications.integrationCreatedToastText + ); + navigateToWorkchatUrl(appPaths.integrations.list); + }, [integrationId, navigateToWorkchatUrl, notifications]); + + const onSaveError = useCallback( + (err: Error) => { + notifications.toasts.addError(err, { + title: 'Error', + }); + }, + [notifications] + ); + + const { state, submit } = useIntegrationEdit({ + integrationId, + onSaveSuccess, + onSaveError, + }); + + const integrationTypes = [ + { value: '', text: 'Pick a type' }, + ...Object.values(IntegrationType).map((type) => ({ + value: type, + text: integrationTypeToLabel(type), + })), + ]; + + const onDeleteSuccess = useCallback(() => { + notifications.toasts.addSuccess(integrationLabels.notifications.integrationDeletedToastText); + navigateToWorkchatUrl(appPaths.integrations.list); + }, [navigateToWorkchatUrl, notifications]); + + const onDeleteError = useCallback( + (err: Error) => { + notifications.toasts.addError(err, { + title: 'Error deleting integration', + }); + }, + [notifications] + ); + + const { deleteIntegration, isDeleting } = useIntegrationDelete({ + onDeleteSuccess, + onDeleteError, + }); + + const handleDelete = useCallback(() => { + if (integrationId) { + deleteIntegration(integrationId); + } + setIsDeleteModalVisible(false); + }, [deleteIntegration, integrationId]); + + const showDeleteModal = () => { + setIsDeleteModalVisible(true); + }; + + const closeDeleteModal = () => { + setIsDeleteModalVisible(false); + }; + + const formMethods = useForm({ + values: state, + }); + + const { + handleSubmit, + formState: { isSubmitting }, + control, + watch, + } = formMethods; + + const ConfigurationForm = useIntegrationConfigurationForm(watch('type')); + + return ( + + + + + + + submit(data))}> + Base configuration} + description="Configure your integration" + > + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + + + + {ConfigurationForm && } + + + + + + + + + {integrationLabels.editView.cancelButtonLabel} + + + {integrationId && ( + + + Delete + + + )} + + + + + {integrationLabels.editView.saveButtonLabel} + + + + + + {isDeleteModalVisible && ( + +

+ Are you sure you want to delete this integration? This action cannot be undone. +

+
+ )} +
+
+
+
+ ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/i18n.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/i18n.ts new file mode 100644 index 000000000000..3e41ebf36b5a --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/i18n.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const integrationLabels = { + breadcrumb: { + integrationsPill: i18n.translate('workchatApp.integrations.breadcrumb.integrations', { + defaultMessage: 'Integrations', + }), + editIntegrationPill: i18n.translate('workchatApp.integrations.breadcrumb.editIntegration', { + defaultMessage: 'Edit integration', + }), + createIntegrationPill: i18n.translate('workchatApp.integrations.breadcrumb.createIntegration', { + defaultMessage: 'Create integration', + }), + }, + notifications: { + integrationCreatedToastText: i18n.translate( + 'workchatApp.integrations.notifications.integrationCreatedToastText', + { + defaultMessage: 'Integration created', + } + ), + integrationUpdatedToastText: i18n.translate( + 'workchatApp.integrations.notifications.integrationUpdatedToastText', + { + defaultMessage: 'Integration updated', + } + ), + integrationDeletedToastText: i18n.translate( + 'workchatApp.integrations.notifications.integrationDeletedToastText', + { + defaultMessage: 'Integration successfully deleted', + } + ), + }, + editView: { + createIntegrationTitle: i18n.translate('workchatApp.integrations.editView.createTitle', { + defaultMessage: 'Create a new integration', + }), + editIntegrationTitle: i18n.translate('workchatApp.integrations.editView.editTitle', { + defaultMessage: 'Edit integration', + }), + cancelButtonLabel: i18n.translate('workchatApp.integrations.editView.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + saveButtonLabel: i18n.translate('workchatApp.integrations.editView.saveButtonLabel', { + defaultMessage: 'Save', + }), + }, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/listing/integration_list_view.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/listing/integration_list_view.tsx new file mode 100644 index 000000000000..79a31377e2e0 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/listing/integration_list_view.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiButton } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { IntegrationType } from '@kbn/wci-common'; +import type { Integration } from '../../../../../common/integrations'; +import { useNavigation } from '../../../hooks/use_navigation'; +import { appPaths } from '../../../app_paths'; +import { integrationTypeToLabel } from '../utils'; + +interface IntegrationListViewProps { + integrations: Integration[]; +} + +export const IntegrationListView: React.FC = ({ integrations }) => { + const { navigateToWorkchatUrl } = useNavigation(); + const columns: Array> = [ + { field: 'name', name: 'Name' }, + { + field: 'type', + name: 'Type', + render: (type: IntegrationType) => integrationTypeToLabel(type), + }, + { field: 'description', name: 'Description' }, + { + name: 'Actions', + actions: [ + { + name: 'Edit', + description: 'Edit this integration', + isPrimary: true, + icon: 'documentEdit', + type: 'icon', + onClick: ({ id }) => { + navigateToWorkchatUrl(appPaths.integrations.edit({ integrationId: id })); + }, + 'data-test-subj': 'integrationListTable-edit-btn', + }, + ], + }, + ]; + + return ( + + + + + { + return navigateToWorkchatUrl(appPaths.integrations.create); + }} + > + Create new integration + + + + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/utils.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/utils.ts new file mode 100644 index 000000000000..d93c93034b7b --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/components/integrations/utils.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType } from '@kbn/wci-common'; + +export const integrationTypeToLabel = (type: IntegrationType) => { + switch (type) { + case IntegrationType.index_source: + return 'Index Source'; + case IntegrationType.external_server: + return 'External Server'; + case IntegrationType.salesforce: + return 'Salesforce'; + default: + return type; + } +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/context/workchat_services_context.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/context/workchat_services_context.tsx new file mode 100644 index 000000000000..420282121d0c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/context/workchat_services_context.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createContext } from 'react'; +import type { WorkChatServices } from '../../services/types'; + +export const WorkChatServicesContext = createContext(undefined); diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent.ts new file mode 100644 index 000000000000..8523789d1e72 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { useWorkChatServices } from './use_workchat_service'; +import { queryKeys } from '../query_keys'; + +export const useAgent = ({ agentId }: { agentId: string }) => { + const { agentService } = useWorkChatServices(); + + const { data: agent, isLoading } = useQuery({ + queryKey: queryKeys.agents.details(agentId), + queryFn: async () => { + return agentService.get(agentId); + }, + }); + + return { + agent, + isLoading, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_edition.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_edition.ts new file mode 100644 index 000000000000..7264041c1581 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_edition.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState, useEffect } from 'react'; +import { useWorkChatServices } from './use_workchat_service'; +import type { Agent } from '../../../common/agents'; + +export interface AgentEditState { + name: string; + description: string; + systemPrompt: string; + public: boolean; +} + +const emptyState = (): AgentEditState => { + return { + name: '', + description: '', + systemPrompt: '', + public: false, + }; +}; + +export const useAgentEdition = ({ + agentId, + onSaveSuccess, +}: { + agentId: string | undefined; + onSaveSuccess: (agent: Agent) => void; +}) => { + const { agentService } = useWorkChatServices(); + + const [editState, setEditState] = useState(emptyState()); + const [isSubmitting, setSubmitting] = useState(false); + + useEffect(() => { + const fetchAgent = async () => { + if (agentId) { + const agent = await agentService.get(agentId); + setEditState({ + name: agent.name, + description: agent.description, + systemPrompt: agent.configuration.systemPrompt ?? '', + public: agent.public, + }); + } + }; + fetchAgent(); + }, [agentId, agentService]); + + const setFieldValue = (key: T, value: AgentEditState[T]) => { + setEditState((previous) => ({ ...previous, [key]: value })); + }; + + const submit = useCallback(() => { + setSubmitting(true); + + const payload = { + name: editState.name, + description: editState.description, + configuration: { + systemPrompt: editState.systemPrompt, + }, + public: editState.public, + }; + + (agentId ? agentService.update(agentId, payload) : agentService.create(payload)).then( + (response) => { + setSubmitting(false); + if (response.success) { + onSaveSuccess(response.agent); + } + }, + (err) => { + setSubmitting(false); + } + ); + }, [agentId, editState, agentService, onSaveSuccess]); + + return { + editState, + isSubmitting, + setFieldValue, + submit, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_list.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_list.ts new file mode 100644 index 000000000000..7d5f31e00913 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_agent_list.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { queryKeys } from '../query_keys'; +import { useWorkChatServices } from './use_workchat_service'; + +export const useAgentList = () => { + const { agentService } = useWorkChatServices(); + + const { + data: agents, + isLoading, + refetch: refresh, + } = useQuery({ + queryKey: queryKeys.agents.list, + queryFn: async () => { + return agentService.list(); + }, + initialData: () => [], + }); + + return { + agents, + isLoading, + refresh, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_breadcrumbs.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_breadcrumbs.ts new file mode 100644 index 000000000000..f37716aafaab --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_breadcrumbs.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { ChromeBreadcrumb } from '@kbn/core/public'; +import { WORKCHAT_APP_ID } from '../../../common/features'; +import { useKibana } from './use_kibana'; + +interface WorkchatBreadcrumb { + text: string; + path?: string; +} + +export const useBreadcrumb = (breadcrumbs: WorkchatBreadcrumb[]) => { + const { + services: { chrome, application }, + } = useKibana(); + + const getUrl = useCallback( + (path: string) => { + return application.getUrlForApp(WORKCHAT_APP_ID, { path }); + }, + [application] + ); + + const appUrl = useMemo(() => { + return getUrl(''); + }, [getUrl]); + + const baseCrumbs: ChromeBreadcrumb[] = useMemo(() => { + return [ + { + text: i18n.translate('workchatApp.breadcrumb.workchat', { defaultMessage: 'WorkChat' }), + href: appUrl, + }, + ]; + }, [appUrl]); + + useEffect(() => { + const additionalCrumbs = breadcrumbs.map((crumb) => { + return { + text: crumb.text, + href: crumb.path ? getUrl(crumb.path) : undefined, + }; + }); + + chrome.setBreadcrumbs([...baseCrumbs, ...additionalCrumbs], { + project: { value: additionalCrumbs.length ? additionalCrumbs : baseCrumbs, absolute: true }, + }); + return () => { + chrome.setBreadcrumbs([]); + }; + }, [chrome, baseCrumbs, breadcrumbs, getUrl]); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_capabilities.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_capabilities.ts new file mode 100644 index 000000000000..eedcdb690d13 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_capabilities.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { uiCapabilities } from '../../../common/features'; +import { hasWorkchatCapability } from '../utils/has_capability'; +import { useKibana } from './use_kibana'; + +type UiCapabilitiesKeys = keyof typeof uiCapabilities; + +export const useCapabilities = (): Record => { + const { + services: { + application: { capabilities }, + }, + } = useKibana(); + + const caps = useMemo(() => { + return Object.entries(uiCapabilities).reduce((map, [key, value]) => { + map[key as UiCapabilitiesKeys] = hasWorkchatCapability(capabilities, value); + return map; + }, {} as Record); + }, [capabilities]); + + return caps; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_chat.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_chat.ts new file mode 100644 index 000000000000..cf86bca436ba --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_chat.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { ConversationCreatedEvent } from '../../../common/chat_events'; +import type { ChatError } from '../../../common/errors'; +import { + type ConversationEvent, + createUserMessage, + createAssistantMessage, + createToolResult, +} from '../../../common/conversation_events'; +import { useWorkChatServices } from './use_workchat_service'; +import { useKibana } from './use_kibana'; + +interface UseChatProps { + conversationId: string | undefined; + agentId: string; + connectorId?: string; + onConversationUpdate: (changes: ConversationCreatedEvent['conversation']) => void; + onError?: (error: ChatError) => void; +} + +export type ChatStatus = 'ready' | 'loading' | 'error'; + +export const useChat = ({ + conversationId, + agentId, + connectorId, + onConversationUpdate, + onError, +}: UseChatProps) => { + const { chatService } = useWorkChatServices(); + const { + services: { notifications }, + } = useKibana(); + const [conversationEvents, setConversationEvents] = useState([]); + const [pendingMessages, setPendingMessages] = useState([]); + const [status, setStatus] = useState('ready'); + + const sendMessage = useCallback( + (nextMessage: string) => { + if (status === 'loading') { + return; + } + + setStatus('loading'); + setConversationEvents((prevEvents) => [ + ...prevEvents, + createUserMessage({ content: nextMessage }), + ]); + + const events$ = chatService.converse({ + nextMessage, + conversationId, + agentId, + connectorId, + }); + + const streamMessages: ConversationEvent[] = []; + + let concatenatedChunks = ''; + + const getAllStreamMessages = () => { + return streamMessages.concat( + concatenatedChunks.length ? [createAssistantMessage({ content: concatenatedChunks })] : [] + ); + }; + + events$.subscribe({ + next: (event) => { + // if (event.type !== 'message_chunk') { + // console.log('*** event', event); + // } + + // chunk received, we append it to the chunk buffer + if (event.type === 'message_chunk') { + concatenatedChunks += event.content_chunk; + setPendingMessages(getAllStreamMessages()); + } + + // full message received - we purge the chunk buffer + // and insert the received message into the temporary list + if (event.type === 'message') { + concatenatedChunks = ''; + streamMessages.push(event.message); + setPendingMessages(getAllStreamMessages()); + } + + if (event.type === 'tool_result') { + concatenatedChunks = ''; + streamMessages.push( + createToolResult({ + toolCallId: event.toolResult.callId, + toolResult: event.toolResult.result, + }) + ); + setPendingMessages(getAllStreamMessages()); + } + + if (event.type === 'conversation_created') { + onConversationUpdate(event.conversation); + } + }, + complete: () => { + setConversationEvents((prevEvents) => [...prevEvents, ...streamMessages]); + setPendingMessages([]); + setStatus('ready'); + }, + error: (err) => { + setConversationEvents((prevEvents) => [...prevEvents, ...streamMessages]); + setPendingMessages([]); + setStatus('error'); + onError?.(err); + + notifications.toasts.addError(err, { + title: i18n.translate('xpack.workchatApp.chat.chatError.title', { + defaultMessage: 'Error loading chat response', + }), + toastMessage: `${err.code} - ${err.message}`, + }); + }, + }); + }, + [ + chatService, + notifications, + status, + agentId, + conversationId, + connectorId, + onConversationUpdate, + onError, + ] + ); + + const setConversationEventsExternal = useCallback((newEvents: ConversationEvent[]) => { + setConversationEvents(newEvents); + setPendingMessages([]); + }, []); + + const allEvents = useMemo(() => { + return [...conversationEvents, ...pendingMessages]; + }, [conversationEvents, pendingMessages]); + + return { + status, + sendMessage, + conversationEvents: allEvents, + setConversationEvents: setConversationEventsExternal, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_connectors.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_connectors.ts new file mode 100644 index 000000000000..3c1300b5253f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_connectors.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { ListConnectorsResponse } from '../../../common/http_api/connectors'; +import { queryKeys } from '../query_keys'; +import { useKibana } from './use_kibana'; + +export const useConnectors = () => { + const { + services: { http }, + } = useKibana(); + + const { data: connectors, isLoading } = useQuery({ + queryKey: queryKeys.connectors.list, + queryFn: async () => { + const response = await http.get('/internal/workchat/connectors'); + return response.connectors; + }, + initialData: () => [], + }); + + return { + connectors, + isLoading, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation.ts new file mode 100644 index 000000000000..39abd706ddfb --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useAbortableAsync } from '@kbn/react-hooks'; +import type { ConversationEventChanges } from '../../../common/chat_events'; +import { useChat } from './use_chat'; +import { useWorkChatServices } from './use_workchat_service'; + +export const useConversation = ({ + agentId, + conversationId, + connectorId, + onConversationUpdate, +}: { + agentId: string; + conversationId: string | undefined; + connectorId?: string; + onConversationUpdate: (update: ConversationEventChanges) => void; +}) => { + const { conversationService } = useWorkChatServices(); + + const onConversationUpdateInternal = useCallback( + (update: ConversationEventChanges) => { + onConversationUpdate(update); + }, + [onConversationUpdate] + ); + + const { + conversationEvents, + setConversationEvents, + sendMessage, + status: chatStatus, + } = useChat({ + agentId, + conversationId, + connectorId, + onConversationUpdate: onConversationUpdateInternal, + }); + + useAbortableAsync(async () => { + // TODO: better conv state management - only has events atm + if (conversationId) { + const conversation = await conversationService.get(conversationId); + setConversationEvents(conversation.events); + } else { + setConversationEvents([]); + } + }, [conversationId, conversationService]); + + return { conversationEvents, chatStatus, sendMessage }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation_list.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation_list.ts new file mode 100644 index 000000000000..ba4ed8e910f9 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_conversation_list.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { queryKeys } from '../query_keys'; +import { useWorkChatServices } from './use_workchat_service'; + +export const useConversationList = ({ agentId }: { agentId: string }) => { + const { conversationService } = useWorkChatServices(); + + const { + data: conversations, + isLoading, + refetch: refresh, + } = useQuery({ + queryKey: queryKeys.conversations.byAgent(agentId), + queryFn: async () => { + return conversationService.list({ agentId }); + }, + initialData: () => [], + }); + + return { + conversations, + isLoading, + refresh, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_current_user.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_current_user.ts new file mode 100644 index 000000000000..44ddd33bc1f6 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_current_user.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { queryKeys } from '../query_keys'; +import { useKibana } from './use_kibana'; + +export const useCurrentUser = () => { + const { + services: { security }, + } = useKibana(); + + const { data: user } = useQuery({ + queryKey: queryKeys.users.current, + queryFn: async () => { + return security.authc.getCurrentUser(); + }, + }); + + return user; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_configuration_form.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_configuration_form.ts new file mode 100644 index 000000000000..1a661818f64c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_configuration_form.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { IntegrationType } from '@kbn/wci-common'; +import { useWorkChatServices } from './use_workchat_service'; + +/** + * Hook to get the configuration form component for a specific integration type + * @param type - The integration type + * @returns The configuration form component or undefined if not available + */ +export const useIntegrationConfigurationForm = (type: string | undefined) => { + const { integrationRegistry } = useWorkChatServices(); + + const configurationForm = useMemo(() => { + if (!type) return undefined; + + const integrationDefinition = integrationRegistry.get(type as IntegrationType); + return integrationDefinition?.getConfigurationForm?.(); + }, [type, integrationRegistry]); + + return configurationForm; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_delete.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_delete.ts new file mode 100644 index 000000000000..6d84f5fb4a07 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_delete.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import { useWorkChatServices } from './use_workchat_service'; + +interface UseIntegrationDeleteProps { + onDeleteSuccess?: () => void; + onDeleteError?: (error: Error) => void; +} + +export const useIntegrationDelete = ({ + onDeleteSuccess, + onDeleteError, +}: UseIntegrationDeleteProps = {}) => { + const [isDeleting, setIsDeleting] = useState(false); + const { integrationService } = useWorkChatServices(); + + const deleteIntegration = useCallback( + async (integrationId: string) => { + setIsDeleting(true); + try { + await integrationService.delete(integrationId); + if (onDeleteSuccess) { + onDeleteSuccess(); + } + } catch (error) { + if (onDeleteError) { + onDeleteError(error as Error); + } + } finally { + setIsDeleting(false); + } + }, + [integrationService, onDeleteSuccess, onDeleteError] + ); + + return { + deleteIntegration, + isDeleting, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_edit.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_edit.ts new file mode 100644 index 000000000000..b7361893961d --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_edit.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState, useEffect } from 'react'; +import { Integration } from '../../../common/integrations'; +import { useWorkChatServices } from './use_workchat_service'; + +export interface IntegrationEditState { + name: string; + description: string; + type: string; + configuration: Record; +} + +const emptyState = (): IntegrationEditState => { + return { + name: '', + description: '', + type: '', + configuration: {}, + }; +}; +export const useIntegrationEdit = ({ + integrationId, + onSaveSuccess, + onSaveError, +}: { + integrationId: string | undefined; + onSaveSuccess: (integration: Integration) => void; + onSaveError: (err: Error) => void; +}) => { + const { integrationService } = useWorkChatServices(); + const [state, setState] = useState(emptyState()); + const [isSubmitting, setSubmitting] = useState(false); + + useEffect(() => { + const fetchIntegration = async () => { + if (integrationId) { + const integration = await integrationService.get(integrationId); + setState({ + name: integration.name, + description: integration.description, + type: integration.type, + configuration: integration.configuration || {}, + }); + } + }; + fetchIntegration(); + }, [integrationId, integrationService]); + + const submit = useCallback( + (updatedIntegration: IntegrationEditState) => { + setSubmitting(true); + + (integrationId + ? integrationService.update(integrationId, { + name: updatedIntegration.name, + description: updatedIntegration.description, + configuration: updatedIntegration.configuration, + }) + : integrationService.create({ + type: updatedIntegration.type, + name: updatedIntegration.name, + description: updatedIntegration.description, + configuration: updatedIntegration.configuration, + }) + ).then( + (response) => { + setSubmitting(false); + onSaveSuccess(response); + }, + (err) => { + setSubmitting(false); + onSaveError(err); + } + ); + }, + [integrationId, integrationService, onSaveSuccess, onSaveError] + ); + + return { + state, + isSubmitting, + submit, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_list.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_list.ts new file mode 100644 index 000000000000..fd8df686102f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_list.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { queryKeys } from '../query_keys'; +import { useWorkChatServices } from './use_workchat_service'; + +export const useIntegrationList = () => { + const { integrationService } = useWorkChatServices(); + + const { + data: integrations, + isLoading, + refetch: refresh, + } = useQuery({ + queryKey: queryKeys.integrations.list, + queryFn: async () => { + return integrationService.list(); + }, + initialData: () => [], + }); + + return { + integrations, + isLoading, + refresh, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_tool_view.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_tool_view.tsx new file mode 100644 index 000000000000..36f74fb62c5b --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_integration_tool_view.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { parseToolName } from '@kbn/wci-common'; +import { IntegrationToolComponentProps } from '@kbn/wci-browser'; +import { useWorkChatServices } from './use_workchat_service'; +import { useIntegrationList } from './use_integration_list'; +import { ChatDefaultToolCallRendered } from '../components/chat/chat_default_tool_call'; + +type WiredToolComponentProps = Omit; + +/** + * Hook to get the component that should be used to render a tool call in the conversation history + */ +export const useIntegrationToolView = ( + fullToolName: string +): React.ComponentType => { + const { integrationRegistry } = useWorkChatServices(); + const { integrationId, toolName } = useMemo(() => parseToolName(fullToolName), [fullToolName]); + const { integrations } = useIntegrationList(); + + const integration = useMemo(() => { + return integrations.find((integ) => integ.id === integrationId); + }, [integrationId, integrations]); + + const ToolRenderedComponent = useMemo(() => { + if (integration) { + const definition = integrationRegistry.get(integration.type); + if (definition) { + return definition.getToolCallComponent(toolName); + } + } + }, [integrationRegistry, integration, toolName]); + + return useMemo(() => { + if (integration && ToolRenderedComponent) { + return (props: WiredToolComponentProps) => ( + + ); + } + return ChatDefaultToolCallRendered; + }, [ToolRenderedComponent, integration]); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_kibana.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_kibana.ts new file mode 100644 index 000000000000..0b334c14ca5f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_kibana.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { WorkChatAppPluginStartDependencies } from '../../types'; + +export type StartServices = CoreStart & { + plugins: WorkChatAppPluginStartDependencies; +}; + +const useTypedKibana = () => useKibana(); + +export { useTypedKibana as useKibana }; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_navigation.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_navigation.ts new file mode 100644 index 000000000000..7fd58e83be2a --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_navigation.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { WORKCHAT_APP_ID } from '../../../common/features'; +import { useKibana } from './use_kibana'; + +export const useNavigation = () => { + const { + services: { application }, + } = useKibana(); + + const navigateToWorkchatUrl = useCallback( + (path: string) => { + application.navigateToApp(WORKCHAT_APP_ID, { path }); + }, + [application] + ); + + const createWorkchatUrl = useCallback( + (path: string) => { + return application.getUrlForApp(WORKCHAT_APP_ID, { path }); + }, + [application] + ); + + return { + createWorkchatUrl, + navigateToWorkchatUrl, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_stick_to_bottom.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_stick_to_bottom.ts new file mode 100644 index 000000000000..e85a03e68ce4 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_stick_to_bottom.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; + +const isAtBottom = (parent: HTMLElement) => + parent.scrollTop + parent.clientHeight >= parent.scrollHeight; + +export const useStickToBottom = ({ + defaultState, + scrollContainer, +}: { + defaultState?: boolean; + scrollContainer: HTMLDivElement | null; +}) => { + const [stickToBottom, setStickToBottom] = useState(defaultState ?? true); + + useEffect(() => { + const parent = scrollContainer?.parentElement; + if (!parent) { + return; + } + + const onScroll = () => { + setStickToBottom(isAtBottom(parent!)); + }; + + parent.addEventListener('scroll', onScroll); + + return () => { + parent.removeEventListener('scroll', onScroll); + }; + }, [scrollContainer]); + + useEffect(() => { + const parent = scrollContainer?.parentElement; + if (!parent) { + return; + } + + if (stickToBottom) { + parent.scrollTop = parent.scrollHeight; + } + }); + + return { + stickToBottom, + setStickToBottom, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_workchat_service.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_workchat_service.ts new file mode 100644 index 000000000000..f362336563c8 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/hooks/use_workchat_service.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext } from 'react'; +import { WorkChatServicesContext } from '../context/workchat_services_context'; + +export const useWorkChatServices = () => { + const services = useContext(WorkChatServicesContext); + if (services === undefined) { + throw new Error( + `WorkChatServicesContext not set. Did you wrap your component in ?` + ); + } + return services; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/index.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/index.ts new file mode 100644 index 000000000000..8bac8a23217f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { registerApp } from './register'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/mount.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/mount.tsx new file mode 100644 index 000000000000..bf94dfb2fba3 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/mount.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { I18nProvider } from '@kbn/i18n-react'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import { CoreStart, ScopedHistory } from '@kbn/core/public'; +import { Router } from '@kbn/shared-ux-router'; +import type { WorkChatServices } from '../services'; +import type { WorkChatAppPluginStartDependencies } from '../types'; +import { WorkChatServicesContext } from './context/workchat_services_context'; +import { WorkchatAppRoutes } from './routes'; + +export const mountApp = async ({ + core, + plugins, + services, + element, + history, +}: { + core: CoreStart; + plugins: WorkChatAppPluginStartDependencies; + services: WorkChatServices; + element: HTMLElement; + history: ScopedHistory; +}) => { + const kibanaServices = { ...core, plugins }; + const queryClient = new QueryClient(); + ReactDOM.render( + + + + + + + + + + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agent_edit_or_create.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agent_edit_or_create.tsx new file mode 100644 index 000000000000..59ada75d3b3e --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agent_edit_or_create.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { AgentEditView } from '../components/agents/edition/agent_edit_view'; + +const newAgentId = 'create'; + +export const WorkChatAgentEditOrCreatePage: React.FC<{}> = () => { + const { agentId: agentIdFromParams } = useParams<{ + agentId: string; + }>(); + + const agentId = useMemo(() => { + return agentIdFromParams === newAgentId ? undefined : agentIdFromParams; + }, [agentIdFromParams]); + + return ; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agents.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agents.tsx new file mode 100644 index 000000000000..686e04eb3465 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/agents.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useBreadcrumb } from '../hooks/use_breadcrumbs'; +import { useAgentList } from '../hooks/use_agent_list'; +import { AgentListView } from '../components/agents/listing/agent_list_view'; + +export const WorkChatAgentsPage: React.FC<{}> = () => { + useBreadcrumb([{ text: 'Agents' }]); + const { agents } = useAgentList(); + return ; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/chat.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/chat.tsx new file mode 100644 index 000000000000..a38167d8e280 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/chat.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { WorkchatChatView } from '../components/chat/chat_view'; +import { useBreadcrumb } from '../hooks/use_breadcrumbs'; +import { useAgent } from '../hooks/use_agent'; + +const newConversationId = 'new'; + +export const WorkchatChatPage: React.FC<{}> = () => { + const { agentId, conversationId: conversationIdFromParams } = useParams<{ + agentId: string; + conversationId: string | undefined; + }>(); + + const { agent } = useAgent({ agentId }); + + useBreadcrumb([{ text: agent?.name ?? 'Agent' }, { text: 'Chat' }]); + + const conversationId = useMemo(() => { + return conversationIdFromParams === newConversationId ? undefined : conversationIdFromParams; + }, [conversationIdFromParams]); + + return ; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/home.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/home.tsx new file mode 100644 index 000000000000..780d1aa27258 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/home.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useBreadcrumb } from '../hooks/use_breadcrumbs'; +import { WorkChatHomeView } from '../components/home/home_view'; + +export const WorkChatHomePage: React.FC<{}> = () => { + useBreadcrumb([]); + return ; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integration_edit_or_create.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integration_edit_or_create.tsx new file mode 100644 index 000000000000..405b7a9af909 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integration_edit_or_create.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { IntegrationEditView } from '../components/integrations/edit/integration_edit_view'; + +const newIntegrationId = 'create'; + +export const WorkChatIntegrationEditOrCreatePage: React.FC<{}> = () => { + const { integrationId: integrationIdFromParams } = useParams<{ + integrationId: string; + }>(); + + const integrationId = useMemo(() => { + return integrationIdFromParams === newIntegrationId ? undefined : integrationIdFromParams; + }, [integrationIdFromParams]); + + return ; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integrations.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integrations.tsx new file mode 100644 index 000000000000..3858b08353c3 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/pages/integrations.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useBreadcrumb } from '../hooks/use_breadcrumbs'; +import { useIntegrationList } from '../hooks/use_integration_list'; +import { IntegrationListView } from '../components/integrations/listing/integration_list_view'; + +export const WorkChatIntegrationsPage: React.FC<{}> = () => { + useBreadcrumb([{ text: 'Integrations' }]); + const { integrations } = useIntegrationList(); + return ; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/query_keys.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/query_keys.ts new file mode 100644 index 000000000000..be86c8bef734 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/query_keys.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Query keys for react-query + */ +export const queryKeys = { + conversations: { + all: ['conversations'] as const, + byAgent: (agentId: string) => ['conversations', 'list', { agentId }], + }, + agents: { + all: ['agents'] as const, + list: ['agents', 'list'] as const, + details: (agentId: string) => ['agents', { id: agentId }], + }, + connectors: { + all: ['connectors'] as const, + list: ['connectors', 'list'] as const, + }, + integrations: { + all: ['integrations'] as const, + list: ['integrations', 'list'] as const, + }, + users: { + current: ['users', 'current'] as const, + }, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/register.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/register.ts new file mode 100644 index 000000000000..90f08eae4014 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/register.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type CoreSetup, AppStatus, DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; +import { WORKCHAT_APP_ID } from '../../common/features'; +import type { WorkChatAppPluginStartDependencies } from '../types'; +import { WorkChatServices } from '../services'; + +export const registerApp = ({ + core, + getServices, +}: { + core: CoreSetup; + getServices: () => WorkChatServices; +}) => { + core.application.register({ + id: WORKCHAT_APP_ID, + appRoute: `/app/${WORKCHAT_APP_ID}`, + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + euiIconType: 'logoElasticsearch', + status: AppStatus.accessible, + title: 'WorkChat', + updater$: undefined, + deepLinks: [ + { id: 'agents', path: '/agents', title: 'Agents' }, + { id: 'integrations', path: '/integrations', title: 'Integrations' }, + ], + visibleIn: ['sideNav', 'globalSearch'], + async mount({ element, history }) { + const [coreStart, startPluginDeps] = await core.getStartServices(); + const services = getServices(); + const { mountApp } = await import('./mount'); + return mountApp({ core: coreStart, services, element, history, plugins: startPluginDeps }); + }, + }); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/routes.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/application/routes.tsx new file mode 100644 index 000000000000..8e6f5573d336 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/routes.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Routes } from '@kbn/shared-ux-router'; +import { WorkChatHomePage } from './pages/home'; +import { WorkchatChatPage } from './pages/chat'; +import { WorkChatAgentsPage } from './pages/agents'; +import { WorkChatAgentEditOrCreatePage } from './pages/agent_edit_or_create'; +import { WorkChatIntegrationsPage } from './pages/integrations'; +import { WorkChatIntegrationEditOrCreatePage } from './pages/integration_edit_or_create'; +export const WorkchatAppRoutes: React.FC<{}> = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/conversation_items.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/conversation_items.ts new file mode 100644 index 000000000000..c6472d9a46c1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/conversation_items.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AssistantMessage, UserMessage, ToolCall } from '../../../common/conversation_events'; + +interface ConversationItemBase { + id: string; + loading: boolean; +} + +export type UserMessageConversationItem = ConversationItemBase & { + type: 'user_message'; + message: UserMessage; +}; + +export type AssistantMessageConversationItem = ConversationItemBase & { + type: 'assistant_message'; + message: AssistantMessage; +}; + +export type ToolCallConversationItem = ConversationItemBase & { + type: 'tool_call'; + messageId: string; + toolCall: ToolCall; + toolResult?: string; +}; + +/** + * UI-specific representation of the conversation events. + */ +export type ConversationItem = + | UserMessageConversationItem + | AssistantMessageConversationItem + | ToolCallConversationItem; + +export const isUserMessageItem = (item: ConversationItem): item is UserMessageConversationItem => { + return item.type === 'user_message'; +}; + +export const isAssistantMessageItem = ( + item: ConversationItem +): item is AssistantMessageConversationItem => { + return item.type === 'assistant_message'; +}; + +export const isToolCallItem = (item: ConversationItem): item is ToolCallConversationItem => { + return item.type === 'tool_call'; +}; + +export const createUserMessageItem = ({ + message, + loading = false, +}: { + message: UserMessage; + loading?: boolean; +}): UserMessageConversationItem => { + return { + id: message.id, + type: 'user_message', + message, + loading, + }; +}; + +export const createAssistantMessageItem = ({ + message, + loading = false, +}: { + message: AssistantMessage; + loading?: boolean; +}): AssistantMessageConversationItem => { + return { + id: message.id, + type: 'assistant_message', + message, + loading, + }; +}; + +export const createToolCallItem = ({ + messageId, + toolCall, + loading = false, +}: { + messageId: string; + toolCall: ToolCall; + loading?: boolean; +}): ToolCallConversationItem => { + return { + id: toolCall.toolCallId, + type: 'tool_call', + messageId, + toolCall, + loading, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/get_chart_conversation_items.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/get_chart_conversation_items.ts new file mode 100644 index 000000000000..8e1b4480d4e1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/get_chart_conversation_items.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + type ConversationEvent, + isAssistantMessage, + isUserMessage, + isToolResult, + createAssistantMessage, +} from '../../../common/conversation_events'; +import type { ChatStatus } from '../hooks/use_chat'; +import { + type ConversationItem, + type ToolCallConversationItem, + createUserMessageItem, + createAssistantMessageItem, + createToolCallItem, + isAssistantMessageItem, + isToolCallItem, +} from './conversation_items'; + +/** + * Utility function preparing the data to display the chat conversation + */ +export const getChartConversationItems = ({ + conversationEvents, + chatStatus, +}: { + conversationEvents: ConversationEvent[]; + chatStatus: ChatStatus; +}): ConversationItem[] => { + const toolCallMap = new Map(); + + const items = conversationEvents.reduce((list, item) => { + if (isUserMessage(item)) { + list.push(createUserMessageItem({ message: item })); + } + + if (isAssistantMessage(item)) { + if (item.content) { + list.push(createAssistantMessageItem({ message: item })); + } + for (const toolCall of item.toolCalls) { + const toolCallItem = createToolCallItem({ messageId: item.id, toolCall }); + toolCallMap.set(toolCallItem.toolCall.toolCallId, toolCallItem); + list.push(toolCallItem); + } + } + + if (isToolResult(item)) { + const toolCallItem = toolCallMap.get(item.toolCallId); + if (toolCallItem) { + toolCallItem.toolResult = item.toolResult; + } + } + + return list; + }, []); + + if (chatStatus === 'loading') { + const lastItem = items[items.length - 1]; + if (isAssistantMessageItem(lastItem)) { + lastItem.loading = true; + } else if (isToolCallItem(lastItem) && !lastItem.toolResult) { + lastItem.loading = true; + } else { + // need to insert loading placeholder + items.push( + createAssistantMessageItem({ + message: createAssistantMessage({ content: '' }), + loading: true, + }) + ); + } + } + + return items; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/has_capability.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/has_capability.ts new file mode 100644 index 000000000000..e3b8b67ff4f5 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/has_capability.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ApplicationStart } from '@kbn/core/public'; +import { WORKCHAT_FEATURE_ID } from '../../../common/features'; + +export const hasWorkchatCapability = ( + capabilities: ApplicationStart['capabilities'], + capability: string +) => { + const workchatCaps = capabilities[WORKCHAT_FEATURE_ID] ?? {}; + return workchatCaps[capability] === true; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/sort_and_group_conversations.ts b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/sort_and_group_conversations.ts new file mode 100644 index 000000000000..0abd5abed5e2 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/application/utils/sort_and_group_conversations.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import datemath from '@kbn/datemath'; +import { i18n } from '@kbn/i18n'; +import type { ConversationSummary } from '../../../common/conversations'; + +export interface ConversationGroup { + dateLabel: string; + conversations: ConversationSummary[]; +} + +type ConversationGroupWithDate = ConversationGroup & { + dateLimit: number; +}; + +const getGroups = () => { + return [ + { + code: 'TODAY', + label: i18n.translate('xpack.workchatApp.conversationGroups.labels.today', { + defaultMessage: 'Today', + }), + limit: 'now/d', + }, + { + code: 'YESTERDAY', + label: i18n.translate('xpack.workchatApp.conversationGroups.labels.yesterday', { + defaultMessage: 'Yesterday', + }), + limit: 'now-1d/d', + }, + { + code: 'THIS_WEEK', + label: i18n.translate('xpack.workchatApp.conversationGroups.labels.thisWeek', { + defaultMessage: 'This week', + }), + limit: 'now/w', + }, + { + code: 'OLDER', + label: i18n.translate('xpack.workchatApp.conversationGroups.labels.before', { + defaultMessage: 'Before', + }), + limit: '', + }, + ]; +}; + +/** + * Sort and group conversation by time period to display them in the ConversationList component. + */ +export const sortAndGroupConversations = ( + conversations: ConversationSummary[] +): ConversationGroup[] => { + const now = new Date(); + + const getEpochLimit = (range: string) => { + return getAbsoluteTime(range, { forceNow: now })?.valueOf() ?? 0; + }; + + const groups = getGroups().map(({ label, limit }) => { + return emptyGroup(label, getEpochLimit(limit)); + }); + + conversations + .map((conversation) => { + return { + conversation, + date: moment(conversation.lastUpdated), + }; + }) + .sort((conv1, conv2) => { + return conv2.date.valueOf() - conv1.date.valueOf(); + }) + .forEach(({ conversation, date }) => { + for (const group of groups) { + if (date.isAfter(group.dateLimit)) { + group.conversations.push(conversation); + break; + } + } + }); + + return groups + .filter((group) => group.conversations.length > 0) + .map(({ conversations: convs, dateLabel }) => ({ conversations: convs, dateLabel })); +}; + +const emptyGroup = (label: string, limit: number): ConversationGroupWithDate => ({ + dateLabel: label, + dateLimit: limit, + conversations: [], +}); + +const getAbsoluteTime = (range: string, opts: Parameters[1] = {}) => { + const parsed = datemath.parse(range, opts); + + return parsed?.isValid() ? parsed : undefined; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/index.ts b/x-pack/solutions/chat/plugins/workchat-app/public/index.ts new file mode 100644 index 000000000000..0ae272bb04b1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { WorkChatAppPlugin } from './plugin'; +import { + WorkChatAppPluginSetup, + WorkChatAppPluginStart, + WorkChatAppPluginStartDependencies, +} from './types'; + +export const plugin: PluginInitializer< + WorkChatAppPluginSetup, + WorkChatAppPluginStart, + {}, + WorkChatAppPluginStartDependencies +> = (context: PluginInitializerContext) => { + return new WorkChatAppPlugin(context); +}; + +export type { WorkChatAppPluginSetup, WorkChatAppPluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/plugin.tsx b/x-pack/solutions/chat/plugins/workchat-app/public/plugin.tsx new file mode 100644 index 000000000000..2123de149b04 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/plugin.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import type { + WorkChatAppPluginSetup, + WorkChatAppPluginStart, + WorkChatAppPluginSetupDependencies, + WorkChatAppPluginStartDependencies, +} from './types'; +import { registerApp } from './application'; +import { ChatService, ConversationService, AgentService, type WorkChatServices } from './services'; +import { IntegrationService } from './services/integration/integration_service'; +import { IntegrationRegistry } from './services/integration/integration_registry'; + +export class WorkChatAppPlugin + implements + Plugin< + WorkChatAppPluginSetup, + WorkChatAppPluginStart, + WorkChatAppPluginSetupDependencies, + WorkChatAppPluginStartDependencies + > +{ + private services?: WorkChatServices; + private readonly integrationRegistry = new IntegrationRegistry(); + + constructor(context: PluginInitializerContext) {} + + public setup( + core: CoreSetup + ): WorkChatAppPluginSetup { + registerApp({ + core, + getServices: () => { + if (!this.services) { + throw new Error('getServices called before plugin start'); + } + return this.services; + }, + }); + + return { + integrations: { + register: (integrationComponents) => { + this.integrationRegistry.register(integrationComponents); + }, + }, + }; + } + + public start( + { http }: CoreStart, + pluginsStart: WorkChatAppPluginStartDependencies + ): WorkChatAppPluginStart { + const conversationService = new ConversationService({ + http, + }); + const chatService = new ChatService({ + http, + }); + const agentService = new AgentService({ + http, + }); + const integrationService = new IntegrationService({ + http, + }); + + this.services = { + chatService, + agentService, + conversationService, + integrationService, + integrationRegistry: this.integrationRegistry, + }; + + return {}; + } + + public stop() {} +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/agent/agent_service.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/agent/agent_service.ts new file mode 100644 index 000000000000..9cdba8246b38 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/agent/agent_service.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { + GetAgentResponse, + ListAgentResponse, + CreateAgentPayload, + CreateAgentResponse, + UpdateAgentResponse, + UpdateAgentPayload, +} from '../../../common/http_api/agents'; + +export class AgentService { + private readonly http: HttpSetup; + + constructor({ http }: { http: HttpSetup }) { + this.http = http; + } + + async list() { + const response = await this.http.get('/internal/workchat/agents'); + return response.agents; + } + + async get(agentId: string) { + return await this.http.get(`/internal/workchat/agents/${agentId}`); + } + + async create(request: CreateAgentPayload) { + return await this.http.post(`/internal/workchat/agents`, { + body: JSON.stringify(request), + }); + } + + async update(id: string, request: UpdateAgentPayload) { + return await this.http.put(`/internal/workchat/agents/${id}`, { + body: JSON.stringify(request), + }); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/chat/chat_service.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/chat/chat_service.ts new file mode 100644 index 000000000000..9cee4e17f41b --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/chat/chat_service.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defer, catchError, throwError } from 'rxjs'; +import { isSSEError } from '@kbn/sse-utils'; +import { httpResponseIntoObservable } from '@kbn/sse-utils-client'; +import { HttpSetup } from '@kbn/core/public'; +import { ChatEvent } from '../../../common/chat_events'; +import { createChatError, ChatErrorCode } from '../../../common/errors'; + +interface ConverseParams { + conversationId?: string; + connectorId?: string; + agentId: string; + nextMessage: string; +} + +export class ChatService { + private readonly http: HttpSetup; + + constructor({ http }: { http: HttpSetup }) { + this.http = http; + } + + converse({ nextMessage, conversationId, agentId, connectorId }: ConverseParams) { + return defer(() => { + return this.http.post('/internal/workchat/chat', { + asResponse: true, + rawResponse: true, + body: JSON.stringify({ nextMessage, conversationId, agentId, connectorId }), + }); + }).pipe( + // @ts-expect-error SseEvent mixin issue + httpResponseIntoObservable(), + catchError((err) => { + if (isSSEError(err)) { + return throwError(() => + createChatError(err.code as ChatErrorCode, err.message, err.meta) + ); + } + return throwError(() => err); + }) + ); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/conversation/conversation_service.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/conversation/conversation_service.ts new file mode 100644 index 000000000000..87c3a29e210f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/conversation/conversation_service.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { + ListConversationRequest, + ListConversationResponse, + GetConversationResponse, +} from '../../../common/http_api/conversation'; + +export class ConversationService { + private readonly http: HttpSetup; + + constructor({ http }: { http: HttpSetup }) { + this.http = http; + } + + async list(request: ListConversationRequest) { + const response = await this.http.post( + '/internal/workchat/conversations', + { + body: JSON.stringify(request), + } + ); + return response.conversations; + } + + async get(conversationId: string) { + return await this.http.get( + `/internal/workchat/conversations/${conversationId}` + ); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/index.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/index.ts new file mode 100644 index 000000000000..6d73e5528b85 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { WorkChatServices } from './types'; +export { ChatService } from './chat/chat_service'; +export { ConversationService } from './conversation/conversation_service'; +export { AgentService } from './agent/agent_service'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_registry.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_registry.ts new file mode 100644 index 000000000000..8e10a04ed7a6 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_registry.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType } from '@kbn/wci-common'; +import type { IntegrationComponentDescriptor } from '@kbn/wci-browser'; + +export class IntegrationRegistry { + private allowRegistration = true; + private integrationTypes = new Map(); + + register(definition: IntegrationComponentDescriptor) { + if (!this.allowRegistration) { + throw new Error(`Tried to register integration but allowRegistration is false`); + } + if (this.has(definition.getType())) { + throw new Error( + `Tried to register Integration [${definition.getType()}], but already registered` + ); + } + this.integrationTypes.set(definition.getType(), definition); + } + + blockRegistration() { + this.allowRegistration = false; + } + + has(type: IntegrationType) { + return this.integrationTypes.has(type); + } + + get(type: IntegrationType) { + if (!this.has(type)) { + throw new Error(`Integration definition for type [${type}] not found`); + } + return this.integrationTypes.get(type)!; + } + + getAll() { + return [...this.integrationTypes.values()]; + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_service.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_service.ts new file mode 100644 index 000000000000..0119a385eb30 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/integration_service.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { + GetIntegrationResponse, + ListIntegrationsResponse, + CreateIntegrationPayload, + CreateIntegrationResponse, + UpdateIntegrationResponse, + UpdateIntegrationPayload, +} from '../../../common/http_api/integrations'; + +export class IntegrationService { + private readonly http: HttpSetup; + + constructor({ http }: { http: HttpSetup }) { + this.http = http; + } + + async list() { + const response = await this.http.get( + '/internal/workchat/integrations' + ); + return response.integrations; + } + + async get(integrationId: string) { + return await this.http.get( + `/internal/workchat/integrations/${integrationId}` + ); + } + + async create(request: CreateIntegrationPayload) { + return await this.http.post(`/internal/workchat/integrations`, { + body: JSON.stringify(request), + }); + } + + async update(id: string, request: UpdateIntegrationPayload) { + return await this.http.put(`/internal/workchat/integrations/${id}`, { + body: JSON.stringify(request), + }); + } + + async delete(integrationId: string) { + return await this.http.delete(`/internal/workchat/integrations/${integrationId}`); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/types.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/types.ts new file mode 100644 index 000000000000..1fec1c76430e --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/integration/types.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/services/types.ts b/x-pack/solutions/chat/plugins/workchat-app/public/services/types.ts new file mode 100644 index 000000000000..20011051d399 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/services/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ChatService } from './chat/chat_service'; +import type { ConversationService } from './conversation/conversation_service'; +import type { AgentService } from './agent/agent_service'; +import type { IntegrationService } from './integration/integration_service'; +import type { IntegrationRegistry } from './integration/integration_registry'; +export interface WorkChatServices { + chatService: ChatService; + conversationService: ConversationService; + agentService: AgentService; + integrationService: IntegrationService; + integrationRegistry: IntegrationRegistry; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/public/types.ts b/x-pack/solutions/chat/plugins/workchat-app/public/types.ts new file mode 100644 index 000000000000..d612dc84e8b5 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/public/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InferencePublicStart } from '@kbn/inference-plugin/public'; +import type { IntegrationComponentDescriptor } from '@kbn/wci-browser'; + +export interface WorkChatAppPluginSetup { + integrations: { + register: (integration: IntegrationComponentDescriptor) => void; + }; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WorkChatAppPluginStart {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WorkChatAppPluginSetupDependencies {} + +export interface WorkChatAppPluginStartDependencies { + inference: InferencePublicStart; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/config.ts b/x-pack/solutions/chat/plugins/workchat-app/server/config.ts new file mode 100644 index 000000000000..3b21428ea533 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type WorkChatAppConfig = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: {}, + schema: configSchema, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/errors.ts b/x-pack/solutions/chat/plugins/workchat-app/server/errors.ts new file mode 100644 index 000000000000..c86eb024e12c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/errors.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class WorkchatError extends Error { + constructor(message: string, public readonly statusCode = 500) { + super(message); + } +} + +export const isWorkChatError = (err: unknown): err is WorkchatError => { + return err instanceof WorkchatError; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/features.ts b/x-pack/solutions/chat/plugins/workchat-app/server/features.ts new file mode 100644 index 000000000000..a9aac38686a2 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/features.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { + WORKCHAT_FEATURE_ID, + WORKCHAT_FEATURE_NAME, + WORKCHAT_APP_ID, + capabilityGroups, +} from '../common/features'; +import { integrationTypeName, agentTypeName, conversationTypeName } from './saved_objects'; + +export const registerFeatures = ({ features }: { features: FeaturesPluginSetup }) => { + features.registerKibanaFeature({ + id: WORKCHAT_FEATURE_ID, + name: WORKCHAT_FEATURE_NAME, + minimumLicense: 'enterprise', + order: 1000, + category: DEFAULT_APP_CATEGORIES.chat, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: ['kibana', WORKCHAT_APP_ID], + catalogue: [WORKCHAT_FEATURE_ID], + privileges: { + all: { + app: ['kibana', WORKCHAT_APP_ID], + api: [...capabilityGroups.api.all], + catalogue: [WORKCHAT_FEATURE_ID], + savedObject: { + all: [integrationTypeName, agentTypeName, conversationTypeName], + read: [], + }, + ui: [...capabilityGroups.ui.all], + }, + read: { + app: ['kibana', WORKCHAT_APP_ID], + api: [...capabilityGroups.api.read], + catalogue: [WORKCHAT_FEATURE_ID], + savedObject: { + all: [conversationTypeName], + read: [integrationTypeName, agentTypeName], + }, + ui: [...capabilityGroups.ui.read], + }, + }, + }); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/index.ts new file mode 100644 index 000000000000..d9467ee44cbf --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { WorkChatAppPlugin } from './plugin'; + +export { config } from './config'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new WorkChatAppPlugin(initializerContext); +}; + +export type { WorkChatAppPluginSetup, WorkChatAppPluginStart } from './types'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/plugin.ts b/x-pack/solutions/chat/plugins/workchat-app/server/plugin.ts new file mode 100644 index 000000000000..8bcacf91a834 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/plugin.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoreStart, + CoreSetup, + Plugin, + PluginInitializerContext, + LoggerFactory, +} from '@kbn/core/server'; +import { registerRoutes } from './routes'; +import { registerTypes } from './saved_objects'; +import { registerFeatures } from './features'; +import type { InternalServices } from './services/types'; +import { IntegrationRegistry } from './services/integrations'; +import { createServices } from './services/create_services'; +import type { + WorkChatAppPluginSetup, + WorkChatAppPluginStart, + WorkChatAppPluginSetupDependencies, + WorkChatAppPluginStartDependencies, +} from './types'; + +export class WorkChatAppPlugin + implements + Plugin< + WorkChatAppPluginSetup, + WorkChatAppPluginStart, + WorkChatAppPluginSetupDependencies, + WorkChatAppPluginStartDependencies + > +{ + private readonly logger: LoggerFactory; + private readonly integrationRegistry = new IntegrationRegistry(); + private services?: InternalServices; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger; + } + + public setup( + core: CoreSetup, + setupDeps: WorkChatAppPluginSetupDependencies + ): WorkChatAppPluginSetup { + const router = core.http.createRouter(); + registerRoutes({ + core, + router, + logger: this.logger.get('routes'), + getServices: () => { + if (!this.services) { + throw new Error('getServices called before #start'); + } + return this.services; + }, + }); + + registerTypes({ savedObjects: core.savedObjects }); + + registerFeatures({ features: setupDeps.features }); + + return { + integrations: { + register: (integration) => { + return this.integrationRegistry.register(integration); + }, + }, + }; + } + + public start( + core: CoreStart, + pluginsDependencies: WorkChatAppPluginStartDependencies + ): WorkChatAppPluginStart { + this.services = createServices({ + core, + logger: this.logger, + pluginsDependencies, + integrationRegistry: this.integrationRegistry, + }); + + return {}; + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/agents.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/agents.ts new file mode 100644 index 000000000000..06f4d4f32c78 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/agents.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import type { + GetAgentResponse, + ListAgentResponse, + CreateAgentResponse, + CreateAgentPayload, +} from '../../common/http_api/agents'; +import { apiCapabilities } from '../../common/features'; +import type { RouteDependencies } from './types'; +import { getHandlerWrapper } from './wrap_handler'; + +export const registerAgentRoutes = ({ getServices, router, logger }: RouteDependencies) => { + const wrapHandler = getHandlerWrapper({ logger }); + + // API to get a single agent + router.get( + { + path: '/internal/workchat/agents/{agentId}', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: { + params: schema.object({ + agentId: schema.string(), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { agentService } = getServices(); + const client = await agentService.getScopedClient({ request }); + + const { agentId } = request.params; + + const agent = await client.get({ agentId }); + + return res.ok({ + body: agent, + }); + }) + ); + + // API to create an agent + router.post( + { + path: '/internal/workchat/agents', + security: { + authz: { + requiredPrivileges: [apiCapabilities.manageWorkchat], + }, + }, + validate: { + body: schema.object({ + name: schema.string(), + description: schema.string({ defaultValue: '' }), + configuration: schema.object({ + systemPrompt: schema.maybe(schema.string()), + }), + public: schema.boolean({ defaultValue: false }), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const payload: CreateAgentPayload = request.body; + + const { agentService } = getServices(); + const client = await agentService.getScopedClient({ request }); + + // TODO: validation + + const agent = await client.create(payload); + + return res.ok({ + body: { + success: true, + agent, + }, + }); + }) + ); + + // API to update an agent + router.put( + { + path: '/internal/workchat/agents/{agentId}', + security: { + authz: { + requiredPrivileges: [apiCapabilities.manageWorkchat], + }, + }, + validate: { + params: schema.object({ + agentId: schema.string(), + }), + body: schema.object({ + name: schema.string(), + description: schema.string({ defaultValue: '' }), + configuration: schema.object({ + systemPrompt: schema.maybe(schema.string()), + }), + public: schema.boolean({ defaultValue: false }), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { agentId } = request.params; + const payload: CreateAgentPayload = request.body; + + const { agentService } = getServices(); + const client = await agentService.getScopedClient({ request }); + + // TODO: validation + + const agent = await client.update(agentId, payload); + + return res.ok({ + body: { + success: true, + agent, + }, + }); + }) + ); + + // API to list all accessible agents + router.get( + { + path: '/internal/workchat/agents', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: false, + }, + wrapHandler(async (ctx, request, res) => { + const { agentService } = getServices(); + const client = await agentService.getScopedClient({ request }); + + const agents = await client.list(); + + return res.ok({ + body: { + agents, + }, + }); + }) + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/chat.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/chat.ts new file mode 100644 index 000000000000..d8ccd27d9bde --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/chat.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Observable } from 'rxjs'; +import { schema } from '@kbn/config-schema'; +import { ServerSentEvent } from '@kbn/sse-utils'; +import { observableIntoEventSourceStream } from '@kbn/sse-utils-server'; +import { apiCapabilities } from '../../common/features'; +import type { RouteDependencies } from './types'; +import { getHandlerWrapper } from './wrap_handler'; + +export const registerChatRoutes = ({ getServices, router, logger }: RouteDependencies) => { + const wrapHandler = getHandlerWrapper({ logger }); + + const stubLogger = { + debug: () => undefined, + error: () => undefined, + }; + + router.post( + { + path: '/internal/workchat/chat', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: { + body: schema.object({ + conversationId: schema.maybe(schema.string()), + connectorId: schema.maybe(schema.string()), + agentId: schema.string(), + nextMessage: schema.string(), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { chatService } = getServices(); + + const { nextMessage, conversationId, agentId, connectorId } = request.body; + + const abortController = new AbortController(); + request.events.aborted$.subscribe(() => { + abortController.abort(); + }); + + const events$ = chatService.converse({ + request, + connectorId: connectorId ?? 'azure-gpt4', // TODO: auto-select on server-side when not present + agentId, + nextUserMessage: nextMessage, + conversationId, + }); + + return res.ok({ + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + body: observableIntoEventSourceStream(events$ as unknown as Observable, { + signal: abortController.signal, + // already logging at the service level + logger: stubLogger, + }), + }); + }) + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/connectors.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/connectors.ts new file mode 100644 index 000000000000..172b67fdf94f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/connectors.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getConnectorList } from '@kbn/wc-genai-utils'; +import type { ListConnectorsResponse } from '../../common/http_api/connectors'; +import type { RouteDependencies } from './types'; +import { apiCapabilities } from '../../common/features'; +import { getHandlerWrapper } from './wrap_handler'; + +export const registerConnectorRoutes = ({ logger, router, core }: RouteDependencies) => { + const wrapHandler = getHandlerWrapper({ logger }); + + router.get( + { + path: '/internal/workchat/connectors', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: false, + }, + wrapHandler(async (ctx, request, res) => { + const [, startDeps] = await core.getStartServices(); + const { actions } = startDeps; + + const connectors = await getConnectorList({ + request, + actions, + }); + + return res.ok({ + body: { connectors }, + }); + }) + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/conversation.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/conversation.ts new file mode 100644 index 000000000000..29f366d62f16 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/conversation.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import type { ConversationSummary } from '../../common/conversations'; +import type { + ListConversationRequest, + ListConversationResponse, + GetConversationResponse, +} from '../../common/http_api/conversation'; +import { apiCapabilities } from '../../common/features'; +import type { RouteDependencies } from './types'; +import { getHandlerWrapper } from './wrap_handler'; + +export const registerConversationRoutes = ({ getServices, router, logger }: RouteDependencies) => { + const wrapHandler = getHandlerWrapper({ logger }); + + // get conversation by id + router.get( + { + path: '/internal/workchat/conversations/{conversationId}', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: { + params: schema.object({ + conversationId: schema.string(), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { conversationService } = getServices(); + const client = await conversationService.getScopedClient({ request }); + + const { conversationId } = request.params; + + const conversation = await client.get({ conversationId }); + + return res.ok({ + body: conversation, + }); + }) + ); + + // list all conversations for a given agent + router.post( + { + path: '/internal/workchat/conversations', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: { + body: schema.object({ + agentId: schema.maybe(schema.string()), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { conversationService } = getServices(); + const client = await conversationService.getScopedClient({ request }); + + const params: ListConversationRequest = request.body; + + const conversations = await client.list({ + agentId: params.agentId, + }); + + const summaries = conversations.map((conv) => { + return { + id: conv.id, + agentId: conv.agentId, + title: conv.title, + lastUpdated: conv.lastUpdated, + }; + }); + + return res.ok({ + body: { + conversations: summaries, + }, + }); + }) + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/index.ts new file mode 100644 index 000000000000..91d04afedfbc --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RouteDependencies } from './types'; +import { registerChatRoutes } from './chat'; +import { registerConversationRoutes } from './conversation'; +import { registerConnectorRoutes } from './connectors'; +import { registerIntegrationsRoutes } from './integrations'; +import { registerAgentRoutes } from './agents'; + +export const registerRoutes = (dependencies: RouteDependencies) => { + registerChatRoutes(dependencies); + registerConversationRoutes(dependencies); + registerConnectorRoutes(dependencies); + registerAgentRoutes(dependencies); + registerIntegrationsRoutes(dependencies); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/integrations.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/integrations.ts new file mode 100644 index 000000000000..36a9750ab9c1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/integrations.ts @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IntegrationType } from '@kbn/wci-common'; +import type { + ListIntegrationsResponse, + GetIntegrationResponse, + CreateIntegrationResponse, + UpdateIntegrationResponse, + DeleteIntegrationResponse, +} from '../../common/http_api/integrations'; +import { apiCapabilities } from '../../common/features'; +import type { RouteDependencies } from './types'; +import { getHandlerWrapper } from './wrap_handler'; + +export const registerIntegrationsRoutes = ({ getServices, router, logger }: RouteDependencies) => { + const wrapHandler = getHandlerWrapper({ logger }); + + // Get a single integration by ID + router.get( + { + path: '/internal/workchat/integrations/{integrationId}', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: { + params: schema.object({ + integrationId: schema.string(), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { integrationsService } = getServices(); + const client = await integrationsService.getScopedClient({ request }); + + const { integrationId } = request.params; + + const integration = await client.get({ integrationId }); + + return res.ok({ + body: integration, + }); + }) + ); + + // List all integrations + router.get( + { + path: '/internal/workchat/integrations', + security: { + authz: { + requiredPrivileges: [apiCapabilities.useWorkchat], + }, + }, + validate: {}, + }, + wrapHandler(async (ctx, request, res) => { + const { integrationsService } = getServices(); + const client = await integrationsService.getScopedClient({ request }); + + const integrations = await client.list(); + + return res.ok({ + body: { + integrations, + }, + }); + }) + ); + + // Create a new integration + router.post( + { + path: '/internal/workchat/integrations', + security: { + authz: { + requiredPrivileges: [apiCapabilities.manageWorkchat], + }, + }, + validate: { + body: schema.object({ + type: schema.oneOf( + // @ts-expect-error complains that IntegrationType may have less than 1 element... + Object.values(IntegrationType).map((val) => schema.literal(val)) + ), + name: schema.string(), + description: schema.string(), + configuration: schema.recordOf(schema.string(), schema.any()), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { integrationsService } = getServices(); + const client = await integrationsService.getScopedClient({ request }); + + const { type, name, description, configuration } = request.body; + + const integration = await client.create({ + type, + name, + description, + configuration, + }); + + return res.ok({ + body: integration, + }); + }) + ); + + // Update an existing integration + router.put( + { + path: '/internal/workchat/integrations/{integrationId}', + security: { + authz: { + requiredPrivileges: [apiCapabilities.manageWorkchat], + }, + }, + validate: { + params: schema.object({ + integrationId: schema.string(), + }), + body: schema.object({ + name: schema.maybe(schema.string()), + description: schema.maybe(schema.string()), + configuration: schema.recordOf(schema.string(), schema.any()), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { integrationsService } = getServices(); + const client = await integrationsService.getScopedClient({ request }); + + const { integrationId } = request.params; + const { name, description, configuration } = request.body; + + const integration = await client.update(integrationId, { + name, + description, + configuration, + }); + + return res.ok({ + body: integration, + }); + }) + ); + + // Delete an integration + router.delete( + { + path: '/internal/workchat/integrations/{integrationId}', + security: { + authz: { + requiredPrivileges: [apiCapabilities.manageWorkchat], + }, + }, + validate: { + params: schema.object({ + integrationId: schema.string(), + }), + }, + }, + wrapHandler(async (ctx, request, res) => { + const { integrationsService } = getServices(); + const client = await integrationsService.getScopedClient({ request }); + + const { integrationId } = request.params; + + await client.delete(integrationId); + + return res.ok({ + body: { + success: true, + }, + }); + }) + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/types.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/types.ts new file mode 100644 index 000000000000..db1e848ddd64 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IRouter, Logger, CoreSetup } from '@kbn/core/server'; +import type { WorkChatAppPluginStartDependencies } from '../types'; +import type { InternalServices } from '../services'; + +export interface RouteDependencies { + core: CoreSetup; + router: IRouter; + logger: Logger; + getServices: () => InternalServices; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/routes/wrap_handler.ts b/x-pack/solutions/chat/plugins/workchat-app/server/routes/wrap_handler.ts new file mode 100644 index 000000000000..a8ca671dfbad --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/routes/wrap_handler.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, RequestHandler } from '@kbn/core/server'; +import { isWorkChatError } from '../errors'; + +export const getHandlerWrapper = + ({ logger }: { logger: Logger }) => + (handler: RequestHandler): RequestHandler => { + return (ctx, req, res) => { + try { + return handler(ctx, req, res); + } catch (e) { + logger.error(e); + if (isWorkChatError(e)) { + return res.customError({ + body: { message: e.message }, + statusCode: e.statusCode, + }); + } else { + throw e; + } + } + }; + }; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/agents.ts b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/agents.ts new file mode 100644 index 000000000000..c70a352912c1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/agents.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsType } from '@kbn/core/server'; + +export const agentTypeName = 'workchat_agent' as const; + +export const agentSoType: SavedObjectsType = { + // TODO: specific SO index for workchat + name: agentTypeName, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: 'strict', + properties: { + agent_id: { type: 'keyword' }, + agent_name: { type: 'text' }, + description: { type: 'text' }, + last_updated: { type: 'date' }, + configuration: { dynamic: false, type: 'object', properties: {} }, + user_id: { type: 'keyword' }, + user_name: { type: 'keyword' }, + access_control: { + properties: { + public: { type: 'boolean' }, + }, + }, + }, + }, +}; + +export interface AgentAttributes { + agent_id: string; + agent_name: string; + description: string; + last_updated: string; + configuration: Record; + user_id: string; + user_name: string; + access_control: { + public: boolean; + }; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/conversations.ts b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/conversations.ts new file mode 100644 index 000000000000..a4ba404764af --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/conversations.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsType } from '@kbn/core/server'; +import type { ConversationEvent } from '../../common/conversation_events'; + +export const conversationTypeName = 'workchat_conversation' as const; + +export const conversationSoType: SavedObjectsType = { + // TODO: specific SO index for workchat + name: conversationTypeName, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: 'strict', + properties: { + conversation_id: { type: 'keyword' }, + agent_id: { type: 'keyword' }, + title: { type: 'text' }, + last_updated: { type: 'date' }, + + events: { dynamic: false, type: 'object', properties: {} }, + + user_id: { type: 'keyword' }, + user_name: { type: 'keyword' }, + + access_control: { + properties: { + public: { type: 'boolean' }, + }, + }, + }, + }, +}; + +export interface ConversationAttributes { + conversation_id: string; + agent_id: string; + title: string; + last_updated: string; + + user_id: string; + user_name: string; + + events: ConversationEvent[]; + + access_control: { + public: boolean; + }; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/index.ts new file mode 100644 index 000000000000..96f278a731c1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsServiceSetup } from '@kbn/core/server'; +import { conversationSoType } from './conversations'; +import { agentSoType } from './agents'; +import { integrationSoType } from './integrations'; + +export const registerTypes = ({ savedObjects }: { savedObjects: SavedObjectsServiceSetup }) => { + savedObjects.registerType(conversationSoType); + savedObjects.registerType(agentSoType); + savedObjects.registerType(integrationSoType); +}; + +export { conversationTypeName, type ConversationAttributes } from './conversations'; +export { agentTypeName, type AgentAttributes } from './agents'; +export { integrationTypeName, type IntegrationAttributes } from './integrations'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/integrations.ts b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/integrations.ts new file mode 100644 index 000000000000..13201c7c3ba9 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/saved_objects/integrations.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsType } from '@kbn/core/server'; +import { IntegrationType, IntegrationConfiguration } from '@kbn/wci-common'; + +export const integrationTypeName = 'workchat_integration' as const; + +export const integrationSoType: SavedObjectsType = { + name: integrationTypeName, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: 'strict', + properties: { + integration_id: { type: 'keyword' }, + type: { type: 'keyword' }, + name: { type: 'keyword' }, + description: { type: 'text' }, + configuration: { dynamic: false, type: 'object', properties: {} }, + created_at: { type: 'date' }, + updated_at: { type: 'date' }, + created_by: { type: 'keyword' }, + }, + }, +}; + +export interface IntegrationAttributes { + integration_id: string; + type: IntegrationType; + name: string; + description: string; + configuration: IntegrationConfiguration; + created_at: string; + updated_at: string; + created_by: string; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_client.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_client.ts new file mode 100644 index 000000000000..e2bfc4eed930 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_client.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 as uuidv4 } from 'uuid'; +import type { SavedObjectsClientContract, Logger, SavedObject } from '@kbn/core/server'; +import type { UserNameAndId } from '../../../common/shared'; +import type { Agent, AgentCreateRequest } from '../../../common/agents'; +import { agentTypeName, type AgentAttributes } from '../../saved_objects/agents'; +import { WorkchatError } from '../../errors'; +import { createBuilder } from '../../utils/so_filters'; +import { savedObjectToModel, createRequestToRaw, updateToAttributes } from './convert_model'; + +interface AgentClientOptions { + logger: Logger; + client: SavedObjectsClientContract; + user: UserNameAndId; +} + +export type AgentUpdatableFields = Partial< + Pick +>; + +/** + * Agent client scoped to a current user. + * All APIs exposed by the client are performing actions on behalf of the user, + * only accessing/returning agents that are accessible to the user. + */ +export interface AgentClient { + /** + * Returns all the agents available to the current user. + */ + list(): Promise; + /** + * Get an agent by id, if the current user is allowed to access it + */ + get(options: { agentId: string }): Promise; + /** + * Creates an agent bound to the current user and returns it. + */ + create(request: AgentCreateRequest): Promise; + /** + * Updates an agent and returns it. + */ + update(agentId: string, fields: AgentUpdatableFields): Promise; +} + +export class AgentClientImpl implements AgentClient { + private readonly client: SavedObjectsClientContract; + private readonly user: UserNameAndId; + // @ts-expect-error will be used at some point + private readonly logger: Logger; + + constructor({ client, user, logger }: AgentClientOptions) { + this.client = client; + this.user = user; + this.logger = logger; + } + + async list(): Promise { + const builder = createBuilder(agentTypeName); + const filter = builder + .or(builder.equals('user_id', this.user.id), builder.equals('access_control.public', true)) + .toKQL(); + + const { saved_objects: results } = await this.client.find({ + type: agentTypeName, + filter, + perPage: 1000, + }); + + return results.map(savedObjectToModel); + } + + async get({ agentId }: { agentId: string }): Promise { + const conversationSo = await this._rawGet({ agentId }); + return savedObjectToModel(conversationSo); + } + + async create(createRequest: AgentCreateRequest): Promise { + const now = new Date(); + const id = createRequest.id ?? uuidv4(); + const attributes = createRequestToRaw({ + createRequest, + id, + user: this.user, + creationDate: now, + }); + const created = await this.client.create(agentTypeName, attributes, { id }); + return savedObjectToModel(created); + } + + async update(agentId: string, updatedFields: AgentUpdatableFields): Promise { + const conversationSo = await this._rawGet({ agentId }); + const updatedAttributes = { + ...conversationSo.attributes, + ...updateToAttributes({ updatedFields }), + }; + + await this.client.update(agentTypeName, conversationSo.id, updatedAttributes); + + return savedObjectToModel({ + ...conversationSo, + attributes: updatedAttributes, + }); + } + + private async _rawGet({ agentId }: { agentId: string }): Promise> { + const builder = createBuilder(agentTypeName); + + const filter = builder + .and( + builder.equals('agent_id', agentId), + builder.or( + builder.equals('user_id', this.user.id), + builder.equals('access_control.public', true) + ) + ) + .toKQL(); + + const { saved_objects: results } = await this.client.find({ + type: agentTypeName, + filter, + }); + if (results.length > 0) { + return results[0]; + } + throw new WorkchatError(`Conversation ${agentId} not found`, 404); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_service.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_service.ts new file mode 100644 index 000000000000..cb6c4d506e38 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/agent_service.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObjectsServiceStart, + KibanaRequest, + Logger, + SecurityServiceStart, +} from '@kbn/core/server'; +import { agentTypeName } from '../../saved_objects/agents'; +import { AgentClientImpl, AgentClient } from './agent_client'; + +interface ConversationServiceOptions { + logger: Logger; + savedObjects: SavedObjectsServiceStart; + security: SecurityServiceStart; +} + +export interface AgentService { + /** + * Returns an agent client scoped to the current user. + */ + getScopedClient(options: { request: KibanaRequest }): Promise; +} + +export class AgentServiceImpl implements AgentService { + private readonly savedObjects: SavedObjectsServiceStart; + private readonly security: SecurityServiceStart; + private readonly logger: Logger; + + constructor({ savedObjects, security, logger }: ConversationServiceOptions) { + this.savedObjects = savedObjects; + this.security = security; + this.logger = logger; + } + + async getScopedClient({ request }: { request: KibanaRequest }): Promise { + const user = this.security.authc.getCurrentUser(request); + if (!user) { + throw new Error('No user bound to the provided request'); + } + const soClient = this.savedObjects.getScopedClient(request, { + includedHiddenTypes: [agentTypeName], + }); + + return new AgentClientImpl({ + logger: this.logger.get('client'), + client: soClient, + user: { id: user.profile_uid!, name: user.username }, + }); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/convert_model.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/convert_model.ts new file mode 100644 index 000000000000..22371f48d9d0 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/convert_model.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObject } from '@kbn/core/server'; +import type { UserNameAndId } from '../../../common/shared'; +import type { Agent, AgentCreateRequest } from '../../../common/agents'; +import type { AgentAttributes } from '../../saved_objects/agents'; +import type { AgentUpdatableFields } from './agent_client'; + +export const savedObjectToModel = ({ attributes }: SavedObject): Agent => { + return { + id: attributes.agent_id, + name: attributes.agent_name, + description: attributes.description, + lastUpdated: attributes.last_updated, + user: { + id: attributes.user_id, + name: attributes.user_name, + }, + configuration: attributes.configuration, + public: attributes.access_control.public, + }; +}; + +export const updateToAttributes = ({ + updatedFields, +}: { + updatedFields: AgentUpdatableFields; +}): Partial => { + return { + agent_name: updatedFields.name, + description: updatedFields.description, + configuration: updatedFields.configuration, + }; +}; + +export const createRequestToRaw = ({ + createRequest, + id, + user, + creationDate, +}: { + createRequest: AgentCreateRequest; + id: string; + user: UserNameAndId; + creationDate: Date; +}): AgentAttributes => { + return { + agent_id: id, + agent_name: createRequest.name, + description: createRequest.description, + configuration: createRequest.configuration, + last_updated: creationDate.toISOString(), + user_id: user.id, + user_name: user.name, + access_control: { + public: createRequest.public, + }, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/index.ts new file mode 100644 index 000000000000..ede4671df97d --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AgentServiceImpl, type AgentService } from './agent_service'; +export type { AgentClient } from './agent_client'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/mocks.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/mocks.ts new file mode 100644 index 000000000000..baee934b3fea --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/agents/mocks.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AgentClient } from './agent_client'; +import type { AgentService } from './agent_service'; + +const createAgentClientMock = () => { + const mocked: jest.Mocked = { + list: jest.fn(), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }; + return mocked; +}; + +const createAgentServiceMock = () => { + const mocked: jest.Mocked = { + getScopedClient: jest.fn().mockReturnValue(createAgentClientMock()), + }; + return mocked; +}; + +export const agentMocks = { + create: createAgentServiceMock, + createClient: createAgentClientMock, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/chat_service.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/chat_service.ts new file mode 100644 index 000000000000..6655eb6baa97 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/chat_service.ts @@ -0,0 +1,317 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 as uuidv4 } from 'uuid'; +import { + filter, + map, + toArray, + of, + mergeMap, + defer, + shareReplay, + forkJoin, + switchMap, + merge, + catchError, + throwError, + Observable, +} from 'rxjs'; +import { KibanaRequest, Logger } from '@kbn/core/server'; +import type { InferenceChatModel } from '@kbn/inference-langchain'; +import type { InferenceServerStart } from '@kbn/inference-plugin/server'; +import { + conversationCreatedEvent, + conversationUpdatedEvent, + isMessageEvent, + isToolResultEvent, +} from '../../../common/chat_events'; +import { createChatError, isChatError } from '../../../common/errors'; +import { + type ConversationEvent, + createUserMessage, + createToolResult, +} from '../../../common/conversation_events'; +import type { ChatEvent, ToolResultEvent, MessageEvent } from '../../../common/chat_events'; +import type { Conversation } from '../../../common/conversations'; +import { AgentFactory } from '../orchestration'; +import { ConversationService, ConversationClient } from '../conversations'; +import { generateConversationTitle } from './generate_conversation_title'; + +interface ChatServiceOptions { + logger: Logger; + inference: InferenceServerStart; + conversationService: ConversationService; + agentFactory: AgentFactory; +} + +export class ChatService { + private readonly inference: InferenceServerStart; + private readonly logger: Logger; + private readonly conversationService: ConversationService; + private readonly agentFactory: AgentFactory; + + constructor({ inference, logger, conversationService, agentFactory }: ChatServiceOptions) { + this.inference = inference; + this.logger = logger; + this.conversationService = conversationService; + this.agentFactory = agentFactory; + } + + converse({ + agentId, + conversationId, + connectorId, + request, + nextUserMessage, + }: { + agentId: string; + connectorId: string; + conversationId?: string; + nextUserMessage: string; + request: KibanaRequest; + }) { + const logError = (source: string, err: Error) => { + this.logger.error(`Error during converse from ${source}:\n${err.stack ?? err.message}`); + }; + + const isNewConversation = !conversationId; + const nextUserEvent = createUserMessage({ content: nextUserMessage }); + + return forkJoin({ + agentRunner: defer(() => this.agentFactory.getAgentRunner({ request, connectorId, agentId })), + conversationClient: defer(() => this.conversationService.getScopedClient({ request })), + chatModel: defer(() => + this.inference.getChatModel({ + request, + connectorId, + chatModelOptions: {}, + }) + ), + }).pipe( + switchMap(({ conversationClient, chatModel, agentRunner }) => { + const conversation$ = getConversation$({ + agentId, + conversationId, + conversationClient, + }); + + const conversationEvents$ = conversation$.pipe( + map((conversation) => { + return [...conversation.events, nextUserEvent]; + }), + shareReplay() + ); + + const agentEvents$ = conversationEvents$.pipe( + switchMap((conversationEvents) => { + return defer(() => agentRunner.run({ previousEvents: conversationEvents })); + }), + switchMap((agentRunResult) => { + return agentRunResult.events$; + }), + shareReplay() + ); + + // generate a title for the new conversations + const title$ = isNewConversation + ? generatedTitle$({ chatModel, conversationEvents$ }) + : conversation$.pipe( + switchMap((conversation) => { + return of(conversation.title); + }) + ); + + // extract new conversation events from the agent output + const newConversationEvents$ = extractNewConversationEvents$({ agentEvents$ }).pipe( + map((events) => { + return [nextUserEvent, ...events]; + }) + ); + + // save or update the conversation and emit the corresponding chat event + const saveOrUpdateAndEmit$ = isNewConversation + ? createConversation$({ agentId, title$, newConversationEvents$, conversationClient }) + : updateConversation$({ + title$, + conversation$, + conversationClient, + newConversationEvents$, + }); + + return merge(agentEvents$, saveOrUpdateAndEmit$).pipe( + catchError((err) => { + logError('main observable', err); + return throwError(() => { + if (isChatError(err)) { + return err; + } + return createChatError('internalError', err.message, {}); + }); + }), + shareReplay() + ); + }) + ); + } +} + +const generatedTitle$ = ({ + chatModel, + conversationEvents$, +}: { + chatModel: InferenceChatModel; + conversationEvents$: Observable; +}) => { + return conversationEvents$.pipe( + switchMap((conversationEvents) => { + return defer(async () => + generateConversationTitle({ + conversationEvents, + chatModel, + }) + ).pipe(shareReplay()); + }) + ); +}; + +const getConversation$ = ({ + agentId, + conversationId, + conversationClient, +}: { + agentId: string; + conversationId: string | undefined; + conversationClient: ConversationClient; +}) => { + return defer(() => { + if (conversationId) { + return conversationClient.get({ conversationId }); + } else { + return of(placeholderConversation({ agentId })); + } + }).pipe(shareReplay()); +}; + +const placeholderConversation = ({ agentId }: { agentId: string }): Conversation => { + return { + id: uuidv4(), + title: 'New conversation', // TODO: translate default + agentId, + events: [], + lastUpdated: new Date().toISOString(), + user: { + id: 'unknown', + name: 'unknown', + }, + }; +}; + +/** + * Extract the new conversation events from the output of the agent. + * + * Emits only once with an array of event when the agent events observable completes. + */ +const extractNewConversationEvents$ = ({ + agentEvents$, +}: { + agentEvents$: Observable; +}): Observable => { + return agentEvents$.pipe( + filter((event): event is MessageEvent | ToolResultEvent => { + return isMessageEvent(event) || isToolResultEvent(event); + }), + toArray(), + mergeMap((newMessages) => { + return of( + newMessages.map((message) => { + if (isMessageEvent(message)) { + return message.message; + } else { + return createToolResult({ + toolCallId: message.toolResult.callId, + toolResult: message.toolResult.result, + }); + } + }) + ); + }), + shareReplay() + ); +}; + +/** + * Persist a new conversation and emit the corresponding event + */ +const createConversation$ = ({ + agentId, + conversationClient, + title$, + newConversationEvents$, +}: { + agentId: string; + conversationClient: ConversationClient; + title$: Observable; + newConversationEvents$: Observable; +}) => { + return forkJoin({ + title: title$, + newConversationEvents: newConversationEvents$, + }).pipe( + switchMap(({ title, newConversationEvents }) => { + return conversationClient.create({ + title, + agentId, + events: [...newConversationEvents], + }); + }), + switchMap((updatedConversation) => { + return of( + conversationCreatedEvent({ + title: updatedConversation.title, + id: updatedConversation.id, + }) + ); + }) + ); +}; + +/** + * Update an existing conversation and emit the corresponding event + */ +const updateConversation$ = ({ + conversationClient, + conversation$, + title$, + newConversationEvents$, +}: { + title$: Observable; + conversation$: Observable; + newConversationEvents$: Observable; + conversationClient: ConversationClient; +}) => { + return forkJoin({ + conversation: conversation$, + title: title$, + newConversationEvents: newConversationEvents$, + }).pipe( + switchMap(({ conversation, title, newConversationEvents }) => { + return conversationClient.update(conversation.id, { + title, + events: [...conversation.events, ...newConversationEvents], + }); + }), + switchMap((updatedConversation) => { + return of( + conversationUpdatedEvent({ + title: updatedConversation.title, + id: updatedConversation.id, + }) + ); + }) + ); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/generate_conversation_title.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/generate_conversation_title.ts new file mode 100644 index 000000000000..ca7753a66eae --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/generate_conversation_title.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; +import { ChatPromptTemplate } from '@langchain/core/prompts'; +import type { InferenceChatModel } from '@kbn/inference-langchain'; +import type { ConversationEvent } from '../../../common/conversation_events'; +import { conversationEventsToMessages } from '../orchestration/utils'; + +export const generateConversationTitle = async ({ + conversationEvents, + chatModel, +}: { + conversationEvents: ConversationEvent[]; + chatModel: InferenceChatModel; +}) => { + const structuredModel = chatModel.withStructuredOutput( + z.object({ + title: z.string().describe('The title for the conversation'), + }) + ); + + const prompt = ChatPromptTemplate.fromMessages([ + [ + 'system', + "'You are a helpful assistant. Assume the following message is the start of a conversation between you and a user; give this conversation a title based on the content below", + ], + ['placeholder', '{messages}'], + ]); + + const messages = conversationEventsToMessages(conversationEvents); + + const chain = prompt.pipe(structuredModel); + + const { title } = await chain.invoke({ messages }); + + return title; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/index.ts new file mode 100644 index 000000000000..531449d4e15a --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/chat/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ChatService } from './chat_service'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_client.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_client.ts new file mode 100644 index 000000000000..34d451197c78 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_client.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 as uuidv4 } from 'uuid'; +import type { SavedObjectsClientContract, Logger, SavedObject } from '@kbn/core/server'; +import type { Conversation, ConversationCreateRequest } from '../../../common/conversations'; +import { + conversationTypeName, + type ConversationAttributes, +} from '../../saved_objects/conversations'; +import { createBuilder } from '../../utils/so_filters'; +import { WorkchatError } from '../../errors'; +import type { ClientUser } from './types'; +import { savedObjectToModel, createRequestToRaw, updateToAttributes } from './convert_model'; + +interface ConversationClientOptions { + logger: Logger; + client: SavedObjectsClientContract; + user: ClientUser; +} + +export type ConversationUpdatableFields = Partial>; + +export interface ConversationListOptions { + agentId?: string; +} + +export interface ConversationClient { + list(options?: ConversationListOptions): Promise; + get(options: { conversationId: string }): Promise; + create(conversation: ConversationCreateRequest): Promise; + update(conversationId: string, fields: ConversationUpdatableFields): Promise; +} + +export class ConversationClientImpl implements ConversationClient { + private readonly client: SavedObjectsClientContract; + private readonly user: ClientUser; + // @ts-expect-error will be used at some point + private readonly logger: Logger; + + constructor({ client, user, logger }: ConversationClientOptions) { + this.client = client; + this.user = user; + this.logger = logger; + } + + async list(options: ConversationListOptions = {}): Promise { + const builder = createBuilder(conversationTypeName); + const filter = builder + .and( + builder.equals('user_id', this.user.id), + ...(options.agentId ? [builder.equals('agent_id', options.agentId)] : []) + ) + .toKQL(); + const { saved_objects: results } = await this.client.find({ + type: conversationTypeName, + filter, + perPage: 1000, + }); + + return results.map(savedObjectToModel); + } + + async get({ conversationId }: { conversationId: string }): Promise { + const conversationSo = await this._rawGet({ conversationId }); + return savedObjectToModel(conversationSo); + } + + async create(conversation: ConversationCreateRequest): Promise { + const now = new Date(); + const id = conversation.id ?? uuidv4(); + const attributes = createRequestToRaw({ + conversation, + id, + user: this.user, + creationDate: now, + }); + const created = await this.client.create( + conversationTypeName, + attributes, + { id } + ); + return savedObjectToModel(created); + } + + async update( + conversationId: string, + updatedFields: ConversationUpdatableFields + ): Promise { + const conversationSo = await this._rawGet({ conversationId }); + const updatedAttributes = { + ...conversationSo.attributes, + ...updateToAttributes({ updatedFields }), + }; + + await this.client.update( + conversationTypeName, + conversationSo.id, + updatedAttributes + ); + + return savedObjectToModel({ + ...conversationSo, + attributes: updatedAttributes, + }); + } + + private async _rawGet({ + conversationId, + }: { + conversationId: string; + }): Promise> { + const builder = createBuilder(conversationTypeName); + const filter = builder + .and( + builder.equals('user_id', this.user.id), + builder.equals('conversation_id', conversationId) + ) + .toKQL(); + + const { saved_objects: results } = await this.client.find({ + type: conversationTypeName, + filter, + }); + if (results.length > 0) { + return results[0]; + } + throw new WorkchatError(`Conversation ${conversationId} not found`, 404); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_service.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_service.ts new file mode 100644 index 000000000000..0b1b98052658 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/conversation_service.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObjectsServiceStart, + KibanaRequest, + Logger, + SecurityServiceStart, +} from '@kbn/core/server'; +import { conversationTypeName } from '../../saved_objects/conversations'; +import { ConversationClientImpl, ConversationClient } from './conversation_client'; + +interface ConversationServiceOptions { + logger: Logger; + savedObjects: SavedObjectsServiceStart; + security: SecurityServiceStart; +} + +export interface ConversationService { + /** + * Returns a conversation client scoped to the current user. + */ + getScopedClient(options: { request: KibanaRequest }): Promise; +} + +export class ConversationServiceImpl implements ConversationService { + private readonly savedObjects: SavedObjectsServiceStart; + private readonly security: SecurityServiceStart; + private readonly logger: Logger; + + constructor({ savedObjects, security, logger }: ConversationServiceOptions) { + this.savedObjects = savedObjects; + this.security = security; + this.logger = logger; + } + + async getScopedClient({ request }: { request: KibanaRequest }): Promise { + const user = this.security.authc.getCurrentUser(request); + if (!user) { + throw new Error('No user bound to the provided request'); + } + const soClient = this.savedObjects.getScopedClient(request, { + includedHiddenTypes: [conversationTypeName], + }); + + return new ConversationClientImpl({ + logger: this.logger.get('client'), + client: soClient, + user: { id: user.profile_uid!, username: user.username }, + }); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/convert_model.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/convert_model.ts new file mode 100644 index 000000000000..365bf68f7835 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/convert_model.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObject } from '@kbn/core/server'; +import type { Conversation, ConversationCreateRequest } from '../../../common/conversations'; +import type { ConversationAttributes } from '../../saved_objects/conversations'; +import type { ClientUser } from './types'; +import type { ConversationUpdatableFields } from './conversation_client'; + +export const savedObjectToModel = ({ + attributes, +}: SavedObject): Conversation => { + return { + id: attributes.conversation_id, + agentId: attributes.agent_id, + title: attributes.title, + lastUpdated: attributes.last_updated, + user: { + id: attributes.user_id, + name: attributes.user_name, + }, + events: attributes.events, + }; +}; + +export const updateToAttributes = ({ + updatedFields, +}: { + updatedFields: ConversationUpdatableFields; +}): Partial => { + return { + title: updatedFields.title, + events: updatedFields.events, + }; +}; + +export const createRequestToRaw = ({ + conversation, + id, + user, + creationDate, +}: { + conversation: ConversationCreateRequest; + id: string; + user: ClientUser; + creationDate: Date; +}): ConversationAttributes => { + return { + conversation_id: id, + agent_id: conversation.agentId, + title: conversation.title, + last_updated: creationDate.toISOString(), + user_id: user.id, + user_name: user.username, + events: conversation.events, + access_control: { + public: false, + }, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/index.ts new file mode 100644 index 000000000000..accf2dd1c889 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ConversationServiceImpl, type ConversationService } from './conversation_service'; +export type { ConversationClient } from './conversation_client'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/mocks.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/mocks.ts new file mode 100644 index 000000000000..0cd096730a30 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/mocks.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ConversationClient } from './conversation_client'; +import type { ConversationService } from './conversation_service'; + +const createConversationClientMock = () => { + const mocked: jest.Mocked = { + list: jest.fn(), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }; + return mocked; +}; + +const createConversationServiceMock = () => { + const mocked: jest.Mocked = { + getScopedClient: jest.fn().mockReturnValue(createConversationClientMock()), + }; + return mocked; +}; + +export const conversationMocks = { + create: createConversationServiceMock, + createClient: createConversationClientMock, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/types.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/types.ts new file mode 100644 index 000000000000..b80517cbdb69 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/conversations/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ClientUser { + id: string; + username: string; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/create_services.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/create_services.ts new file mode 100644 index 000000000000..1cebda815a03 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/create_services.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreStart } from '@kbn/core/server'; +import type { LoggerFactory } from '@kbn/core/server'; +import type { WorkChatAppPluginStartDependencies } from '../types'; +import type { InternalServices } from './types'; +import { IntegrationsServiceImpl } from './integrations/integrations_service'; +import { ConversationServiceImpl } from './conversations'; +import { ChatService } from './chat'; +import { AgentFactory } from './orchestration'; +import { AgentServiceImpl } from './agents'; +import { IntegrationRegistry } from './integrations'; + +interface CreateServicesParams { + core: CoreStart; + logger: LoggerFactory; + pluginsDependencies: WorkChatAppPluginStartDependencies; + integrationRegistry: IntegrationRegistry; +} + +export function createServices({ + core, + logger, + pluginsDependencies, + integrationRegistry, +}: CreateServicesParams): InternalServices { + integrationRegistry.blockRegistration(); + + const integrationsService = new IntegrationsServiceImpl({ + logger: logger.get('services.integrations'), + elasticsearch: core.elasticsearch, + registry: integrationRegistry, + savedObjects: core.savedObjects, + security: core.security, + }); + + const conversationService = new ConversationServiceImpl({ + savedObjects: core.savedObjects, + security: core.security, + logger: logger.get('services.conversations'), + }); + + const agentService = new AgentServiceImpl({ + savedObjects: core.savedObjects, + security: core.security, + logger: logger.get('services.agent'), + }); + + const agentFactory = new AgentFactory({ + inference: pluginsDependencies.inference, + logger: logger.get('services.agentFactory'), + agentService, + integrationsService, + }); + + const chatService = new ChatService({ + inference: pluginsDependencies.inference, + logger: logger.get('services.chat'), + agentFactory, + conversationService, + }); + + return { + conversationService, + agentService, + agentFactory, + integrationsService, + chatService, + }; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/index.ts new file mode 100644 index 000000000000..90ac623686fe --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { InternalServices } from './types'; +export { IntegrationsServiceImpl } from './integrations/integrations_service'; +export { ConversationServiceImpl } from './conversations'; +export { ChatService } from './chat'; +export { AgentFactory } from './orchestration'; +export { AgentServiceImpl, type AgentService } from './agents'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/convert_model.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/convert_model.ts new file mode 100644 index 000000000000..00411b3e90a8 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/convert_model.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObject } from '@kbn/core/server'; +import type { Integration, IntegrationCreateRequest } from '../../../common/integrations'; +import type { IntegrationAttributes } from '../../saved_objects/integrations'; +import type { ClientUser } from './types'; + +export const savedObjectToModel = ({ + attributes, +}: SavedObject): Integration => { + return { + id: attributes.integration_id, + type: attributes.type, + name: attributes.name, + description: attributes.description, + configuration: attributes.configuration, + createdAt: attributes.created_at, + updatedAt: attributes.updated_at, + createdBy: attributes.created_by, + }; +}; + +export const updateToAttributes = ({ + updatedFields, +}: { + updatedFields: Partial; +}): Partial => { + const result: Partial = {}; + + if (updatedFields.name !== undefined) { + result.name = updatedFields.name; + } + + if (updatedFields.description !== undefined) { + result.description = updatedFields.description; + } + + if (updatedFields.configuration !== undefined) { + result.configuration = updatedFields.configuration; + } + + if (Object.keys(result).length > 0) { + result.updated_at = new Date().toISOString(); + } + + return result; +}; + +export const createRequestToRaw = ({ + integration, + id, + user, + creationDate, +}: { + integration: IntegrationCreateRequest; + id: string; + user: ClientUser; + creationDate: Date; +}): IntegrationAttributes => { + const now = creationDate.toISOString(); + + return { + integration_id: id, + type: integration.type, + name: integration.name, + description: integration.description, + configuration: integration.configuration, + created_at: now, + updated_at: now, + created_by: user.id, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/index.ts new file mode 100644 index 000000000000..3444a62082f2 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { IntegrationRegistry } from './integration_registry'; +export { IntegrationsServiceImpl, type IntegrationsService } from './integrations_service'; +export type { IntegrationClient } from './integration_client'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_client.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_client.ts new file mode 100644 index 000000000000..62b4c1c30123 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_client.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 as uuidv4 } from 'uuid'; +import type { SavedObjectsClientContract, Logger, SavedObject } from '@kbn/core/server'; +import type { Integration, IntegrationCreateRequest } from '../../../common/integrations'; +import { integrationTypeName, type IntegrationAttributes } from '../../saved_objects/integrations'; +import type { ClientUser } from './types'; +import { savedObjectToModel, createRequestToRaw, updateToAttributes } from './convert_model'; + +interface IntegrationClientOptions { + logger: Logger; + client: SavedObjectsClientContract; + user: ClientUser; +} + +export type IntegrationUpdatableFields = Partial< + Pick +>; + +export interface IntegrationClient { + list(): Promise; + get(options: { integrationId: string }): Promise; + create(integration: IntegrationCreateRequest): Promise; + update(integrationId: string, fields: IntegrationUpdatableFields): Promise; + delete(integrationId: string): Promise; +} + +export class IntegrationClientImpl implements IntegrationClient { + private readonly client: SavedObjectsClientContract; + private readonly user: ClientUser; + // @ts-ignore will be used later + private readonly logger: Logger; + + constructor({ client, user, logger }: IntegrationClientOptions) { + this.client = client; + this.user = user; + this.logger = logger; + } + + async list(): Promise { + const { saved_objects: results } = await this.client.find({ + type: integrationTypeName, + perPage: 1000, + }); + + return results.map(savedObjectToModel); + } + + async get({ integrationId }: { integrationId: string }): Promise { + const integrationSo = await this._rawGet({ integrationId }); + return savedObjectToModel(integrationSo); + } + + async create(integration: IntegrationCreateRequest): Promise { + const now = new Date(); + const id = integration.id ?? uuidv4(); + const attributes = createRequestToRaw({ + integration, + id, + user: this.user, + creationDate: now, + }); + const created = await this.client.create( + integrationTypeName, + attributes, + { id } + ); + return savedObjectToModel(created); + } + + async update( + integrationId: string, + updatedFields: IntegrationUpdatableFields + ): Promise { + const integrationSo = await this._rawGet({ integrationId }); + const updatedAttributes = { + ...integrationSo.attributes, + ...updateToAttributes({ updatedFields }), + }; + + await this.client.update( + integrationTypeName, + integrationSo.id, + updatedAttributes + ); + + return savedObjectToModel({ + ...integrationSo, + attributes: updatedAttributes, + }); + } + + async delete(integrationId: string): Promise { + const integrationSo = await this._rawGet({ integrationId }); + await this.client.delete(integrationTypeName, integrationSo.id); + } + + private async _rawGet({ + integrationId, + }: { + integrationId: string; + }): Promise> { + const { saved_objects: results } = await this.client.find({ + type: integrationTypeName, + filter: `${integrationTypeName}.attributes.integration_id: ${integrationId}`, + }); + if (results.length > 0) { + return results[0]; + } + throw new Error(`Integration ${integrationId} not found`); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_registry.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_registry.ts new file mode 100644 index 000000000000..04a33071b37f --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integration_registry.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IntegrationType } from '@kbn/wci-common'; +import type { WorkchatIntegrationDefinition } from '@kbn/wci-server'; + +export class IntegrationRegistry { + private allowRegistration = true; + private integrationTypes = new Map(); + + register(definition: WorkchatIntegrationDefinition) { + if (!this.allowRegistration) { + throw new Error(`Tried to register integration but allowRegistration is false`); + } + if (this.has(definition.getType())) { + throw new Error( + `Tried to register Integration [${definition.getType()}], but alrrady registered` + ); + } + this.integrationTypes.set(definition.getType(), definition); + } + + blockRegistration() { + this.allowRegistration = false; + } + + has(type: IntegrationType) { + return this.integrationTypes.has(type); + } + + get(type: IntegrationType) { + if (!this.has(type)) { + throw new Error(`Integration definition for type [${type}] not found`); + } + return this.integrationTypes.get(type)!; + } + + getAll() { + return [...this.integrationTypes.values()]; + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integrations_service.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integrations_service.ts new file mode 100644 index 000000000000..cde70d431dd6 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/integrations_service.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + Logger, + ElasticsearchServiceStart, + KibanaRequest, + SavedObjectsServiceStart, + SecurityServiceStart, +} from '@kbn/core/server'; +import type { McpProvider } from '@kbn/wci-server'; +import type { Integration } from '../../../common/integrations'; +import { integrationTypeName } from '../../saved_objects/integrations'; +import type { IntegrationRegistry } from './integration_registry'; +import { IntegrationClientImpl, IntegrationClient } from './integration_client'; + +interface IntegrationsServiceOptions { + logger: Logger; + elasticsearch: ElasticsearchServiceStart; + registry: IntegrationRegistry; + savedObjects: SavedObjectsServiceStart; + security: SecurityServiceStart; +} + +export interface IntegrationsService { + /** + * Returns an integration client scoped to the current user. + */ + getScopedClient({ request }: { request: KibanaRequest }): Promise; + + /** + * Create integration providers for given integration ids. Use '*' to resolve all integrations + */ + getIntegrationProviders({ + integrationIds, + request, + }: { + integrationIds: string[] | '*'; + request: KibanaRequest; + }): Promise; +} + +export class IntegrationsServiceImpl implements IntegrationsService { + private readonly logger: Logger; + private readonly registry: IntegrationRegistry; + private readonly savedObjects: SavedObjectsServiceStart; + private readonly security: SecurityServiceStart; + + constructor({ logger, registry, savedObjects, security }: IntegrationsServiceOptions) { + this.logger = logger; + this.registry = registry; + this.savedObjects = savedObjects; + this.security = security; + } + + async getScopedClient({ request }: { request: KibanaRequest }): Promise { + const user = this.security.authc.getCurrentUser(request); + if (!user) { + throw new Error('No user bound to the provided request'); + } + const soClient = this.savedObjects.getScopedClient(request, { + includedHiddenTypes: [integrationTypeName], + }); + + return new IntegrationClientImpl({ + logger: this.logger.get('client'), + client: soClient, + user: { id: user.profile_uid!, username: user.username }, + }); + } + + async getIntegrationProviders({ + integrationIds, + request, + }: { + integrationIds: string[] | '*'; + request: KibanaRequest; + }): Promise { + const client = await this.getScopedClient({ request }); + + let integrations: Integration[] = []; + if (typeof integrationIds === 'string') { + integrations = await client.list(); + } else { + for (const integrationId of integrationIds) { + // TODO: bulk get on client + integrations.push(await client.get({ integrationId })); + } + } + + return await Promise.all( + integrations.map>(async (source) => { + return integrationToProvider({ + integration: source, + request, + registry: this.registry, + }); + }) + ); + } +} + +const integrationToProvider = async ({ + integration, + registry, + request, +}: { + integration: Integration; + registry: IntegrationRegistry; + request: KibanaRequest; +}): Promise => { + const definition = registry.get(integration.type); + const instance = await definition.createIntegration({ + request, + integrationId: integration.id, + configuration: integration.configuration, + description: integration.description, + }); + return { + id: integration.id, + connect: instance.connect, + meta: {}, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/mocks.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/mocks.ts new file mode 100644 index 000000000000..ef948402097c --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/mocks.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IntegrationClient } from './integration_client'; +import type { IntegrationsService } from './integrations_service'; + +const createIntegrationClientMock = () => { + const mocked: jest.Mocked = { + list: jest.fn(), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }; + return mocked; +}; + +const createIntegrationsServiceMock = () => { + const mocked: jest.Mocked = { + getScopedClient: jest.fn().mockReturnValue(createIntegrationClientMock()), + getIntegrationProviders: jest.fn(), + }; + return mocked; +}; + +export const integrationMocks = { + create: createIntegrationsServiceMock, + createClient: createIntegrationClientMock, +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/types.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/types.ts new file mode 100644 index 000000000000..5c6c364861e4 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/integrations/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO: remove to use UserNameAndId from common instead +export interface ClientUser { + id: string; + username: string; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_factory.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_factory.ts new file mode 100644 index 000000000000..38097ae99875 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_factory.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest, Logger } from '@kbn/core/server'; +import { InferenceServerStart } from '@kbn/inference-plugin/server'; +import { IntegrationsServiceImpl } from '../integrations/integrations_service'; +import type { AgentService } from '../agents'; +import type { AgentRunner } from './types'; +import { createAgentRunner } from './agent_runner'; +import { McpGatewaySession, McpGatewaySessionImpl } from './mcp_gateway'; +import { getBaseToolProvider } from './base_tools'; + +interface AgentFactoryArgs { + logger: Logger; + inference: InferenceServerStart; + agentService: AgentService; + integrationsService: IntegrationsServiceImpl; +} + +export class AgentFactory { + private readonly inference: InferenceServerStart; + private readonly logger: Logger; + private readonly agentService: AgentService; + private readonly integrationsService: IntegrationsServiceImpl; + + constructor({ inference, logger, integrationsService, agentService }: AgentFactoryArgs) { + this.inference = inference; + this.logger = logger; + this.integrationsService = integrationsService; + this.agentService = agentService; + } + + async getAgentRunner({ + request, + connectorId, + agentId, + }: { + agentId: string; + request: KibanaRequest; + connectorId: string; + }): Promise { + this.logger.debug(`getAgent [agentId=${agentId}] [connectorId=${connectorId}]`); + + const createSession = async () => { + return await this.createGatewaySession({ + request, + }); + }; + + const agentClient = await this.agentService.getScopedClient({ request }); + + const agent = await agentClient.get({ agentId }); + + const chatModel = await this.inference.getChatModel({ + request, + connectorId, + chatModelOptions: {}, + }); + + return await createAgentRunner({ agent, chatModel, createSession, logger: this.logger }); + } + + private async createGatewaySession({ + request, + }: { + request: KibanaRequest; + }): Promise { + const integrationProviders = await this.integrationsService.getIntegrationProviders({ + integrationIds: '*', + request, + }); + const additionalProviders = [await getBaseToolProvider()]; + return new McpGatewaySessionImpl({ + providers: [...integrationProviders, ...additionalProviders], + logger: this.logger.get('session'), + }); + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_graph.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_graph.ts new file mode 100644 index 000000000000..75171ccf5ef4 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_graph.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { StateGraph, Annotation } from '@langchain/langgraph'; +import { BaseMessage, AIMessage } from '@langchain/core/messages'; +import { messagesStateReducer } from '@langchain/langgraph'; +import { ToolNode } from '@langchain/langgraph/prebuilt'; +import { StructuredTool } from '@langchain/core/tools'; +import { InferenceChatModel } from '@kbn/inference-langchain'; +import type { Agent } from '../../../common/agents'; +import { withSystemPrompt } from './prompts'; + +export const createAgentGraph = async ({ + agent, + chatModel, + integrationTools, +}: { + agent: Agent; + chatModel: InferenceChatModel; + integrationTools: StructuredTool[]; +}) => { + const StateAnnotation = Annotation.Root({ + initialMessages: Annotation({ + reducer: messagesStateReducer, + default: () => [], + }), + addedMessages: Annotation({ + reducer: messagesStateReducer, + default: () => [], + }), + }); + + const tools = [...integrationTools]; + + const toolNode = new ToolNode(tools); + + const model = chatModel.bindTools(tools).withConfig({ + tags: ['workflow', `agent:${agent.id}`], + }); + + const callModel = async (state: typeof StateAnnotation.State) => { + const response = await model.invoke( + await withSystemPrompt({ + agentPrompt: agent.configuration.systemPrompt, + messages: [...state.initialMessages, ...state.addedMessages], + }) + ); + return { + addedMessages: [response], + }; + }; + + const shouldContinue = async (state: typeof StateAnnotation.State) => { + const messages = state.addedMessages; + const lastMessage: AIMessage = messages[messages.length - 1]; + if (lastMessage && lastMessage.tool_calls?.length) { + return 'tools'; + } + return '__end__'; + }; + + const toolHandler = async (state: typeof StateAnnotation.State) => { + const toolNodeResult = await toolNode.invoke(state.addedMessages); + return { + addedMessages: [...state.addedMessages, ...toolNodeResult], + }; + }; + + const graph = new StateGraph(StateAnnotation) + .addNode('agent', callModel) + .addNode('tools', toolHandler) + .addEdge('__start__', 'agent') + .addEdge('tools', 'agent') + .addConditionalEdges('agent', shouldContinue, { + tools: 'tools', + __end__: '__end__', + }) + .compile(); + + return graph; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_runner.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_runner.ts new file mode 100644 index 000000000000..340af1f9a6dc --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/agent_runner.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { from, filter, shareReplay } from 'rxjs'; +import { StreamEvent } from '@langchain/core/tracers/log_stream'; +import type { Logger } from '@kbn/core/server'; +import type { InferenceChatModel } from '@kbn/inference-langchain'; +import type { Agent } from '../../../common/agents'; +import { getLCTools } from './mcp_gateway/utils'; +import { createAgentGraph } from './agent_graph'; +import { langchainToChatEvents, conversationEventsToMessages } from './utils'; +import type { AgentRunner, AgentRunResult } from './types'; +import type { McpGatewaySession } from './mcp_gateway'; + +export const createAgentRunner = async ({ + logger, + agent, + chatModel, + createSession, +}: { + logger: Logger; + agent: Agent; + chatModel: InferenceChatModel; + createSession: () => Promise; +}): Promise => { + const session = await createSession(); + + const closeSession = () => { + session.close().catch((err) => { + logger.warn(`Error disconnecting integrations: ${err.message}`); + }); + }; + + const integrationTools = await getLCTools({ session, logger }); + + const agentGraph = await createAgentGraph({ agent, chatModel, integrationTools }); + + return { + run: async ({ previousEvents }): Promise => { + const initialMessages = conversationEventsToMessages(previousEvents); + + const runName = 'defaultAgentGraph'; + + const eventStream = agentGraph.streamEvents( + { initialMessages }, + { + version: 'v2', + runName, + metadata: { + agentId: agent.id, + }, + recursionLimit: 5, + } + ); + + const events$ = from(eventStream).pipe( + filter(isStreamEvent), + langchainToChatEvents({ runName }), + shareReplay() + ); + + events$.subscribe({ + complete: () => { + closeSession(); + }, + error: () => { + closeSession(); + }, + }); + + return { + events$, + }; + }, + }; +}; + +const isStreamEvent = (input: any): input is StreamEvent => { + return 'event' in input; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/base_tools_provider.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/base_tools_provider.ts new file mode 100644 index 000000000000..d6f3b2c2c17a --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/base_tools_provider.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getConnectToInternalServer, createMcpServer, type McpProvider } from '@kbn/wci-server'; +import { baseToolsProviderId } from '../../../../common/constants'; +import { getCalculatorTool } from './calculator'; + +export const getBaseToolProvider = async (): Promise => { + const tools = [getCalculatorTool()]; + + const server = createMcpServer({ + name: baseToolsProviderId, + version: '1.0.0', + tools, + }); + + return { + id: baseToolsProviderId, + connect: getConnectToInternalServer({ + server, + clientName: 'baseToolsClient', + }), + meta: { + builtin: true, + }, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/calculator.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/calculator.ts new file mode 100644 index 000000000000..2641a3722ebf --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/calculator.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Parser } from 'expr-eval'; +import { z } from '@kbn/zod'; +import { type McpTool, toolResult } from '@kbn/wci-server'; + +export const getCalculatorTool = (): McpTool => { + return { + name: 'calculator', + description: ` + Useful for getting the result of a math expression. + The input should be a valid mathematical expression that could be executed by a simple calculator. + + Examples: + - 125 * (5 + 19) + - 4 - 12^3 + `, + schema: { + input: z.string().describe('the expression to evaluate'), + }, + execute: async ({ input }) => { + try { + return toolResult.text(Parser.evaluate(input).toString()); + } catch (e) { + return toolResult.error(`Error evaluating expression: ${e.message}`); + } + }, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/index.ts new file mode 100644 index 000000000000..3709f908d7aa --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/base_tools/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getBaseToolProvider } from './base_tools_provider'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/index.ts new file mode 100644 index 000000000000..9d9878772abc --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AgentFactory } from './agent_factory'; +export type { AgentRunner } from './types'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/index.ts new file mode 100644 index 000000000000..63ef1e25ec93 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { type McpGatewaySession, McpGatewaySessionImpl } from './session'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.test.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.test.ts new file mode 100644 index 000000000000..db1e74f445db --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from '@kbn/zod'; +import { loggerMock } from '@kbn/logging-mocks'; +import { buildToolName } from '@kbn/wci-common'; +import { getConnectToInternalServer } from '@kbn/wci-server'; +import { IntegrationToolInputSchema } from './types'; +import { McpGatewaySessionImpl } from './session'; +import type { McpProvider } from '@kbn/wci-server'; + +describe('McpGatewaySession', () => { + describe('MCP servers with tools', () => { + const logger = loggerMock.create(); + + const serverA = () => { + const server = new McpServer({ + name: 'Test Server 1', + version: '1.0.0', + }); + + server.tool('add', { a: z.number(), b: z.number() }, async ({ a, b }) => ({ + content: [{ type: 'text', text: String(a + b) }], + })); + + return server; + }; + + const serverB = () => { + const server = new McpServer({ + name: 'Test Server 2', + version: '1.0.0', + }); + + server.tool('tool3', { test: z.string() }, async ({ test }) => ({ + content: [ + { type: 'text', text: `Tool 3 executed with params: ${JSON.stringify({ test })}` }, + ], + })); + + return server; + }; + + const getProviders = async () => { + const provider1: McpProvider = { + id: 'test-client-1', + connect: () => + getConnectToInternalServer({ + server: serverA(), + clientName: 'Test Server 1', + })(), + }; + + const provider2: McpProvider = { + id: 'test-client-2', + connect: () => + getConnectToInternalServer({ + server: serverB(), + clientName: 'Test Server 2', + })(), + }; + + return [provider1, provider2]; + }; + + it('should register multiple MCP servers with tools and call all tools', async () => { + const integrationSession = new McpGatewaySessionImpl({ + logger, + providers: await getProviders(), + }); + + const allTools = await integrationSession.listTools(); + expect(allTools.length).toBe(2); + expect(allTools.map((tool) => tool.name)).toEqual([ + buildToolName({ integrationId: 'test-client-1', toolName: 'add' }), + buildToolName({ integrationId: 'test-client-2', toolName: 'tool3' }), + ]); + }); + + it('should allow to call a tool', async () => { + const integrationSession = new McpGatewaySessionImpl({ + logger, + providers: await getProviders(), + }); + + const result = await integrationSession.executeTool( + buildToolName({ integrationId: 'test-client-1', toolName: 'add' }), + { + a: 1, + b: 2, + } as unknown as IntegrationToolInputSchema + ); + expect(result).toEqual({ content: [{ type: 'text', text: '3' }] }); + }); + }); +}); diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.ts new file mode 100644 index 000000000000..958b7017fb37 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/session.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { parseToolName } from '@kbn/wci-common'; +import type { McpProvider, McpClient } from '@kbn/wci-server'; +import type { Logger } from '@kbn/core/server'; +import { IntegrationTool, IntegrationToolInputSchema } from './types'; +import { listClientsTools } from './utils'; + +/** + * A request-bound + */ +export interface McpGatewaySession { + /** + * List all available tools from all connected integrations. + */ + listTools(): Promise; + /** + * Execute a tool from a specific integration. + */ + executeTool(serverToolName: string, params: IntegrationToolInputSchema): Promise; + /** + * Close the session and disconnect from all integrations. + */ + close(): Promise; +} + +export class McpGatewaySessionImpl implements McpGatewaySession { + private readonly providers: McpProvider[]; + private readonly logger: Logger; + + private sessionClients: Record = {}; + private connected = false; + + constructor({ providers, logger }: { providers: McpProvider[]; logger: Logger }) { + this.providers = providers; + this.logger = logger; + } + + private async ensureConnected() { + if (this.connected) { + return; + } + + this.sessionClients = await this.providers.reduce(async (accPromise, provider) => { + const acc = await accPromise; + try { + acc[provider.id] = await provider.connect(); + } catch (e) { + this.logger.warn(`Error connecting integration: ${provider.id}`); + } + return acc; + }, Promise.resolve>({})); + + this.connected = true; + } + + async listTools(): Promise { + await this.ensureConnected(); + return listClientsTools({ clients: this.sessionClients, logger: this.logger }); + } + + async executeTool(serverToolName: string, params: IntegrationToolInputSchema) { + await this.ensureConnected(); + + const { integrationId, toolName } = parseToolName(serverToolName); + const client = this.sessionClients[integrationId]; + if (!client) { + throw new Error(`Client not found: ${integrationId}`); + } + return client.callTool({ + name: toolName, + arguments: params, + }); + } + + async close() { + if (!this.connected) { + return; + } + + await Promise.all(Object.values(this.sessionClients).map((session) => session.disconnect())); + this.sessionClients = {}; + this.connected = false; + } +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/types.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/types.ts new file mode 100644 index 000000000000..7add79443866 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { JsonSchemaObject } from '@n8n/json-schema-to-zod'; + +export type IntegrationToolInputSchema = Tool['inputSchema']; + +export interface IntegrationTool { + name: string; + description: string; + inputSchema: JsonSchemaObject; +} + +export interface IntegrationTool { + name: string; + description: string; + inputSchema: JsonSchemaObject; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/index.ts new file mode 100644 index 000000000000..0fdc946efd03 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { listClientsTools } from './list_clients_tools'; +export { getLCTools } from './to_langchain_tool'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/list_clients_tools.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/list_clients_tools.ts new file mode 100644 index 000000000000..eef7da4a33af --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/list_clients_tools.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JsonSchemaObject } from '@n8n/json-schema-to-zod'; +import { buildToolName } from '@kbn/wci-common'; +import type { McpClient } from '@kbn/wci-server'; +import type { Logger } from '@kbn/core/server'; +import { IntegrationTool } from '../types'; + +/** + * Retrieve all the tools from a list of MCP clients. + */ +export const listClientsTools = async ({ + clients, + logger, +}: { + clients: Record; + logger?: Logger; +}): Promise => { + const clientsTools = await Promise.all( + Object.entries(clients).map>(async ([clientId, client]) => { + try { + const toolsResponse = await client.listTools(); + if (toolsResponse?.tools?.length) { + return toolsResponse.tools.map((tool) => ({ + name: buildToolName({ integrationId: clientId, toolName: tool.name }), + description: tool.description || '', + inputSchema: (tool.inputSchema || {}) as JsonSchemaObject, + })); + } + return []; + } catch (err) { + logger?.warn(`Error fetching tools for client: ${clientId}`); + return []; + } + }) + ); + + return clientsTools.flat(); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/to_langchain_tool.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/to_langchain_tool.ts new file mode 100644 index 000000000000..b80b9cfe5a50 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/mcp_gateway/utils/to_langchain_tool.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { StructuredTool, tool as toTool } from '@langchain/core/tools'; +import { Logger } from '@kbn/core/server'; +import { jsonSchemaToZod } from '@n8n/json-schema-to-zod'; +import { IntegrationTool } from '../types'; +import { McpGatewaySession } from '../session'; + +export async function getLCTools({ + session, + logger, +}: { + session: McpGatewaySession; + logger: Logger; +}): Promise { + const tools = await session.listTools(); + return tools.map((tool) => + toLangchainTool(tool, async (input) => { + try { + const result = await session.executeTool(tool.name, input); + return JSON.stringify(result); + } catch (e) { + logger.warn(`error calling tool ${tool.name}: ${e.message}`); + throw e; + } + }) + ); +} + +function toLangchainTool( + integrationTool: IntegrationTool, + action: (input: any) => Promise +): StructuredTool { + const schema = jsonSchemaToZod(integrationTool.inputSchema); + + return toTool(action, { + name: integrationTool.name, + description: integrationTool.description, + schema, + }); +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/prompts.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/prompts.ts new file mode 100644 index 000000000000..80c44e7aed14 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/prompts.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BaseMessage } from '@langchain/core/messages'; +import { ChatPromptTemplate } from '@langchain/core/prompts'; + +const getSystemPrompt = (agentPrompt: string) => { + return `You are a helpful chat assistant from the Elasticsearch company. + + You have tools at your disposal that you can use to answer the user's question. + E.g. when available and relevant, use the search docs tool to search the knowledge base for relevant documents. + + ### Specific agent instructions + + ${agentPrompt} + + ### Additional info + - The current date is: ${new Date().toISOString()} + - You can use markdown format to structure your response + `; +}; + +export const withSystemPrompt = async ({ + agentPrompt, + messages, +}: { + agentPrompt: string; + messages: BaseMessage[]; +}) => { + return await ChatPromptTemplate.fromMessages([ + ['system', getSystemPrompt(agentPrompt)], + ['placeholder', '{messages}'], + ]).invoke({ messages }); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/types.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/types.ts new file mode 100644 index 000000000000..b7302dc7bfe7 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/types.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Observable } from 'rxjs'; +import type { ChunkEvent, MessageEvent, ToolResultEvent } from '../../../common/chat_events'; +import type { ConversationEvent } from '../../../common/conversation_events'; + +/** + * Represents an instance of a configured agent, ready to run. + */ +export interface AgentRunner { + run(options: AgentRunOptions): Promise; +} + +/** + * Options to call {@link AgentRunner.run} + */ +export interface AgentRunOptions { + previousEvents: ConversationEvent[]; +} + +/** + * Subsets of chat events the agent can emit + */ +export type AgentRunEvents = ChunkEvent | MessageEvent | ToolResultEvent; + +/** + * Output of {@link AgentRunner.run} + */ +export interface AgentRunResult { + /** + * Live events that can be streamed back to the UI + */ + events$: Observable; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.test.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.test.ts new file mode 100644 index 000000000000..09d70b1c08c1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.test.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { firstValueFrom, lastValueFrom, of, toArray } from 'rxjs'; +import { langchainToChatEvents } from './convert_langchain_events'; +import { AIMessageChunk, AIMessage } from '@langchain/core/messages'; +import { StreamEvent as LangchainStreamEvent } from '@langchain/core/tracers/log_stream'; + +const createLangchainEvent = ({ + event, + name, + data, + metadata = {}, +}: { + event: string; + name: string; + data: any; + metadata?: Record; +}): LangchainStreamEvent => ({ + event, + name, + data, + metadata, + run_id: 'test-run-id', +}); + +describe('langchainToChatEvents', () => { + const runName = 'test-run'; + + it('should convert chat model stream events to message chunks', async () => { + const chunk = new AIMessageChunk({ content: 'Hello world', id: 'test-message-id' }); + + const event = createLangchainEvent({ + event: 'on_chat_model_stream', + name: 'chat_model', + data: { chunk }, + }); + + const result = await firstValueFrom(of(event).pipe(langchainToChatEvents({ runName }))); + + expect(result).toEqual({ + type: 'message_chunk', + content_chunk: 'Hello world', + message_id: 'test-message-id', + }); + }); + + it('should convert agent chain end events to messages', async () => { + const message = new AIMessage({ content: 'Agent response', id: 'test-message-id' }); + + const event = createLangchainEvent({ + event: 'on_chain_end', + name: 'agent', + data: { + output: { + addedMessages: [message], + }, + }, + }); + + const result = await firstValueFrom(of(event).pipe(langchainToChatEvents({ runName }))); + + expect(result).toEqual({ + type: 'message', + message: { + content: 'Agent response', + type: 'assistant_message', + id: 'test-message-id', + toolCalls: [], + createdAt: expect.any(String), + }, + }); + }); + + it('should convert tool end events to tool results', async () => { + const event = createLangchainEvent({ + event: 'on_tool_end', + name: 'tool', + data: { + output: { + tool_call_id: 'test-tool-call-id', + content: 'Tool execution result', + }, + }, + metadata: { langgraph_node: 'tools' }, + }); + + const result = await firstValueFrom(of(event).pipe(langchainToChatEvents({ runName }))); + + expect(result).toEqual({ + type: 'tool_result', + toolResult: { + callId: 'test-tool-call-id', + result: 'Tool execution result', + }, + }); + }); + + it('should handle multiple events in sequence', async () => { + const events: LangchainStreamEvent[] = [ + createLangchainEvent({ + event: 'on_chat_model_stream', + name: 'chat_model', + data: { + chunk: new AIMessageChunk({ content: 'Hello', id: 'test-run-id' }), + }, + }), + createLangchainEvent({ + event: 'on_tool_end', + name: 'tool', + data: { + output: { + tool_call_id: 'tool-1', + content: 'Tool result', + }, + }, + metadata: { langgraph_node: 'tools' }, + }), + ]; + + const results = await lastValueFrom( + of(...events) + .pipe(langchainToChatEvents({ runName })) + .pipe(toArray()) + ); + + expect(results).toEqual([ + { + type: 'message_chunk', + content_chunk: 'Hello', + message_id: 'test-run-id', + }, + { + type: 'tool_result', + toolResult: { + callId: 'tool-1', + result: 'Tool result', + }, + }, + ]); + }); + + it('should return empty array for unhandled events', async () => { + const event = createLangchainEvent({ + event: 'on_chain_start', + name: 'some_chain', + data: {}, + }); + + const result = await lastValueFrom( + of(event).pipe(langchainToChatEvents({ runName })).pipe(toArray()) + ); + + expect(result).toEqual([]); + }); +}); diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.ts new file mode 100644 index 000000000000..eb7836d8776e --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/convert_langchain_events.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mergeMap, OperatorFunction, of } from 'rxjs'; +import type { AIMessageChunk } from '@langchain/core/messages'; +import { StreamEvent as LangchainStreamEvent } from '@langchain/core/tracers/log_stream'; +import type { ToolResultEvent } from '../../../../common/chat_events'; +import { AgentRunEvents } from '../types'; +import { extractTextContent, messageFromLangchain } from './from_langchain_messages'; + +function filterMap(project: (value: T) => R[]): OperatorFunction { + return mergeMap((value: T) => { + const result = project(value); + return of(...result); + }); +} + +export const langchainToChatEvents = ({ + runName, +}: { + runName: string; +}): OperatorFunction => { + return (langchain$) => { + return langchain$.pipe( + filterMap((event) => { + // stream text chunks for the UI + if (event.event === 'on_chat_model_stream') { + const chunk: AIMessageChunk = event.data.chunk; + const content = extractTextContent(chunk); + if (content) { + return [ + { + type: 'message_chunk', + content_chunk: content, + message_id: chunk.id ?? event.run_id, + }, + ]; + } + } + + // emit full message on each agent step + if (event.event === 'on_chain_end' && event.name === 'agent') { + const addedMessages = event.data.output.addedMessages; + const lastMessage = addedMessages[addedMessages.length - 1]; + return [{ type: 'message', message: messageFromLangchain(lastMessage) }]; + } + + // emit tool result events + if (event.event === 'on_tool_end' && event.metadata?.langgraph_node === 'tools') { + return [ + { + type: 'tool_result', + toolResult: { + callId: event.data.output.tool_call_id, + result: event.data.output.content, + }, + } as ToolResultEvent, + ]; + } + + return []; + }) + ); + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.test.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.test.ts new file mode 100644 index 000000000000..c43662952578 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HumanMessage, AIMessage, ToolMessage } from '@langchain/core/messages'; +import { + createUserMessage, + createAssistantMessage, + createToolResult, + ConversationEventType, +} from '../../../../common/conversation_events'; +import { conversationEventsToMessages } from './events_to_messages'; + +describe('conversationEventsToMessages', () => { + it('should convert user message to HumanMessage', () => { + const userMessage = createUserMessage({ + content: 'Hello, how are you?', + }); + + const result = conversationEventsToMessages([userMessage]); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(HumanMessage); + expect(result[0].content).toBe('Hello, how are you?'); + }); + + it('should convert assistant message to AIMessage', () => { + const assistantMessage = createAssistantMessage({ + content: 'I am doing well, thank you!', + toolCalls: [ + { + toolCallId: 'call-1', + toolName: 'search', + args: { query: 'test' }, + }, + ], + }); + + const result = conversationEventsToMessages([assistantMessage]); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(AIMessage); + expect(result[0].content).toBe('I am doing well, thank you!'); + const aiMessage = result[0] as AIMessage & { + tool_calls: Array<{ id: string; name: string; args: any; type: string }>; + }; + expect(aiMessage.tool_calls).toHaveLength(1); + expect(aiMessage.tool_calls[0]).toEqual({ + id: 'call-1', + name: 'search', + args: { query: 'test' }, + type: 'tool_call', + }); + }); + + it('should convert tool result to ToolMessage', () => { + const toolResult = createToolResult({ + toolCallId: 'call-1', + toolResult: 'Search results: ...', + }); + + const result = conversationEventsToMessages([toolResult]); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(ToolMessage); + expect(result[0].content).toBe('Search results: ...'); + const toolMessage = result[0] as ToolMessage; + expect(toolMessage.tool_call_id).toBe('call-1'); + }); + + it('should handle multiple events in sequence', () => { + const events = [ + createUserMessage({ + content: 'Hello', + }), + createAssistantMessage({ + content: 'Hi there!', + toolCalls: [], + }), + createToolResult({ + toolCallId: 'call-1', + toolResult: 'Result', + }), + ]; + + const result = conversationEventsToMessages(events); + expect(result).toHaveLength(3); + expect(result[0]).toBeInstanceOf(HumanMessage); + expect(result[1]).toBeInstanceOf(AIMessage); + expect(result[2]).toBeInstanceOf(ToolMessage); + }); + + it('should handle unknown event types by returning empty array', () => { + const unknownEvent = { + type: 'unknown_type' as ConversationEventType, + id: 'test-id', + createdAt: new Date().toISOString(), + content: 'test', + }; + + const result = conversationEventsToMessages([unknownEvent as any]); + expect(result).toHaveLength(0); + }); +}); diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.ts new file mode 100644 index 000000000000..1923f54cace3 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/events_to_messages.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BaseMessage, AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages'; +import { + type ConversationEvent, + type UserMessage, + type AssistantMessage, + type ToolResult, + isUserMessage, + isAssistantMessage, + isToolResult, +} from '../../../../common/conversation_events'; + +export const conversationEventsToMessages = (events: ConversationEvent[]): BaseMessage[] => { + return events + .map((event) => { + if (isUserMessage(event)) { + return [userMessageToLangchain(event)]; + } + if (isAssistantMessage(event)) { + return [assistantMessageToLangchain(event)]; + } + if (isToolResult(event)) { + return [toolResultToLangchain(event)]; + } else { + // not handling other types for now. + return []; + } + }) + .flat(); +}; + +export const userMessageToLangchain = (message: UserMessage): BaseMessage => { + return new HumanMessage({ content: message.content }); +}; + +export const assistantMessageToLangchain = (message: AssistantMessage): BaseMessage => { + return new AIMessage({ + content: message.content, + tool_calls: message.toolCalls.map((toolCall) => { + return { + id: toolCall.toolCallId, + name: toolCall.toolName, + args: toolCall.args, + type: 'tool_call', + }; + }), + }); +}; + +export const toolResultToLangchain = (message: ToolResult): BaseMessage => { + return new ToolMessage({ tool_call_id: message.toolCallId, content: message.toolResult }); +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.test.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.test.ts new file mode 100644 index 000000000000..b16819975cd3 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AIMessage, HumanMessage, BaseMessage, MessageType } from '@langchain/core/messages'; +import { messageFromLangchain } from './from_langchain_messages'; +import { ConversationEventType } from '../../../../common/conversation_events'; + +describe('messageFromLangchain', () => { + it('should convert an AI message with string content', () => { + const message = new AIMessage({ + content: 'Hello, I am an AI', + id: 'ai-1', + }); + + const result = messageFromLangchain(message); + + expect(result).toMatchObject({ + type: ConversationEventType.assistantMessage, + id: 'ai-1', + content: 'Hello, I am an AI', + toolCalls: [], + }); + expect(result.createdAt).toBeDefined(); + }); + + it('should convert an AI message with complex text content', () => { + const message = new AIMessage({ + content: [ + { type: 'text', text: 'Hello, ' }, + { type: 'text', text: 'I am an AI' }, + ], + id: 'ai-2', + }); + + const result = messageFromLangchain(message); + + expect(result).toMatchObject({ + type: ConversationEventType.assistantMessage, + id: 'ai-2', + content: 'Hello, I am an AI', + toolCalls: [], + }); + expect(result.createdAt).toBeDefined(); + }); + + it('should handle mixed content types in AI message', () => { + const message = new AIMessage({ + content: [ + { type: 'text', text: 'Hello, ' }, + { type: 'image', image_url: 'https://example.com/image.jpg' }, + { type: 'text', text: 'I am an AI' }, + { type: 'function', function: { name: 'test', arguments: '{}' } }, + ], + id: 'ai-2', + }); + + const result = messageFromLangchain(message); + + expect(result).toMatchObject({ + type: ConversationEventType.assistantMessage, + id: 'ai-2', + content: 'Hello, I am an AI', + toolCalls: [], + }); + expect(result.createdAt).toBeDefined(); + }); + + it('should convert an AI message with tool calls', () => { + const message = new AIMessage({ + content: 'I will help you with that', + id: 'ai-3', + tool_calls: [ + { + id: 'tool-1', + name: 'search', + args: { query: 'test' }, + }, + ], + }); + + const result = messageFromLangchain(message); + + expect(result).toMatchObject({ + type: ConversationEventType.assistantMessage, + id: 'ai-3', + content: 'I will help you with that', + toolCalls: [ + { + toolCallId: 'tool-1', + toolName: 'search', + args: { query: 'test' }, + }, + ], + }); + }); + + it('should convert a human message', () => { + const message = new HumanMessage({ + content: 'Hello, can you help me?', + id: 'human-1', + }); + + const result = messageFromLangchain(message); + + expect(result).toMatchObject({ + type: ConversationEventType.userMessage, + id: 'human-1', + content: 'Hello, can you help me?', + }); + }); + + it('should throw an error for unsupported message types', () => { + // Create a class that extends BaseMessage but doesn't implement the required methods + class UnsupportedMessage extends BaseMessage { + constructor() { + super({ content: 'test' }); + } + _getType(): MessageType { + throw new Error('Unsupported message type'); + } + } + + const unsupportedMessage = new UnsupportedMessage(); + + expect(() => messageFromLangchain(unsupportedMessage)).toThrow('Unsupported message type'); + }); +}); diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.ts new file mode 100644 index 000000000000..2f310f6e98b1 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/from_langchain_messages.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + BaseMessage, + MessageContentComplex, + isAIMessage, + isHumanMessage, +} from '@langchain/core/messages'; +import type { ToolCall as LangchainToolCall } from '@langchain/core/messages/tool'; +import { + createUserMessage, + createAssistantMessage, + type ToolCall, +} from '../../../../common/conversation_events'; + +export const messageFromLangchain = (message: BaseMessage) => { + if (isAIMessage(message)) { + const toolCalls = message.tool_calls?.map(convertLangchainToolCall) ?? []; + return createAssistantMessage({ + id: message.id, + content: extractTextContent(message), + toolCalls, + }); + } + if (isHumanMessage(message)) { + return createUserMessage({ id: message.id, content: extractTextContent(message) }); + } + + // tools will come later + throw new Error(`Unsupported message type ${message}`); +}; + +const convertLangchainToolCall = (toolCall: LangchainToolCall): ToolCall => { + return { + toolCallId: toolCall.id!, // TODO: figure out a default, e.g {messageId}_{callIndex} + toolName: toolCall.name, + args: toolCall.args, + }; +}; + +export const extractTextContent = (message: BaseMessage): string => { + if (typeof message.content === 'string') { + return message.content; + } else { + let content = ''; + for (const item of message.content as MessageContentComplex[]) { + if (item.type === 'text') { + content += item.text; + } + } + return content; + } +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/index.ts new file mode 100644 index 000000000000..a734158f96d0 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/orchestration/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { langchainToChatEvents } from './convert_langchain_events'; +export { conversationEventsToMessages } from './events_to_messages'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/services/types.ts b/x-pack/solutions/chat/plugins/workchat-app/server/services/types.ts new file mode 100644 index 000000000000..68d872b16466 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/services/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IntegrationsService } from './integrations/integrations_service'; +import type { AgentFactory } from './orchestration'; +import type { ConversationService } from './conversations'; +import type { ChatService } from './chat'; +import type { AgentService } from './agents'; + +export interface InternalServices { + agentFactory: AgentFactory; + agentService: AgentService; + chatService: ChatService; + conversationService: ConversationService; + integrationsService: IntegrationsService; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/types.ts b/x-pack/solutions/chat/plugins/workchat-app/server/types.ts new file mode 100644 index 000000000000..af9629aa1110 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InferenceServerStart } from '@kbn/inference-plugin/server'; +import type { WorkchatIntegrationDefinition } from '@kbn/wci-server'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; +import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; + +export interface WorkChatAppPluginSetup { + integrations: { + register: (integration: WorkchatIntegrationDefinition) => void; + }; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WorkChatAppPluginStart {} + +export interface WorkChatAppPluginSetupDependencies { + features: FeaturesPluginSetup; +} + +export interface WorkChatAppPluginStartDependencies { + inference: InferenceServerStart; + actions: ActionsPluginStart; +} diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/utils/index.ts b/x-pack/solutions/chat/plugins/workchat-app/server/utils/index.ts new file mode 100644 index 000000000000..3e28015436b5 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { createBuilder } from './so_filters'; diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.test.ts b/x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.test.ts new file mode 100644 index 000000000000..395e986905c3 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createBuilder } from './so_filters'; + +describe('createBuilder', () => { + const soType = 'testType'; + + it('generates expected value for top level `equals`', () => { + const builder = createBuilder(soType); + const filter = builder.equals('foo', 'bar').toKQL(); + expect(filter).toEqual('testType.attributes.foo: bar'); + }); + + it('generates expected value for top level `and`', () => { + const builder = createBuilder(soType); + const filter = builder + .and(builder.equals('foo', 'bar'), builder.equals('hello', 'dolly')) + .toKQL(); + expect(filter).toEqual('(testType.attributes.foo: bar) AND (testType.attributes.hello: dolly)'); + }); + + it('generates expected value for top level `or`', () => { + const builder = createBuilder(soType); + const filter = builder + .or(builder.equals('foo', 'bar'), builder.equals('hello', 'dolly')) + .toKQL(); + expect(filter).toEqual('(testType.attributes.foo: bar) OR (testType.attributes.hello: dolly)'); + }); + + it('generates expected value for nested `and`/`or`', () => { + const builder = createBuilder(soType); + const filter = builder + .and( + builder.equals('hello', 'dolly'), + builder.or(builder.equals('foo', 'bar'), builder.equals('fuz', 'baz')) + ) + .toKQL(); + expect(filter).toEqual( + '(testType.attributes.hello: dolly) AND ((testType.attributes.foo: bar) OR (testType.attributes.fuz: baz))' + ); + }); +}); diff --git a/x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.ts b/x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.ts new file mode 100644 index 000000000000..8ffa5ab55ee3 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/server/utils/so_filters.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface KqlFilterBase { + toKQL(): string; +} + +type FilterValue = string | number | boolean; + +interface FilterBuilder { + or(...clauses: KqlFilterBase[]): KqlFilterBase; + and(...clauses: KqlFilterBase[]): KqlFilterBase; + equals(field: string, value: FilterValue): KqlFilterBase; +} + +/** + * Create a savedObject filter builder for given SO type. + */ +export const createBuilder = (soType: string): FilterBuilder => { + const fieldPath = (fieldName: string) => `${soType}.attributes.${fieldName}`; + const fieldValue = (value: FilterValue) => `${value}`; + + const or = (...clauses: KqlFilterBase[]): KqlFilterBase => { + return { + toKQL: () => clauses.map((clause) => '(' + clause.toKQL() + ')').join(' OR '), + }; + }; + + const and = (...clauses: KqlFilterBase[]): KqlFilterBase => { + return { + toKQL: () => clauses.map((clause) => '(' + clause.toKQL() + ')').join(' AND '), + }; + }; + + const equals = (name: string, value: FilterValue): KqlFilterBase => { + return { + toKQL: () => `${fieldPath(name)}: ${fieldValue(value)}`, + }; + }; + + return { + or, + and, + equals, + }; +}; diff --git a/x-pack/solutions/chat/plugins/workchat-app/tsconfig.json b/x-pack/solutions/chat/plugins/workchat-app/tsconfig.json new file mode 100644 index 000000000000..ebc8b651f732 --- /dev/null +++ b/x-pack/solutions/chat/plugins/workchat-app/tsconfig.json @@ -0,0 +1,46 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../typings/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/config-schema", + "@kbn/inference-plugin", + "@kbn/kibana-react-plugin", + "@kbn/i18n-react", + "@kbn/react-kibana-context-render", + "@kbn/shared-ux-router", + "@kbn/shared-ux-page-kibana-template", + "@kbn/sse-utils-client", + "@kbn/sse-utils", + "@kbn/sse-utils-server", + "@kbn/inference-langchain", + "@kbn/inference-common", + "@kbn/wci-common", + "@kbn/i18n", + "@kbn/user-profile-components", + "@kbn/ai-assistant-icon", + "@kbn/react-hooks", + "@kbn/shared-ux-link-redirect-app", + "@kbn/datemath", + "@kbn/zod", + "@kbn/logging-mocks", + "@kbn/wci-server", + "@kbn/actions-plugin", + "@kbn/wci-browser", + "@kbn/features-plugin", + "@kbn/wc-genai-utils", + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/x-pack/test/common/services/spaces.ts b/x-pack/test/common/services/spaces.ts index 298f2626048a..ba510466178d 100644 --- a/x-pack/test/common/services/spaces.ts +++ b/x-pack/test/common/services/spaces.ts @@ -22,7 +22,7 @@ interface SpaceCreate { description?: string; color?: string; initials?: string; - solution?: 'es' | 'oblt' | 'security' | 'classic'; + solution?: 'es' | 'oblt' | 'security' | 'chat' | 'classic'; disabledFeatures?: string[]; } diff --git a/yarn.lock b/yarn.lock index 3f4829b3ca12..7551b766f7d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5170,6 +5170,10 @@ version "0.0.0" uid "" +"@kbn/deeplinks-chat@link:src/platform/packages/shared/deeplinks/chat": + version "0.0.0" + uid "" + "@kbn/deeplinks-devtools@link:src/platform/packages/shared/deeplinks/devtools": version "0.0.0" uid "" @@ -7958,6 +7962,42 @@ version "0.0.0" uid "" +"@kbn/wc-genai-utils@link:x-pack/solutions/chat/packages/wc-genai-utils": + version "0.0.0" + uid "" + +"@kbn/wc-index-schema-builder@link:x-pack/solutions/chat/packages/wc-index-schema-builder": + version "0.0.0" + uid "" + +"@kbn/wc-integration-utils@link:x-pack/solutions/chat/packages/wc-integration-utils": + version "0.0.0" + uid "" + +"@kbn/wci-browser@link:x-pack/solutions/chat/packages/wci-browser": + version "0.0.0" + uid "" + +"@kbn/wci-common@link:x-pack/solutions/chat/packages/wci-common": + version "0.0.0" + uid "" + +"@kbn/wci-external-server@link:x-pack/solutions/chat/plugins/wci-external-server": + version "0.0.0" + uid "" + +"@kbn/wci-index-source@link:x-pack/solutions/chat/plugins/wci-index-source": + version "0.0.0" + uid "" + +"@kbn/wci-salesforce@link:x-pack/solutions/chat/plugins/wci-salesforce": + version "0.0.0" + uid "" + +"@kbn/wci-server@link:x-pack/solutions/chat/packages/wci-server": + version "0.0.0" + uid "" + "@kbn/web-worker-stub@link:packages/kbn-web-worker-stub": version "0.0.0" uid "" @@ -7966,6 +8006,10 @@ version "0.0.0" uid "" +"@kbn/workchat-app@link:x-pack/solutions/chat/plugins/workchat-app": + version "0.0.0" + uid "" + "@kbn/xstate-utils@link:src/platform/packages/shared/kbn-xstate-utils": version "0.0.0" uid "" @@ -8444,6 +8488,21 @@ dependencies: "@types/mdx" "^2.0.0" +"@modelcontextprotocol/sdk@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.6.0.tgz#1d1849c9b36c0e494cf77398579dbd7d46c1ed34" + integrity sha512-585s8g+jzuGBomzgzDeP5l8gEyiSs+KhoAHbA2ZZ24Zgm83IZsyCLl/fmWhPHbfYsuLG8NE6SWGZA5ZBql8jSw== + dependencies: + content-type "^1.0.5" + cors "^2.8.5" + eventsource "^3.0.2" + express "^5.0.1" + express-rate-limit "^7.5.0" + pkce-challenge "^4.1.0" + raw-body "^3.0.0" + zod "^3.23.8" + zod-to-json-schema "^3.24.1" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -8502,6 +8561,11 @@ outvariant "^1.4.3" strict-event-emitter "^0.5.1" +"@n8n/json-schema-to-zod@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@n8n/json-schema-to-zod/-/json-schema-to-zod-1.1.0.tgz#d59ecf5f0c90c560b8b09374ea3efafc2dd5b84e" + integrity sha512-HfL3jq5NSb0Mng56QCfUnzD09F4wxWR543jYJZ+XBQpb0f9B102r6K3qEtdXVTs1mwHVnxz5KBaesqipFYPjww== + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" @@ -12746,6 +12810,14 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -13923,6 +13995,21 @@ body-parser@1.20.3: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.1.0.tgz#2fd84396259e00fa75648835e2d95703bce8e890" + integrity sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.5.2" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" + bonjour-service@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" @@ -14209,7 +14296,7 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== @@ -15284,12 +15371,19 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== + dependencies: + safe-buffer "5.2.1" + content-security-policy-parser@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/content-security-policy-parser/-/content-security-policy-parser-0.6.0.tgz#b361d8587dee0e92def19d308cb23e8d32cc26f6" integrity sha512-wejtC/p+HLNQ7uaWgg1o3CKHhE8QXC9fJ2GCY0X82L5HUNtZSq1dmUvNSHHEb6R7LS02fpmRBq/vP8i4/+9KCg== -content-type@~1.0.4, content-type@~1.0.5: +content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -15311,6 +15405,11 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + cookie@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" @@ -15372,6 +15471,14 @@ core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^7.0.0, cosmiconfig@^7.0.1, cosmiconfig@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -15519,7 +15626,7 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^4.2.0: +crypto-js@^4.1.1, crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== @@ -16257,6 +16364,13 @@ debug@4.3.4: dependencies: ms "2.1.2" +debug@4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -16613,7 +16727,7 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -destroy@1.2.0: +destroy@1.2.0, destroy@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== @@ -17169,16 +17283,16 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== +encodeurl@^2.0.0, encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -17937,7 +18051,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= -etag@~1.8.1: +etag@^1.8.1, etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= @@ -17990,6 +18104,13 @@ eventsource-parser@^3.0.0: resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.0.tgz#9303e303ef807d279ee210a17ce80f16300d9f57" integrity sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA== +eventsource@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.5.tgz#0cae1eee2d2c75894de8b02a91d84e5c57f0cc5a" + integrity sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw== + dependencies: + eventsource-parser "^3.0.0" + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -18099,6 +18220,11 @@ expr-eval@^2.0.2: resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201" integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg== +express-rate-limit@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.0.tgz#6a67990a724b4fbbc69119419feef50c51e8b28f" + integrity sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg== + express@^4.17.3, express@^4.18.2, express@^4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -18136,6 +18262,44 @@ express@^4.17.3, express@^4.18.2, express@^4.21.2: utils-merge "1.0.1" vary "~1.1.2" +express@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.0.1.tgz#5d359a2550655be33124ecbc7400cd38436457e9" + integrity sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ== + dependencies: + accepts "^2.0.0" + body-parser "^2.0.1" + content-disposition "^1.0.0" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "^1.2.1" + debug "4.3.6" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "^2.0.0" + fresh "2.0.0" + http-errors "2.0.0" + merge-descriptors "^2.0.0" + methods "~1.1.2" + mime-types "^3.0.0" + on-finished "2.4.1" + once "1.4.0" + parseurl "~1.3.3" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + router "^2.0.0" + safe-buffer "5.2.1" + send "^1.1.0" + serve-static "^2.1.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "^2.0.0" + utils-merge "1.0.1" + vary "~1.1.2" + ext@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -18496,6 +18660,19 @@ finalhandler@1.3.1: statuses "2.0.1" unpipe "~1.0.0" +finalhandler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.0.0.tgz#9d3c79156dfa798069db7de7dd53bc37546f564b" + integrity sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -18808,11 +18985,16 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -fresh@0.5.2: +fresh@0.5.2, fresh@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fresh@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + fromentries@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.2.0.tgz#e6aa06f240d6267f913cea422075ef88b63e7897" @@ -19010,7 +19192,7 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4, get-intrinsic@^1.2.6: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -20023,7 +20205,7 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -20217,6 +20399,13 @@ iconv-lite@0.6, iconv-lite@0.6.3, iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +iconv-lite@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.2.tgz#af6d628dccfb463b7364d97f715e4b74b8c8c2b8" + integrity sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag== + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" @@ -20907,6 +21096,11 @@ is-promise@^2.1.0, is-promise@^2.2.2: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.4, is-regex@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" @@ -23154,6 +23348,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + memfs@^3.4.1, memfs@^3.4.12: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" @@ -23232,6 +23431,11 @@ merge-descriptors@1.0.3: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -23355,18 +23559,25 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -"mime-db@>= 1.40.0 < 2", mime-db@^1.52.0: +"mime-db@>= 1.40.0 < 2", mime-db@^1.52.0, mime-db@^1.53.0: version "1.53.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +mime-types@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.0.tgz#148453a900475522d095a445355c074cca4f5217" + integrity sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w== + dependencies: + mime-db "^1.53.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -23952,6 +24163,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + neo-async@^2.5.0, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -24365,7 +24581,7 @@ oas@^25.3.0: path-to-regexp "^8.1.0" remove-undefined-objects "^5.0.0" -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -24401,10 +24617,10 @@ object-identity-map@^1.0.2: dependencies: object.entries "^1.1.0" -object-inspect@^1.13.1, object-inspect@^1.13.2, object-inspect@^1.7.0: - version "1.13.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" - integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== +object-inspect@^1.13.1, object-inspect@^1.13.2, object-inspect@^1.13.3, object-inspect@^1.7.0: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2, object-is@^1.1.5, object-is@^1.1.6: version "1.1.6" @@ -24534,7 +24750,7 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -25046,7 +25262,7 @@ parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2: dependencies: entities "^4.4.0" -parseurl@~1.3.2, parseurl@~1.3.3: +parseurl@^1.3.3, parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -25139,7 +25355,7 @@ path-to-regexp@^6.3.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== -path-to-regexp@^8.1.0: +path-to-regexp@^8.0.0, path-to-regexp@^8.1.0: version "8.2.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== @@ -25317,6 +25533,13 @@ pixelmatch@5.3.0, pixelmatch@^5.3.0: dependencies: pngjs "^6.0.0" +pkce-challenge@3.1.0, pkce-challenge@^4.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-3.1.0.tgz#c974ee934e62c501f09da817964e75db201ee8bf" + integrity sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg== + dependencies: + crypto-js "^4.1.1" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -26131,13 +26354,20 @@ pure-rand@^7.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-7.0.1.tgz#6f53a5a9e3e4a47445822af96821ca509ed37566" integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== -qs@6.13.0, qs@^6.11.0, qs@^6.7.0: +qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: side-channel "^1.0.6" +qs@^6.11.0, qs@^6.14.0, qs@^6.7.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + query-string@^6.13.2: version "6.13.2" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.2.tgz#3585aa9412c957cbd358fd5eaca7466f05586dda" @@ -26257,6 +26487,16 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + rbush@^3.0.0, rbush@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" @@ -27551,6 +27791,15 @@ robust-predicates@^3.0.0: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a" integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g== +router@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.1.0.tgz#f256ca2365afb4d386ba4f7a9ee0aa0827c962fa" + integrity sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA== + dependencies: + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + rrweb-cssom@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" @@ -28013,6 +28262,24 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" +send@^1.0.0, send@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/send/-/send-1.1.0.tgz#4efe6ff3bb2139b0e5b2648d8b18d4dec48fc9c5" + integrity sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA== + dependencies: + debug "^4.3.5" + destroy "^1.2.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^0.5.2" + http-errors "^2.0.0" + mime-types "^2.1.35" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" + serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -28043,6 +28310,16 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" +serve-static@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.1.0.tgz#1b4eacbe93006b79054faa4d6d0a501d7f0e84e2" + integrity sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -28234,15 +28511,45 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4, side-channel@^1.0.6, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" @@ -30263,6 +30570,15 @@ type-fest@^4.15.0, type-fest@^4.17.0, type-fest@^4.26.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg== +type-is@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.0.tgz#7d249c2e2af716665cc149575dadb8b3858653af" + integrity sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -30921,7 +31237,7 @@ varint@^6.0.0: resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= @@ -32310,10 +32626,10 @@ zip-stream@^6.0.1: compress-commons "^6.0.2" readable-stream "^4.0.0" -zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.23.0, zod-to-json-schema@^3.23.5: - version "3.24.1" - resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz#f08c6725091aadabffa820ba8d50c7ab527f227a" - integrity sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w== +zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.23.0, zod-to-json-schema@^3.23.5, zod-to-json-schema@^3.24.1: + version "3.24.3" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz#5958ba111d681f8d01c5b6b647425c9b8a6059e7" + integrity sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A== zod@3.24.1, zod@^3.24.1: version "3.24.1"