From 4e59d88af4daf1f357d264fe3f84db59fec111c9 Mon Sep 17 00:00:00 2001 From: Massimiliano Angelino Date: Mon, 15 Jan 2024 18:00:13 +0100 Subject: [PATCH] Feature: replace API Gateway with AppSync to enable deployment in regulated environments (#269) * feat: appsync * feat(appsync): API * feat(appsync): api * feat(appsync): poetry env * feat(appsync): ongoing checkpoint * feat(appsync): checkpoint * feat(appsync): final with removal of ApiGw * feat(appsync): working GraphQL * feat(appsync): CF configuration * feat(appsync): removal of CF in front of AppSync * feat(appsync): using AOI for CF * feat(appsync): fix Delete session removed unused API resources * feat(appsync) : delete session result * feat(appsync): moving handler function correctly replacing api_handler function for better code diff * feat(appsync): json decoder * feat(appsync): cleanup * feat(appsync): exclude autogenerated file from prettier linting * feat(appsync): remove unused imports * feat(appsync): remove unused imports * feat(appsync): fix appsync merge errors * feat(appsync): fixed semantic_search method based on graphql signature * fix(appsync): correct logic for adding form fields to POST * fix(appsync): merged api stability and cleanup * feat(appsync): schema improvements * fix(appsync): reverting changes * feat(appsync): single api * fix(appsync): remove double resolver creation * fix(appsync): start kendra data sync * fix(appsync): correct field type * doc(appsync): update READMEs and diagrams * fix(appsync): correct subnet filtering * feat(appsync): cleanup legacy apigw functionality --- .github/workflows/build.yaml | 12 + .gitignore | 2 + .prettierignore | 6 +- README.md | 132 +- assets/architecture.png | Bin 99013 -> 111254 bytes docs/appsync/appsync.md | 22 + docs/document-retrieval/retriever.md | 32 + docs/sagemker-hosting/inference-script.md | 32 + lib/authentication/index.ts | 4 + lib/aws-genai-llm-chatbot-stack.ts | 3 +- lib/chatbot-api/appsync-ws.ts | 104 + .../functions/api-handler/index.py | 69 +- .../api-handler/routes/cross_encoders.py | 19 +- .../functions/api-handler/routes/documents.py | 351 +- .../api-handler/routes/embeddings.py | 24 +- .../functions/api-handler/routes/health.py | 6 +- .../functions/api-handler/routes/kendra.py | 25 +- .../functions/api-handler/routes/models.py | 6 +- .../functions/api-handler/routes/rag.py | 6 +- .../api-handler/routes/semantic_search.py | 16 +- .../functions/api-handler/routes/sessions.py | 81 +- .../api-handler/routes/workspaces.py | 146 +- lib/chatbot-api/functions/authorizer/index.py | 48 - .../functions/connection-handler/index.py | 67 - .../incoming-message-handler/index.py | 71 - .../outgoing-message-appsync/graphql.ts | 42 + .../outgoing-message-appsync/index.ts | 58 + .../outgoing-message-handler/index.py | 55 - .../functions/resolvers/lambda-resolver.js | 22 + .../resolvers/publish-response-resolver.js | 15 + .../send-query-lambda-resolver/index.py | 35 + .../functions/resolvers/subscribe-resolver.js | 13 + lib/chatbot-api/index.ts | 85 +- lib/chatbot-api/rest-api.ts | 489 +- lib/chatbot-api/schema/schema.graphql | 351 ++ lib/chatbot-api/websocket-api.ts | 193 +- .../functions/request-handler/index.py | 4 - .../functions/request-handler/index.py | 15 +- lib/rag-engines/opensearch-vector/index.ts | 5 +- lib/shared/layers/python-sdk/python/README.md | 0 .../python-sdk/python/genai_core/auth.py | 7 +- .../python-sdk/python/genai_core/sessions.py | 4 +- .../python-sdk/python/genai_core/upload.py | 2 +- .../python/genai_core/utils/json.py | 5 +- .../python/genai_core/workspaces.py | 12 +- .../layers/python-sdk/python/pyproject.toml | 14 + lib/user-interface/index.ts | 77 +- lib/user-interface/react-app/README.md | 8 +- lib/user-interface/react-app/schema.json | 4568 +++++++++++++++++ lib/user-interface/react-app/src/API.ts | 977 ++++ lib/user-interface/react-app/src/app.tsx | 7 +- .../src/common/api-client/api-client-base.ts | 35 - .../src/common/api-client/api-client.ts | 20 +- .../api-client/cross-encoders-client.ts | 52 +- .../src/common/api-client/documents-client.ts | 359 +- .../common/api-client/embeddings-client.ts | 54 +- .../src/common/api-client/health-client.ts | 24 +- .../src/common/api-client/kendra-client.ts | 87 +- .../src/common/api-client/models-client.ts | 23 +- .../common/api-client/rag-engines-client.ts | 26 +- .../api-client/semantic-search-client.ts | 26 +- .../src/common/api-client/sessions-client.ts | 93 +- .../common/api-client/workspaces-client.ts | 156 +- .../react-app/src/common/constants.ts | 12 +- .../react-app/src/common/file-uploader.ts | 13 +- .../common/helpers/embeddings-model-helper.ts | 6 +- .../react-app/src/common/types.ts | 158 - .../react-app/src/common/utils.ts | 10 +- .../components/chatbot/chat-input-panel.tsx | 284 +- .../react-app/src/components/chatbot/chat.tsx | 31 +- .../src/components/chatbot/multi-chat.tsx | 251 +- .../src/components/chatbot/sessions.tsx | 17 +- .../react-app/src/components/chatbot/types.ts | 17 +- .../react-app/src/components/chatbot/utils.ts | 111 +- .../components/rag/workspace-delete-modal.tsx | 4 +- .../react-app/src/graphql/mutations.ts | 247 + .../react-app/src/graphql/queries.ts | 402 ++ .../react-app/src/graphql/subscriptions.ts | 22 + .../chatbot/models/column-definitions.tsx | 81 +- .../src/pages/chatbot/models/models.tsx | 15 +- .../src/pages/rag/add-data/add-data.tsx | 19 +- .../src/pages/rag/add-data/add-qna.tsx | 21 +- .../rag/add-data/add-rss-subscription.tsx | 25 +- .../src/pages/rag/add-data/add-text.tsx | 21 +- .../src/pages/rag/add-data/crawl-website.tsx | 25 +- .../pages/rag/add-data/data-file-upload.tsx | 35 +- .../create-workspace-aurora.tsx | 45 +- .../create-workspace-kendra.tsx | 20 +- .../create-workspace-opensearch.tsx | 39 +- .../rag/create-workspace/create-workspace.tsx | 14 +- .../cross-encoder-selector-field.tsx | 20 +- .../embeddings-selector-field.tsx | 23 +- .../rag/create-workspace/kendra-form.tsx | 41 +- .../rag/cross-encoders/cross-encoders.tsx | 37 +- .../rag/dashboard/column-definitions.tsx | 16 +- .../src/pages/rag/dashboard/dashboard.tsx | 23 +- .../pages/rag/dashboard/workspaces-table.tsx | 8 +- .../src/pages/rag/embeddings/embeddings.tsx | 44 +- .../src/pages/rag/engines/engines.tsx | 18 +- .../rag/semantic-search/result-items.tsx | 15 +- .../semantic-search-details.tsx | 2 +- .../rag/semantic-search/semantic-search.tsx | 63 +- .../workspace/aurora-workspace-settings.tsx | 12 +- .../src/pages/rag/workspace/columns.tsx | 69 +- .../src/pages/rag/workspace/documents-tab.tsx | 39 +- .../workspace/kendra-workspace-settings.tsx | 38 +- .../open-search-workspace-settings.tsx | 12 +- .../src/pages/rag/workspace/rss-feed.tsx | 162 +- .../src/pages/rag/workspace/workspace.tsx | 22 +- .../rag/workspaces/column-definitions.tsx | 18 +- .../rag/workspaces/workspaces-page-header.tsx | 27 +- .../pages/rag/workspaces/workspaces-table.tsx | 14 +- package-lock.json | 10 + package.json | 1 + tsconfig.json | 6 +- 115 files changed, 9297 insertions(+), 2690 deletions(-) create mode 100644 .github/workflows/build.yaml create mode 100644 docs/appsync/appsync.md create mode 100644 docs/document-retrieval/retriever.md create mode 100644 docs/sagemker-hosting/inference-script.md create mode 100644 lib/chatbot-api/appsync-ws.ts delete mode 100644 lib/chatbot-api/functions/authorizer/index.py delete mode 100644 lib/chatbot-api/functions/connection-handler/index.py delete mode 100644 lib/chatbot-api/functions/incoming-message-handler/index.py create mode 100644 lib/chatbot-api/functions/outgoing-message-appsync/graphql.ts create mode 100644 lib/chatbot-api/functions/outgoing-message-appsync/index.ts delete mode 100644 lib/chatbot-api/functions/outgoing-message-handler/index.py create mode 100644 lib/chatbot-api/functions/resolvers/lambda-resolver.js create mode 100644 lib/chatbot-api/functions/resolvers/publish-response-resolver.js create mode 100644 lib/chatbot-api/functions/resolvers/send-query-lambda-resolver/index.py create mode 100644 lib/chatbot-api/functions/resolvers/subscribe-resolver.js create mode 100644 lib/chatbot-api/schema/schema.graphql create mode 100644 lib/shared/layers/python-sdk/python/README.md create mode 100644 lib/shared/layers/python-sdk/python/pyproject.toml create mode 100644 lib/user-interface/react-app/schema.json create mode 100644 lib/user-interface/react-app/src/API.ts delete mode 100644 lib/user-interface/react-app/src/common/api-client/api-client-base.ts create mode 100644 lib/user-interface/react-app/src/graphql/mutations.ts create mode 100644 lib/user-interface/react-app/src/graphql/queries.ts create mode 100644 lib/user-interface/react-app/src/graphql/subscriptions.ts diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..2455d584a --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,12 @@ +name: smoke-build +on: push +jobs: + build-cdk: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: "18" + - run: npm install + - run: npm run build diff --git a/.gitignore b/.gitignore index 8b06a8b09..fa75e92da 100644 --- a/.gitignore +++ b/.gitignore @@ -428,6 +428,7 @@ deploy-models cli/*.js bin/*.js lib/**/*.js +!lib/chatbot-api/functions/resolvers/**/*.js !jest.config.js *.d.ts node_modules @@ -445,3 +446,4 @@ bin/config.json # Docs docs/.vitepress/cache docs/.vitepress/dist +lib/user-interface/react-app/.graphqlconfig.yml diff --git a/.prettierignore b/.prettierignore index 867d432fd..7a0155aa5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,4 +7,8 @@ coverage build public out -cdk.out \ No newline at end of file +cdk.out +lib/user-interface/react-app/src/API.ts +lib/user-interface/react-app/src/graphql/mutations.ts +lib/user-interface/react-app/src/graphql/queries.ts +lib/user-interface/react-app/src/graphql/subscriptions.ts diff --git a/README.md b/README.md index a2594594a..609f8d3bf 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # Deploying a Multi-Model and Multi-RAG Powered Chatbot Using AWS CDK on AWS + [![Release Notes](https://img.shields.io/github/v/release/aws-samples/aws-genai-llm-chatbot)](https://github.com/aws-samples/aws-genai-llm-chatbot/releases) [![GitHub star chart](https://img.shields.io/github/stars/aws-samples/aws-genai-llm-chatbot?style=social)](https://star-history.com/#aws-samples/aws-genai-llm-chatbot) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - [![Deploy with GitHub Codespaces](https://github.com/codespaces/badge.svg)](#deploy-with-github-codespaces) ![sample](assets/chabot-sample.gif "AWS GenAI Chatbot") ## Table of content + - [Features](#features) - [Precautions](#precautions) - [Prequirements](#requirements) @@ -24,24 +25,26 @@ - [Credits](#credits) - [License](#license) - # Features + ## Modular, comprehensive and ready to use + This solution provides ready-to-use code so you can start **experimenting with a variety of Large Language Models and Multimodal Language Models, settings and prompts** in your own AWS account. Supported model providers: -- [Amazon Bedrock](https://aws.amazon.com/bedrock/) + +- [Amazon Bedrock](https://aws.amazon.com/bedrock/) - [Amazon SageMaker](https://aws.amazon.com/sagemaker/) self-hosted models from Foundation, Jumpstart and HuggingFace. - Third-party providers via API such as Anthropic, Cohere, AI21 Labs, OpenAI, etc. [See available langchain integrations](https://python.langchain.com/docs/integrations/llms/) for a comprehensive list. - ## Experiment with multimodal models -Deploy [IDEFICS](https://huggingface.co/blog/idefics) models on [Amazon SageMaker](https://aws.amazon.com/sagemaker/) and see how the chatbot can answer questions about images, describe visual content, generate text grounded in multiple images. +Deploy [IDEFICS](https://huggingface.co/blog/idefics) models on [Amazon SageMaker](https://aws.amazon.com/sagemaker/) and see how the chatbot can answer questions about images, describe visual content, generate text grounded in multiple images. ![sample](assets/multimodal-sample.gif "AWS GenAI Chatbot") Currently, the following multimodal models are supported: + - [IDEFICS 9b Instruct](https://huggingface.co/HuggingFaceM4/idefics-9b) - Requires `ml.g5.12xlarge` instance. - [IDEFICS 80b Instruct](https://huggingface.co/HuggingFaceM4/idefics-80b-instruct) @@ -52,45 +55,42 @@ To have the right instance types and how to request them, read [Amazon SageMaker > NOTE: Make sure to review [IDEFICS models license sections](https://huggingface.co/HuggingFaceM4/idefics-80b-instruct#license). To deploy a multimodal model, follow the [deploy instructions](#deploy) -and select one of the supported models (press Space to select/deselect) from the magic-create CLI step and deploy as [instructed in the above section]((#deployment-dependencies-installation)). +and select one of the supported models (press Space to select/deselect) from the magic-create CLI step and deploy as [instructed in the above section](<(#deployment-dependencies-installation)>). -> ⚠️ NOTE ⚠️ Amazon SageMaker are billed by the hour. Be aware of not letting this model run unused to avoid unnecessary costs. +> ⚠️ NOTE ⚠️ Amazon SageMaker are billed by the hour. Be aware of not letting this model run unused to avoid unnecessary costs. ## Multi-Session Chat: evaluate multiple models at once + Send the same query to 2 to 4 separate models at once and see how each one responds based on its own learned history, context and access to the same powerful document retriever, so all requests can pull from the same up-to-date knowledge. ![sample](assets/multichat-sample.gif "AWS GenAI Chatbot") - ## Experiment with multiple RAG options with Workspaces -A workspace is a logical namespace where you can upload files for indexing and storage in one of the vector databases. You can select the embeddings model and text-splitting configuration of your choice. +A workspace is a logical namespace where you can upload files for indexing and storage in one of the vector databases. You can select the embeddings model and text-splitting configuration of your choice. ![sample](assets/create-workspace-sample.gif "AWS GenAI Chatbot") ## Unlock RAG potentials with Workspaces Debugging Tools + The solution comes with several debugging tools to help you debug RAG scenarios: + - Run RAG queries without chatbot and analyse results, scores, etc. - Test different embeddings models directly in the UI - Test cross encoders and analyse distances from different functions between sentences. - ![sample](assets/workspace-debug-sample.gif "AWS GenAI Chatbot") - ## Full-fledged User Interface -The repository includes a CDK construct to deploy a **full-fledged UI** built with [React](https://react.dev/) to interact with the deployed LLMs/MLMs as chatbots. Hosted on [Amazon S3](https://aws.amazon.com/s3/) and distributed with [Amazon CloudFront](https://aws.amazon.com/cloudfront/). +The repository includes a CDK construct to deploy a **full-fledged UI** built with [React](https://react.dev/) to interact with the deployed LLMs/MLMs as chatbots. Hosted on [Amazon S3](https://aws.amazon.com/s3/) and distributed with [Amazon CloudFront](https://aws.amazon.com/cloudfront/). Protected with [Amazon Cognito Authentication](https://aws.amazon.com/cognito/) to help you interact and experiment with multiple LLMs/MLMs, multiple RAG engines, conversational history support and document upload/progress. - -The interface layer between the UI and backend is built with [API Gateway REST API](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html) for management requests and [Amazon API Gateway WebSocket APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html) for chatbot messages and responses. - +The interface layer between the UI and backend is built with [AppSync](https://docs.aws.amazon.com/appsync/latest/devguide/what-is-appsync.html) for management requests and for realtime interaction with chatbot (messages and responses) using GraphQL subscriptions. Design system provided by [AWS Cloudscape Design System](https://cloudscape.design/). - # ⚠️ Precautions ⚠️ Before you begin using the solution, there are certain precautions you must take into account: @@ -101,64 +101,73 @@ Before you begin using the solution, there are certain precautions you must take - **This is a sample**: the code provided in this repository shouldn't be used for production workloads without further reviews and adaptation. - # Amazon SageMaker requirements (for self-hosted models only) + **Instance type quota increase** If you are looking to self-host models on Amazon SageMaker, you'll likely need to request an increase in service quota for specific SageMaker instance types, such as the `ml.g5` instance type. This will give access to the latest generation of GPU/Multi-GPU instance types. [You can do this from the AWS console](https://console.aws.amazon.com/servicequotas/home/services/sagemaker/quotas) # Amazon Bedrock requirements + **Base Models Access** If you are looking to interact with models from Amazon Bedrock, you need to [request access to the base models in one of the regions where Amazon Bedrock is available](https://console.aws.amazon.com/bedrock/home?#/modelaccess). Make sure to read and accept models' end-user license agreements or EULA. Note: + - You can deploy the solution to a different region from where you requested Base Model access. - **While the Base Model access approval is instant, it might take several minutes to get access and see the list of models in the UI.** ![sample](assets/enable-models.gif "AWS GenAI Chatbot") - # Third-party models requirements -You can also interact with external providers via their API, such as AI21 Labs, Cohere, OpenAI, etc. + +You can also interact with external providers via their API, such as AI21 Labs, Cohere, OpenAI, etc. The provider must be supported in the [Model Interface](./lib/model-interfaces/langchain/functions/request-handler/index.py), [see available langchain integrations](https://python.langchain.com/docs/integrations/llms/) for a comprehensive list of providers. -Usually, an `API_KEY` is required to integrate with 3P models. To do so, the [Model Interface](./lib/model-interfaces/langchain/index.ts) deployes a Secrets in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), intially with an empty JSON `{}`, where you can add your API KEYS for one or more providers. +Usually, an `API_KEY` is required to integrate with 3P models. To do so, the [Model Interface](./lib/model-interfaces/langchain/index.ts) deployes a Secrets in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), intially with an empty JSON `{}`, where you can add your API KEYS for one or more providers. These keys will be injected at runtime into the Lambda function Environment Variables; they won't be visible in the AWS Lambda Console. For example, if you wish to be able to interact with AI21 Labs., OpenAI's and Cohere endpoints: + - Open the [Model Interface Keys Secret](./lib/model-interfaces/langchain/index.ts#L38) in Secrets Manager. You can find the secret name in the stack output, too. -- Update the Secrets by adding a key to the JSON +- Update the Secrets by adding a key to the JSON + ```json { "AI21_API_KEY": "xxxxx", "OPENAI_API_KEY": "sk-xxxxxxxxxxxxxxx", - "COHERE_API_KEY": "xxxxx", + "COHERE_API_KEY": "xxxxx" } -``` +``` + N.B: In case of no keys needs, the secret value must be an empty JSON `{}`, NOT an empty string `''`. make sure that the environment variable matches what is expected by the framework in use, like Langchain ([see available langchain integrations](https://python.langchain.com/docs/integrations/llms/). - # Deploy ### Environment setup #### Deploy with AWS Cloud9 -We recommend deploying with [AWS Cloud9](https://aws.amazon.com/cloud9/). + +We recommend deploying with [AWS Cloud9](https://aws.amazon.com/cloud9/). If you'd like to use Cloud9 to deploy the solution, you will need the following before proceeding: + - select at least `m5.large` as Instance type. - use `Ubuntu Server 22.04 LTS` as the platform. #### Deploy with Github Codespaces + If you'd like to use [GitHub Codespaces](https://github.com/features/codespaces) to deploy the solution, you will need the following before proceeding: + 1. An [AWS account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) 2. An [IAM User](https://console.aws.amazon.com/iamv2/home?#/users/create) with: - - `AdministratorAccess` policy granted to your user (for production, we recommend restricting access as needed) - - Take note of `Access key` and `Secret access key`. + +- `AdministratorAccess` policy granted to your user (for production, we recommend restricting access as needed) +- Take note of `Access key` and `Secret access key`. To get started, click on the button below. @@ -180,6 +189,7 @@ Default output format: json You are all set for deployment; you can now jump to [.3 of the deployment section below](#deployment-dependencies-installation). #### Local deployment + If you have decided not to use AWS Cloud9 or GitHub Codespaces, verify that your environment satisfies the following prerequisites: You have: @@ -188,51 +198,62 @@ You have: 2. `AdministratorAccess` policy granted to your AWS account (for production, we recommend restricting access as needed) 3. Both console and programmatic access 4. [NodeJS 16 or 18](https://nodejs.org/en/download/) installed - - If you are using [`nvm`](https://github.com/nvm-sh/nvm) you can run the following before proceeding - - ``` - nvm install 16 && nvm use 16 - or + - If you are using [`nvm`](https://github.com/nvm-sh/nvm) you can run the following before proceeding + - ``` + nvm install 16 && nvm use 16 + + or + + nvm install 18 && nvm use 18 + ``` - nvm install 18 && nvm use 18 - ``` 5. [AWS CLI](https://aws.amazon.com/cli/) installed and configured to use with your AWS account 6. [Typescript 3.8+](https://www.typescriptlang.org/download) installed 7. [AWS CDK CLI](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed 8. [Docker](https://docs.docker.com/get-docker/) installed - N.B. [`buildx`](https://github.com/docker/buildx) is also required. For Windows and macOS `buildx` [is included](https://github.com/docker/buildx#windows-and-macos) in [Docker Desktop](https://docs.docker.com/desktop/) -10. [Python 3+](https://www.python.org/downloads/) installed +9. [Python 3+](https://www.python.org/downloads/) installed ### Deployment 1. Clone the repository + ```bash git clone https://github.com/aws-samples/aws-genai-llm-chatbot ``` + 2. Move into the cloned repository + ```bash cd aws-genai-llm-chatbot ``` #### (Optional) Only for Cloud9 -If you use Cloud9, increase the instance's EBS volume to at least 100GB. + +If you use Cloud9, increase the instance's EBS volume to at least 100GB. To do this, run the following command from the Cloud9 terminal: + ``` ./scripts/cloud9-resize.sh ``` -See the documentation for more details [on environment resize](https://docs.aws.amazon.com/cloud9/latest/user-guide/move-environment.html#move-environment-resize). - -3. Install the project dependencies and build the project by running this command +See the documentation for more details [on environment resize](https://docs.aws.amazon.com/cloud9/latest/user-guide/move-environment.html#move-environment-resize). + + 3. Install the project dependencies and build the project by running this command + ```bash npm install && npm run build ``` 4. Once done, run the magic-create CLI to help you set up the solution with the features you care most: + ```bash npm run create ``` -You'll be prompted to configure the different aspects of the solution, such as: + +You'll be prompted to configure the different aspects of the solution, such as: + - The LLMs or MLMs to enable (we support all models provided by Bedrock along with SageMaker hosted Idefics, FalconLite, Mistral and more to come) - Setup of the RAG system: engine selection (i.e. Aurora w/ pgvector, OpenSearch, Kendra..) embeddings selection and more to come. @@ -255,11 +276,12 @@ You can now deploy by running: ```bash npx cdk deploy ``` + > **Note**: This step duration can vary greatly, depending on the Constructs you are deploying. You can view the progress of your CDK deployment in the [CloudFormation console](https://console.aws.amazon.com/cloudformation/home) in the selected region. -6. Once deployed, take note of the `User Interface`, `User Pool` and, if you want to interact with [3P models providers](#3p-models-providers), the `Secret` that will, eventually, hold the various `API_KEYS` should you want to experiment with 3P providers. +6. Once deployed, take note of the `User Interface`, `User Pool` and, if you want to interact with [3P models providers](#3p-models-providers), the `Secret` that will, eventually, hold the various `API_KEYS` should you want to experiment with 3P providers. ```bash ... @@ -278,24 +300,24 @@ GenAIChatBotStack.ApiKeysSecretNameXXXX = ApiKeysSecretName-xxxxxx 10. Login with the user created in .8; you will be asked to change the password. - - # Run user interface locally -See instructions in the README file of the [`lib/user-interface/react-app`](./lib/user-interface/react-app) folder. +See instructions in the README file of the [`lib/user-interface/react-app`](./lib/user-interface/react-app/README.md) folder. + +# Using Kendra with a non-english index -# Using kendra with a non-english index If you're using Kendra with an index in a language other than English, you will need to make some code modifications. -You'll need to modify the filters in the file lib/shared/layers/python-sdk/python/genai_core/kendra/query.py. +You'll need to modify the filters in the file `lib/shared/layers/python-sdk/python/genai_core/kendra/query.py`. Example for french : + ```python if kendra_index_external or kendra_use_all_data: result = kendra.retrieve( - IndexId=kendra_index_id, - QueryText=query, - PageSize=limit, + IndexId=kendra_index_id, + QueryText=query, + PageSize=limit, PageNumber=1, AttributeFilter={'AndAllFilters': [{"EqualsTo": {"Key": "_language_code","Value": {"StringValue": "fr"}}}]} ) @@ -305,7 +327,7 @@ Example for french : QueryText=query, PageSize=limit, PageNumber=1, - AttributeFilter={'AndAllFilters': + AttributeFilter={'AndAllFilters': [ {"EqualsTo": {"Key": "_language_code","Value": {"StringValue": "fr"}}}, {"EqualsTo": {"Key": "workspace_id","Value": {"StringValue": workspace_id}}} @@ -315,35 +337,39 @@ Example for french : ``` Please note: If these adjustments are made post-deployment, it's essential to rebuild and redeploy. If done prior to deployment, you can proceed with the walkthrough as usual. + ```bash npm install && npm run build npx cdk deploy ``` # Clean up + You can remove the stacks and all the associated resources created in your AWS account by running the following command: ```bash npx cdk destroy ``` -> **Note**: Depending on which resources have been deployed. Destroying the stack might take a while, up to 45m. If the deletion fails multiple times, please manually delete the remaining stack's ENIs; you can filter ENIs by VPC/Subnet/etc using the search bar [here](https://console.aws.amazon.com/ec2/home#NIC) in the AWS console) and re-attempt a stack deletion. +> **Note**: Depending on which resources have been deployed. Destroying the stack might take a while, up to 45m. If the deletion fails multiple times, please manually delete the remaining stack's ENIs; you can filter ENIs by VPC/Subnet/etc using the search bar [here](https://console.aws.amazon.com/ec2/home#NIC) in the AWS console) and re-attempt a stack deletion. # Architecture -This repository comes with several reusable CDK constructs. Giving you the freedom to decide what to deploy and what not. -Here's an overview: +This repository comes with several reusable CDK constructs. Giving you the freedom to decide what to deploy and what not. + +Here's an overview: ![sample](assets/architecture.png "Architecture Diagram") # Authors + - [Bigad Soleiman](https://www.linkedin.com/in/bigadsoleiman/) - [Sergey Pugachev](https://www.linkedin.com/in/spugachev/) - # Credits This sample was made possible thanks to the following libraries: + - [langchain](https://python.langchain.com/docs/get_started/introduction.html) from [LangChain AI](https://github.com/langchain-ai) - [unstructured](https://github.com/Unstructured-IO/unstructured) from [Unstructured-IO](https://github.com/Unstructured-IO/unstructured) - [pgvector](https://github.com/pgvector/pgvector) from [Andrew Kane](https://github.com/ankane) diff --git a/assets/architecture.png b/assets/architecture.png index ce39db2903ab837e87ba0cb3a3cfd50f25664d10..007dfce296fed9111cc480eb442757419f94e9e0 100644 GIT binary patch literal 111254 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 S_b}1GA0>I%-3nQ=kpBl|e?cn% literal 99013 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 diff --git a/docs/appsync/appsync.md b/docs/appsync/appsync.md new file mode 100644 index 000000000..52cc55982 --- /dev/null +++ b/docs/appsync/appsync.md @@ -0,0 +1,22 @@ +# Using AppSync + +Define or change the schema in `./lib/chatbot-api/schema`. + +At the moment we only use the `schema-ws.graphql` to define the real-time API. The REST API might be replaced by AppSync in the future. + +If you modified the definition for the schema, you can regenerate the client code using + +```bash +cd lib/user-interface/react-app +npx @npx @aws-amplify/cli codegen add --apiId --region +``` + +Accept all the defaults. + +If you use a None data source, you need to modify `src/API.ts` adding: + +```ts +export type NoneQueryVariables = { + none?: string | null; +}; +``` diff --git a/docs/document-retrieval/retriever.md b/docs/document-retrieval/retriever.md new file mode 100644 index 000000000..188f7253d --- /dev/null +++ b/docs/document-retrieval/retriever.md @@ -0,0 +1,32 @@ +# Document stores + +This solution uses "pluggable" document stores to implement RAG, also called **engines**. A document store implementation must provide the following functions: + +- a **query** function to retrieve documents from the store. The function is invoked with the following parameters: + - `workspace_id`: str - the workspace id + - `workspace`: dict: a dictionary containing additional metadata related to your datastore + - `query`: str - the query to search the documents + - `full_response`: boolean - a flag indicating if the response should also include the retrieval scores +- a **create** function that gets invoked when a new workspace using this document store is created. Perform any operations needed to create resources that needs to be exclusively associated with the workspace +- a **delete** function that gets invoked when a workspace is removed. Cleanup any resources you have might have created that are to the exclusive use of the workspace + +To keep the current convention, create a new folder inside `layers/python-sdk/python/genai_core/` called after your engine in which you create the different functions. Export all the different functions via an `__init__.py` file in the same folder. + +You need to modify the `semantic_search.py` file to add the invocation for your query function based on the type of engine. + +You also need to add a specific `create_workspace_` in the `workspaces.py` file. This function is then invoked by the REST API workspace route handler `lib/chatbot-api/functions/api-handler/routes/workspaces.py`. + +[ ] Added API handler function + +[ ] Added create_workspace + +The call to the **delete** function for your workspace must be added to the `rag-engines/workspaces/functions/delete-workspace-workflow/delete/index.py` function. + +[ ] Added delete + +The support for your workspace type must also be added to the front-end. + +- `react-app/src/components/pages/rag/workspace` to implement the document store specific settings +- `react-app/src/components/pages/rag/create-workspace` to implement components necessary to create a new workspace based on this document store + +[ ] Added UI diff --git a/docs/sagemker-hosting/inference-script.md b/docs/sagemker-hosting/inference-script.md new file mode 100644 index 000000000..b373e7a88 --- /dev/null +++ b/docs/sagemker-hosting/inference-script.md @@ -0,0 +1,32 @@ +# Inference script + +We are using a multi-model enpoint hosted on Sagemaker and provide a inference script to process requests and send responses back. + +The inference script is currently hardcoded with the supported models (lib/rag-engines/sagemaker-rag-models/model/inference.py) + +```py +embeddings_models = [ + "intfloat/multilingual-e5-large", + "sentence-transformers/all-MiniLM-L6-v2", +] +cross_encoder_models = ["cross-encoder/ms-marco-MiniLM-L-12-v2"] +``` + +The API is JSON body based: + +```json +{ + "type": "embeddings", + "model": "intfloat/multilingual-e5-large", + "input": "I love Berlin" +} +``` + +```json +{ + "type": "cross-encoder", + "model": "cross-encoder/ms-marco-MiniLM-L-12-v2", + "input": "I love Berlin", + "passages": ["I love Paris", "I love London"] +} +``` diff --git a/lib/authentication/index.ts b/lib/authentication/index.ts index 66821d5cf..7699fccdb 100644 --- a/lib/authentication/index.ts +++ b/lib/authentication/index.ts @@ -52,6 +52,10 @@ export class Authentication extends Construct { value: userPool.userPoolId, }); + new cdk.CfnOutput(this, "IdentityPoolId", { + value: identityPool.identityPoolId, + }); + new cdk.CfnOutput(this, "UserPoolWebClientId", { value: userPoolClient.userPoolClientId, }); diff --git a/lib/aws-genai-llm-chatbot-stack.ts b/lib/aws-genai-llm-chatbot-stack.ts index 2d3b509f0..427b564b9 100644 --- a/lib/aws-genai-llm-chatbot-stack.ts +++ b/lib/aws-genai-llm-chatbot-stack.ts @@ -148,8 +148,7 @@ export class AwsGenAILLMChatbotStack extends cdk.Stack { userPoolId: authentication.userPool.userPoolId, userPoolClientId: authentication.userPoolClient.userPoolClientId, identityPool: authentication.identityPool, - restApi: chatBotApi.restApi, - webSocketApi: chatBotApi.webSocketApi, + api: chatBotApi, chatbotFilesBucket: chatBotApi.filesBucket, crossEncodersEnabled: typeof ragEngines?.sageMakerRagModels?.model !== "undefined", diff --git a/lib/chatbot-api/appsync-ws.ts b/lib/chatbot-api/appsync-ws.ts new file mode 100644 index 000000000..20cb43240 --- /dev/null +++ b/lib/chatbot-api/appsync-ws.ts @@ -0,0 +1,104 @@ +import * as cdk from "aws-cdk-lib"; +import * as appsync from "aws-cdk-lib/aws-appsync"; +import { Code, Function, LayerVersion, Runtime } from "aws-cdk-lib/aws-lambda"; +import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources"; +import { Construct } from "constructs"; +import { Shared } from "../shared"; +import { IQueue } from "aws-cdk-lib/aws-sqs"; +import { ITopic } from "aws-cdk-lib/aws-sns"; +import { UserPool } from "aws-cdk-lib/aws-cognito"; +import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; +import * as path from "path"; + +interface RealtimeResolversProps { + readonly queue: IQueue; + readonly topic: ITopic; + readonly userPool: UserPool; + readonly shared: Shared; + readonly api: appsync.GraphqlApi; +} + +export class RealtimeResolvers extends Construct { + public readonly outgoingMessageHandler: Function; + + constructor(scope: Construct, id: string, props: RealtimeResolversProps) { + super(scope, id); + + const powertoolsLayerJS = LayerVersion.fromLayerVersionArn( + this, + "PowertoolsLayerJS", + `arn:aws:lambda:${ + cdk.Stack.of(this).region + }:094274105915:layer:AWSLambdaPowertoolsTypeScript:22` + ); + + const resolverFunction = new Function(this, "lambda-resolver", { + code: Code.fromAsset( + "./lib/chatbot-api/functions/resolvers/send-query-lambda-resolver" + ), + handler: "index.handler", + runtime: Runtime.PYTHON_3_11, + environment: { + SNS_TOPIC_ARN: props.topic.topicArn, + }, + layers: [props.shared.powerToolsLayer], + }); + + const outgoingMessageHandler = new NodejsFunction( + this, + "outgoing-message-handler", + { + entry: path.join( + __dirname, + "functions/outgoing-message-appsync/index.ts" + ), + layers: [powertoolsLayerJS], + handler: "index.handler", + runtime: Runtime.NODEJS_18_X, + environment: { + GRAPHQL_ENDPOINT: props.api.graphqlUrl, + }, + } + ); + + outgoingMessageHandler.addEventSource(new SqsEventSource(props.queue)); + + props.topic.grantPublish(resolverFunction); + + const functionDataSource = props.api.addLambdaDataSource( + "realtimeResolverFunction", + resolverFunction + ); + const noneDataSource = props.api.addNoneDataSource("none", { + name: "relay-source", + }); + + props.api.createResolver("send-message-resolver", { + typeName: "Mutation", + fieldName: "sendQuery", + dataSource: functionDataSource, + }); + + props.api.createResolver("publish-response-resolver", { + typeName: "Mutation", + fieldName: "publishResponse", + code: appsync.Code.fromAsset( + "./lib/chatbot-api/functions/resolvers/publish-response-resolver.js" + ), + runtime: appsync.FunctionRuntime.JS_1_0_0, + dataSource: noneDataSource, + }); + + props.api.createResolver("subscription-resolver", { + typeName: "Subscription", + fieldName: "receiveMessages", + code: appsync.Code.fromAsset( + "./lib/chatbot-api/functions/resolvers/subscribe-resolver.js" + ), + runtime: appsync.FunctionRuntime.JS_1_0_0, + dataSource: noneDataSource, + }); + + this.outgoingMessageHandler = outgoingMessageHandler; + } +} diff --git a/lib/chatbot-api/functions/api-handler/index.py b/lib/chatbot-api/functions/api-handler/index.py index 6b255334f..cfc2f8885 100644 --- a/lib/chatbot-api/functions/api-handler/index.py +++ b/lib/chatbot-api/functions/api-handler/index.py @@ -1,17 +1,8 @@ -import json -import genai_core.types -import genai_core.parameters -import genai_core.utils.json -from pydantic import ValidationError -from botocore.exceptions import ClientError from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext -from aws_lambda_powertools.event_handler.api_gateway import Response from aws_lambda_powertools.event_handler import ( - APIGatewayRestResolver, - CORSConfig, - content_types, + AppSyncResolver, ) from routes.health import router as health_router from routes.embeddings import router as embeddings_router @@ -27,13 +18,7 @@ tracer = Tracer() logger = Logger() - -cors_config = CORSConfig(allow_origin="*", max_age=0) -app = APIGatewayRestResolver( - cors=cors_config, - strip_prefixes=["/v1"], - serializer=lambda obj: json.dumps(obj, cls=genai_core.utils.json.CustomEncoder), -) +app = AppSyncResolver() app.include_router(health_router) app.include_router(rag_router) @@ -47,55 +32,9 @@ app.include_router(kendra_router) - -@app.exception_handler(genai_core.types.CommonError) -def handle_value_error(e: genai_core.types.CommonError): - logger.exception(e) - - return Response( - status_code=200, - content_type=content_types.APPLICATION_JSON, - body=json.dumps( - {"error": True, "message": str(e)}, cls=genai_core.utils.json.CustomEncoder - ), - ) - - -@app.exception_handler(ClientError) -def handle_value_error(e: ClientError): - logger.exception(e) - - return Response( - status_code=200, - content_type=content_types.APPLICATION_JSON, - body=json.dumps( - {"error": True, "message": str(e)}, - cls=genai_core.utils.json.CustomEncoder, - ), - ) - - -@app.exception_handler(ValidationError) -def handle_value_error(e: ValidationError): - logger.exception(e) - - return Response( - status_code=200, - content_type=content_types.APPLICATION_JSON, - body=json.dumps( - {"error": True, "message": [str(error) for error in e.errors()]}, - cls=genai_core.utils.json.CustomEncoder, - ), - ) - - @logger.inject_lambda_context( - log_event=True, correlation_id_path=correlation_paths.API_GATEWAY_REST + log_event=True, correlation_id_path=correlation_paths.APPSYNC_RESOLVER ) @tracer.capture_lambda_handler def handler(event: dict, context: LambdaContext) -> dict: - origin_verify_header_value = genai_core.parameters.get_origin_verify_header_value() - if event["headers"]["X-Origin-Verify"] == origin_verify_header_value: - return app.resolve(event, context) - - return {"statusCode": 403, "body": "Forbidden"} + return app.resolve(event, context) diff --git a/lib/chatbot-api/functions/api-handler/routes/cross_encoders.py b/lib/chatbot-api/functions/api-handler/routes/cross_encoders.py index 52472be01..8fcebbf16 100644 --- a/lib/chatbot-api/functions/api-handler/routes/cross_encoders.py +++ b/lib/chatbot-api/functions/api-handler/routes/cross_encoders.py @@ -4,7 +4,7 @@ from typing import List from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() @@ -14,23 +14,22 @@ class CrossEncodersRequest(BaseModel): provider: str model: str - input: str + reference: str passages: List[str] -@router.get("/cross-encoders/models") +@router.resolver(field_name="listCrossEncoders") @tracer.capture_method def models(): models = genai_core.cross_encoder.get_cross_encoder_models() - return {"ok": True, "data": models} + return models -@router.post("/cross-encoders") +@router.resolver(field_name="rankPassages") @tracer.capture_method -def cross_encoders(): - data: dict = router.current_event.json_body - request = CrossEncodersRequest(**data) +def cross_encoders(input: dict): + request = CrossEncodersRequest(**input) selected_model = genai_core.cross_encoder.get_cross_encoder_model( request.provider, request.model ) @@ -39,6 +38,6 @@ def cross_encoders(): raise genai_core.types.CommonError("Model not found") ret_value = genai_core.cross_encoder.rank_passages( - selected_model, request.input, request.passages + selected_model, request.reference, request.passages ) - return {"ok": True, "data": ret_value} + return [{"score": v, "passage": p} for v, p in zip(ret_value, request.passages)] diff --git a/lib/chatbot-api/functions/api-handler/routes/documents.py b/lib/chatbot-api/functions/api-handler/routes/documents.py index 1014fe0a5..26c61fd08 100644 --- a/lib/chatbot-api/functions/api-handler/routes/documents.py +++ b/lib/chatbot-api/functions/api-handler/routes/documents.py @@ -4,7 +4,8 @@ import genai_core.documents from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router +from typing import Optional tracer = Tracer() router = Router() @@ -12,37 +13,67 @@ class FileUploadRequest(BaseModel): + workspaceId: str fileName: str class TextDocumentRequest(BaseModel): + workspaceId: str title: str content: str class QnADocumentRequest(BaseModel): + workspaceId: str question: str answer: str class WebsiteDocumentRequest(BaseModel): + workspaceId: str sitemap: bool address: str followLinks: bool limit: int + class RssFeedDocumentRequest(BaseModel): - address: str + workspaceId: str + documentId: Optional[str] + address: Optional[str] limit: int - title: str + title: Optional[str] followLinks: bool + class RssFeedCrawlerUpdateRequest(BaseModel): documentType: str followLinks: bool limit: int +class ListDocumentsRequest(BaseModel): + workspaceId: str + documentType: str + lastDocumentId: Optional[str] + + +class GetDocumentRequest(BaseModel): + workspaceId: str + documentId: str + + +class GetRssPostsRequest(BaseModel): + workspaceId: str + documentId: str + lastDocumentId: Optional[str] + + +class DocumentSubscriptionStatusRequest(BaseModel): + workspaceId: str + documentId: str + status: str + allowed_extensions = set( [ @@ -69,211 +100,195 @@ class RssFeedCrawlerUpdateRequest(BaseModel): ) -@router.post("/workspaces//documents/file-upload") +@router.resolver(field_name="getUploadFileURL") @tracer.capture_method -def file_upload(workspace_id: str): - data: dict = router.current_event.json_body - request = FileUploadRequest(**data) - +def file_upload(input: dict): + request = FileUploadRequest(**input) _, extension = os.path.splitext(request.fileName) if extension not in allowed_extensions: raise genai_core.types.CommonError("Invalid file extension") - result = genai_core.upload.generate_presigned_post(workspace_id, request.fileName) + result = genai_core.upload.generate_presigned_post( + request.workspaceId, request.fileName + ) - return {"ok": True, "data": result} + print(result) + return result -@router.get("/workspaces//documents/") +@router.resolver(field_name="listDocuments") @tracer.capture_method -def get_documents(workspace_id: str, document_type: str): - query_string = router.current_event.query_string_parameters or {} - last_document_id = query_string.get("lastDocumentId", None) - +def get_documents(input: dict): + request = ListDocumentsRequest(**input) result = genai_core.documents.list_documents( - workspace_id, document_type, last_document_id + request.workspaceId, request.documentType, request.lastDocumentId ) return { - "ok": True, - "data": { - "items": [_convert_document(item) for item in result["items"]], - "lastDocumentId": result["last_document_id"], - }, + "items": [_convert_document(item) for item in result["items"]], + "lastDocumentId": result["last_document_id"], } -@router.get("/workspaces//documents//detail") +@router.resolver(field_name="getDocument") @tracer.capture_method -def get_document_details(workspace_id: str, document_id: str): - result = genai_core.documents.get_document(workspace_id, document_id) +def get_document_details(input: dict): + request = GetDocumentRequest(**input) - return { - "ok": True, - "data": { - "items":[_convert_document(result)], - "lastDocumentId": None - } - } + result = genai_core.documents.get_document(request.workspaceId, request.documentId) -@router.get("/workspaces//documents//posts") + return _convert_document(result) + + +@router.resolver(field_name="getRSSPosts") @tracer.capture_method -def get_rss_posts(workspace_id: str, document_id: str): - query_string = router.current_event.query_string_parameters or {} - last_document_id = query_string.get("lastDocumentId", None) +def get_rss_posts(input: dict): + request = GetRssPostsRequest(**input) result = genai_core.documents.list_documents( - workspace_id, "rsspost", last_document_id=last_document_id, - parent_document_id=document_id + workspace_id=request.workspaceId, + document_type="rsspost", + last_document_id=request.lastDocumentId, + parent_document_id=request.documentId, ) return { - "ok": True, - "data": { - "items": [_convert_document(item) for item in result["items"]], - "lastDocumentId": result["last_document_id"], - }, + "items": [_convert_document(item) for item in result["items"]], + "lastDocumentId": result["last_document_id"], } -@router.get("/workspaces//documents//enable") + +@router.resolver(field_name="setDocumentSubscriptionStatus") @tracer.capture_method -def enable_document(workspace_id: str, document_id: str): - result = genai_core.documents.enable_document_subscription(workspace_id, document_id) +def enable_document(input: dict): + request = DocumentSubscriptionStatusRequest(**input) + + if request.status not in ["enabled", "disabled"]: + raise genai_core.types.CommonError("Invalid status") + if request.status == "enabled": + result = genai_core.documents.enable_document_subscription( + request.workspaceId, request.documentId + ) + else: + result = genai_core.documents.disable_document_subscription( + request.workspaceId, request.documentId + ) return { - "ok": True, - "data": { - "workspaceId": workspace_id, - "documentId": document_id, - "status": "enabled" - } + "workspaceId": request.workspaceId, + "documentId": request.documentId, + "status": result, } -@router.get("/workspaces//documents//disable") + +@router.resolver(field_name="addTextDocument") @tracer.capture_method -def disable_document(workspace_id: str, document_id: str): - result = genai_core.documents.disable_document_subscription(workspace_id, document_id) +def add_text_document(input: dict): + request = TextDocumentRequest(**input) + title = request.title.strip()[:1000] + content = request.content.strip()[:10000] + result = genai_core.documents.create_document( + workspace_id=request.workspaceId, + document_type="text", + title=title, + content=content, + ) return { - "ok": True, - "data": { - "workspaceId": workspace_id, - "documentId": document_id, - "status": "disabled" - } + "workspaceId": result["workspace_id"], + "documentId": result["document_id"], } -@router.post("/workspaces//documents/") +@router.resolver(field_name="addQnADocument") @tracer.capture_method -def add_document(workspace_id: str, document_type: str): - data: dict = router.current_event.json_body - - if document_type == "text": - request = TextDocumentRequest(**data) - request.title = request.title.strip()[:1000] - result = genai_core.documents.create_document( - workspace_id=workspace_id, - document_type=document_type, - title=request.title, - content=request.content, - ) +def add_qna_document(input: dict): + request = QnADocumentRequest(**input) + question = request.question.strip()[:1000] + answer = request.answer.strip()[:1000] + result = genai_core.documents.create_document( + workspace_id=request.workspaceId, + document_type="qna", + title=question, + content=question, + content_complement=answer, + ) - return { - "ok": True, - "data": { - "workspaceId": result["workspace_id"], - "documentId": result["document_id"], - }, - } - elif document_type == "qna": - request = QnADocumentRequest(**data) - request.question = request.question.strip()[:1000] - request.answer = request.answer.strip()[:1000] - result = genai_core.documents.create_document( - workspace_id=workspace_id, - document_type=document_type, - title=request.question, - content=request.question, - content_complement=request.answer, - ) + return { + "workspaceId": result["workspace_id"], + "documentId": result["document_id"], + } - return { - "ok": True, - "data": { - "workspaceId": result["workspace_id"], - "documentId": result["document_id"], - }, - } - elif document_type == "website": - request = WebsiteDocumentRequest(**data) - request.address = request.address.strip()[:10000] - document_sub_type = "sitemap" if request.sitemap else None - request.limit = min(max(request.limit, 1), 1000) - - result = genai_core.documents.create_document( - workspace_id=workspace_id, - document_type=document_type, - document_sub_type=document_sub_type, - path=request.address, - crawler_properties={ - "follow_links": request.followLinks, - "limit": request.limit, - }, - ) - return { - "ok": True, - "data": { - "workspaceId": result["workspace_id"], - "documentId": result["document_id"], - }, - } - - elif document_type == "rssfeed": - request = RssFeedDocumentRequest(**data) - request.address = request.address.strip()[:10000] - path=request.address - - result = genai_core.documents.create_document( - workspace_id=workspace_id, - document_type=document_type, - path=path, - title=request.title, - crawler_properties={ - "follow_links": request.followLinks, - "limit": request.limit, - }, - ) +@router.resolver(field_name="addWebsite") +@tracer.capture_method +def add_website(input: dict): + request = WebsiteDocumentRequest(**input) + + address = request.address.strip()[:10000] + document_sub_type = "sitemap" if request.sitemap else None + limit = min(max(request.limit, 1), 1000) + + result = genai_core.documents.create_document( + workspace_id=request.workspaceId, + document_type="website", + document_sub_type=document_sub_type, + path=address, + crawler_properties={ + "follow_links": request.followLinks, + "limit": limit, + }, + ) - return { - "ok": True, - "data": { - "workspaceId": result["workspace_id"], - "documentId": result["document_id"], - } - } - -@router.patch("/workspaces//documents//") + return { + "workspaceId": result["workspace_id"], + "documentId": result["document_id"], + } + + +@router.resolver(field_name="addRssFeed") @tracer.capture_method -def update_document(workspace_id: str, document_id: str): - data: dict = router.current_event.json_body - if "documentType" in data: - if data["documentType"] == "rssfeed": - request = RssFeedCrawlerUpdateRequest(**data) - result = genai_core.documents.update_document( - workspace_id=workspace_id, - document_id=document_id, - document_type=request.documentType, - follow_links=request.followLinks, - limit=request.limit, - ) - return { - "ok": True, - "data": "done" - } +def add_rss_feed( + input: dict, +): + request = RssFeedDocumentRequest(**input) + address = request.address.strip()[:10000] + path = address + + result = genai_core.documents.create_document( + workspace_id=request.workspaceId, + document_type="rssfeed", + path=path, + title=request.title, + crawler_properties={ + "follow_links": request.followLinks, + "limit": request.limit, + }, + ) + + return { + "workspaceId": result["workspace_id"], + "documentId": result["document_id"], + } +@router.resolver(field_name="updateRSSFeed") +@tracer.capture_method +def update_rss_feed(input: dict): + request = RssFeedDocumentRequest(**input) + result = genai_core.documents.update_document( + workspace_id=request.workspaceId, + document_id=request.documentId, + document_type="rssfeed", + follow_links=request.followLinks, + limit=request.limit, + ) + return { + "workspaceId": result["workspace_id"], + "documentId": result["document_id"], + "status": "updated", + } + def _convert_document(document: dict): if "crawler_properties" in document: @@ -296,9 +311,11 @@ def _convert_document(document: dict): "createdAt": document["created_at"], "updatedAt": document.get("updated_at", None), "rssFeedId": document.get("rss_feed_id", None), - "rssLastCheckedAt": document.get("rss_last_checked",None), - "crawlerProperties": { - "followLinks": document.get("crawler_properties").get("follow_links",None), - "limit": document.get("crawler_properties").get("limit",None) - } if document.get("crawler_properties", None) != None else None + "rssLastCheckedAt": document.get("rss_last_checked", None), + "crawlerProperties": { + "followLinks": document.get("crawler_properties").get("follow_links", None), + "limit": document.get("crawler_properties").get("limit", None), + } + if document.get("crawler_properties", None) != None + else None, } diff --git a/lib/chatbot-api/functions/api-handler/routes/embeddings.py b/lib/chatbot-api/functions/api-handler/routes/embeddings.py index 1b6c5bede..47d991550 100644 --- a/lib/chatbot-api/functions/api-handler/routes/embeddings.py +++ b/lib/chatbot-api/functions/api-handler/routes/embeddings.py @@ -4,7 +4,7 @@ from typing import List from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() @@ -14,22 +14,21 @@ class EmbeddingsRequest(BaseModel): provider: str model: str - input: List[str] + passages: List[str] -@router.get("/embeddings/models") +@router.resolver(field_name="listEmbeddingModels") @tracer.capture_method def models(): models = genai_core.embeddings.get_embeddings_models() - return {"ok": True, "data": models} + return models -@router.post("/embeddings") +@router.resolver(field_name="calculateEmbeddings") @tracer.capture_method -def embeddings(): - data: dict = router.current_event.json_body - request = EmbeddingsRequest(**data) +def embeddings(input: dict): + request = EmbeddingsRequest(**input) selected_model = genai_core.embeddings.get_embeddings_model( request.provider, request.model ) @@ -37,6 +36,11 @@ def embeddings(): if selected_model is None: raise genai_core.types.CommonError("Model not found") - ret_value = genai_core.embeddings.generate_embeddings(selected_model, request.input) + ret_value = genai_core.embeddings.generate_embeddings( + selected_model, request.passages + ) - return {"ok": True, "data": ret_value} + return [ + {"vector": v, "passage": request.passages[idx]} + for idx, v in enumerate(ret_value) + ] diff --git a/lib/chatbot-api/functions/api-handler/routes/health.py b/lib/chatbot-api/functions/api-handler/routes/health.py index 5e7694d39..34efdab33 100644 --- a/lib/chatbot-api/functions/api-handler/routes/health.py +++ b/lib/chatbot-api/functions/api-handler/routes/health.py @@ -1,12 +1,12 @@ from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() logger = Logger() -@router.get("/health") +@router.resolver(field_name="checkHealth") @tracer.capture_method def health(): - return {"ok": True} + return True diff --git a/lib/chatbot-api/functions/api-handler/routes/kendra.py b/lib/chatbot-api/functions/api-handler/routes/kendra.py index db62685e5..1999d7e57 100644 --- a/lib/chatbot-api/functions/api-handler/routes/kendra.py +++ b/lib/chatbot-api/functions/api-handler/routes/kendra.py @@ -2,7 +2,7 @@ import genai_core.kendra from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() @@ -13,28 +13,25 @@ class KendraDataSynchRequest(BaseModel): workspaceId: str -@router.get("/rag/engines/kendra/indexes") +@router.resolver(field_name="listKendraIndexes") @tracer.capture_method def kendra_indexes(): indexes = genai_core.kendra.get_kendra_indexes() - return {"ok": True, "data": indexes} + return indexes -@router.post("/rag/engines/kendra/data-sync") +@router.resolver(field_name="startKendraDataSync") @tracer.capture_method -def kendra_data_sync(): - data: dict = router.current_event.json_body - request = KendraDataSynchRequest(**data) +def kendra_data_sync(workspaceId: str): + genai_core.kendra.start_kendra_data_sync(workspace_id=workspaceId) - genai_core.kendra.start_kendra_data_sync(workspace_id=request.workspaceId) + return True - return {"ok": True, "data": True} - -@router.get("/rag/engines/kendra/data-sync/") +@router.resolver(field_name="isKendraDataSynching") @tracer.capture_method -def kendra_is_syncing(workspace_id: str): - result = genai_core.kendra.kendra_is_syncing(workspace_id=workspace_id) +def kendra_is_syncing(workspaceId: str): + result = genai_core.kendra.kendra_is_syncing(workspace_id=workspaceId) - return {"ok": True, "data": result} + return result diff --git a/lib/chatbot-api/functions/api-handler/routes/models.py b/lib/chatbot-api/functions/api-handler/routes/models.py index 6c58bf797..641a7cb19 100644 --- a/lib/chatbot-api/functions/api-handler/routes/models.py +++ b/lib/chatbot-api/functions/api-handler/routes/models.py @@ -1,15 +1,15 @@ import genai_core.models from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() logger = Logger() -@router.get("/models") +@router.resolver(field_name="listModels") @tracer.capture_method def models(): models = genai_core.models.list_models() - return {"ok": True, "data": models} + return models diff --git a/lib/chatbot-api/functions/api-handler/routes/rag.py b/lib/chatbot-api/functions/api-handler/routes/rag.py index 60d3863ad..72a239bcf 100644 --- a/lib/chatbot-api/functions/api-handler/routes/rag.py +++ b/lib/chatbot-api/functions/api-handler/routes/rag.py @@ -2,7 +2,7 @@ import genai_core.kendra from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() @@ -13,7 +13,7 @@ class KendraDataSynchRequest(BaseModel): workspaceId: str -@router.get("/rag/engines") +@router.resolver(field_name="listRagEngines") @tracer.capture_method def engines(): config = genai_core.parameters.get_config() @@ -37,4 +37,4 @@ def engines(): }, ] - return {"ok": True, "data": ret_value} + return ret_value diff --git a/lib/chatbot-api/functions/api-handler/routes/semantic_search.py b/lib/chatbot-api/functions/api-handler/routes/semantic_search.py index 5dbd72d21..dd057c0b0 100644 --- a/lib/chatbot-api/functions/api-handler/routes/semantic_search.py +++ b/lib/chatbot-api/functions/api-handler/routes/semantic_search.py @@ -1,7 +1,7 @@ import genai_core.semantic_search from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() @@ -13,12 +13,10 @@ class SemanticSearchRequest(BaseModel): query: str -@router.post("/semantic-search") +@router.resolver(field_name="performSemanticSearch") @tracer.capture_method -def semantic_search(): - data: dict = router.current_event.json_body - request = SemanticSearchRequest(**data) - +def semantic_search(input: dict): + request = SemanticSearchRequest(**input) if len(request.query) == 0 or len(request.query) > 1000: raise genai_core.types.CommonError( "Query must be between 1 and 1000 characters" @@ -32,7 +30,7 @@ def semantic_search(): ) result = _convert_semantic_search_result(request.workspaceId, result) - return {"ok": True, "data": result} + return result def _convert_semantic_search_result(workspace_id: str, result: dict): @@ -54,7 +52,7 @@ def _convert_semantic_search_result(workspace_id: str, result: dict): ret_value = { "engine": result["engine"], "workspaceId": workspace_id, - "queryLanguage": result.get("query_language"), + "queryLanguage": result.get("query_language", "en"), "supportedLanguages": result.get("supported_languages"), "detectedLanguages": result.get("detected_languages"), "items": items, @@ -80,7 +78,7 @@ def _convert_semantic_search_item(item: dict): "title": item["title"], "content": item["content"], "contentComplement": item["content_complement"], - "vectorSearchScore": item.get("vector_search_score"), + "vectorSearchScore": item.get("vector_search_score", 0), "keywordSearchScore": item.get("keyword_search_score"), "score": item["score"], } diff --git a/lib/chatbot-api/functions/api-handler/routes/sessions.py b/lib/chatbot-api/functions/api-handler/routes/sessions.py index db2915fdc..1bedf807a 100644 --- a/lib/chatbot-api/functions/api-handler/routes/sessions.py +++ b/lib/chatbot-api/functions/api-handler/routes/sessions.py @@ -1,15 +1,17 @@ import genai_core.sessions import genai_core.types import genai_core.auth +import genai_core.utils.json from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router +import json tracer = Tracer() router = Router() logger = Logger() -@router.get("/sessions") +@router.resolver(field_name="listSessions") @tracer.capture_method def get_sessions(): user_id = genai_core.auth.get_user_id(router) @@ -18,53 +20,50 @@ def get_sessions(): sessions = genai_core.sessions.list_sessions_by_user_id(user_id) - return { - "ok": True, - "data": [ - { - "id": session.get("SessionId"), - "title": session.get("History")[0].get("data", {}).get("content") - if session.get("History") - else "", - "startTime": session.get("StartTime"), - } - for session in sessions - ], - } + return [ + { + "id": session.get("SessionId"), + "title": session.get("History", [{}])[0] + .get("data", {}) + .get("content", ""), + "startTime": f'{session.get("StartTime")}Z', + } + for session in sessions + ] -@router.get("/sessions/") +@router.resolver(field_name="getSession") @tracer.capture_method -def get_session(session_id: str): +def get_session(id: str): user_id = genai_core.auth.get_user_id(router) if user_id is None: raise genai_core.types.CommonError("User not found") - session = genai_core.sessions.get_session(session_id, user_id) + session = genai_core.sessions.get_session(id, user_id) if not session: - return {"ok": True, "data": None} + return None return { - "ok": True, - "data": { - "id": session.get("SessionId"), - "title": session.get("History")[0].get("data", {}).get("content") - if session.get("History") - else "", - "startTime": session.get("StartTime"), - "history": [ - { - "type": item.get("type"), - "content": item.get("data", {}).get("content"), - "metadata": item.get("data", {}).get("additional_kwargs"), - } - for item in session.get("History") - ], - }, + "id": session.get("SessionId"), + "title": session.get("History", [{}])[0] + .get("data", {}) + .get("content", ""), + "startTime": f'{session.get("StartTime")}Z', + "history": [ + { + "type": item.get("type"), + "content": item.get("data", {}).get("content"), + "metadata": json.dumps( + item.get("data", {}).get("additional_kwargs"), + cls=genai_core.utils.json.CustomEncoder, + ), + } + for item in session.get("History") + ], } -@router.delete("/sessions") +@router.resolver(field_name="deleteUserSessions") @tracer.capture_method def delete_user_sessions(): user_id = genai_core.auth.get_user_id(router) @@ -73,16 +72,16 @@ def delete_user_sessions(): result = genai_core.sessions.delete_user_sessions(user_id) - return {"ok": True, "data": result} + return result -@router.delete("/sessions/") +@router.resolver(field_name="deleteSession") @tracer.capture_method -def delete_session(session_id: str): +def delete_session(id: str): user_id = genai_core.auth.get_user_id(router) if user_id is None: raise genai_core.types.CommonError("User not found") - result = genai_core.sessions.delete_session(session_id, user_id) + result = genai_core.sessions.delete_session(id, user_id) - return {"ok": True, "data": result} + return result diff --git a/lib/chatbot-api/functions/api-handler/routes/workspaces.py b/lib/chatbot-api/functions/api-handler/routes/workspaces.py index 71cabcbab..1c71a24fe 100644 --- a/lib/chatbot-api/functions/api-handler/routes/workspaces.py +++ b/lib/chatbot-api/functions/api-handler/routes/workspaces.py @@ -5,7 +5,7 @@ import genai_core.workspaces from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() @@ -29,7 +29,7 @@ class CreateWorkspaceAuroraRequest(BaseModel): metric: str index: bool hybridSearch: bool - chunking_strategy: str + chunkingStrategy: str chunkSize: int chunkOverlap: int @@ -43,7 +43,7 @@ class CreateWorkspaceOpenSearchRequest(BaseModel): crossEncoderModelName: str languages: list[str] hybridSearch: bool - chunking_strategy: str + chunkingStrategy: str chunkSize: int chunkOverlap: int @@ -55,57 +55,64 @@ class CreateWorkspaceKendraRequest(BaseModel): useAllData: bool -@router.get("/workspaces") +@router.resolver(field_name="listWorkspaces") @tracer.capture_method -def workspaces(): +def list_workspaces(): workspaces = genai_core.workspaces.list_workspaces() ret_value = [_convert_workspace(workspace) for workspace in workspaces] - return {"ok": True, "data": ret_value} + return ret_value -@router.get("/workspaces/") +@router.resolver(field_name="getWorkspace") @tracer.capture_method -def workspace(workspace_id: str): - workspace = genai_core.workspaces.get_workspace(workspace_id) +def get_workspace(workspaceId: str): + workspace = genai_core.workspaces.get_workspace(workspaceId) if not workspace: - return {"ok": True, "data": None} + return None ret_value = _convert_workspace(workspace) - return {"ok": True, "data": ret_value} + return ret_value -@router.delete("/workspaces/") +@router.resolver(field_name="deleteWorkspace") @tracer.capture_method -def workspace(workspace_id: str): - genai_core.workspaces.delete_workspace(workspace_id) +def delete_workspace(workspaceId: str): + genai_core.workspaces.delete_workspace(workspaceId) - return {"ok": True} + +@router.resolver(field_name="createAuroraWorkspace") +@tracer.capture_method +def create_aurora_workspace(input: dict): + config = genai_core.parameters.get_config() + + request = CreateWorkspaceAuroraRequest(**input) + ret_value = _create_workspace_aurora(request, config) + + return ret_value -@router.put("/workspaces") +@router.resolver(field_name="createOpenSearchWorkspace") @tracer.capture_method -def create_workspace(): - data: dict = router.current_event.json_body - generic_request = GenericCreateWorkspaceRequest(**data) +def create_open_search_workspace(input: dict): config = genai_core.parameters.get_config() - if generic_request.kind == "aurora": - request = CreateWorkspaceAuroraRequest(**data) - ret_value = _create_workspace_aurora(request, config) - elif generic_request.kind == "opensearch": - request = CreateWorkspaceOpenSearchRequest(**data) - ret_value = _create_workspace_open_search(request, config) - elif generic_request.kind == "kendra": - request = CreateWorkspaceKendraRequest(**data) - ret_value = _create_workspace_kendra(request, config) - else: - raise genai_core.types.CommonError("Invalid engine") + request = CreateWorkspaceOpenSearchRequest(**input) + ret_value = _create_workspace_open_search(request, config) + return ret_value + + +@router.resolver(field_name="createKendraWorkspace") +@tracer.capture_method +def create_kendra_workspace(input: dict): + config = genai_core.parameters.get_config() - return {"ok": True, "data": ret_value} + request = CreateWorkspaceKendraRequest(**input) + ret_value = _create_workspace_kendra(request, config) + return ret_value def _create_workspace_aurora(request: CreateWorkspaceAuroraRequest, config: dict): @@ -154,7 +161,7 @@ def _create_workspace_aurora(request: CreateWorkspaceAuroraRequest, config: dict if request.metric not in ["inner", "cosine", "l2"]: raise genai_core.types.CommonError("Invalid metric") - if request.chunking_strategy not in ["recursive"]: + if request.chunkingStrategy not in ["recursive"]: raise genai_core.types.CommonError("Invalid chunking strategy") if request.chunkSize < 100 or request.chunkSize > 10000: @@ -163,20 +170,22 @@ def _create_workspace_aurora(request: CreateWorkspaceAuroraRequest, config: dict if request.chunkOverlap < 0 or request.chunkOverlap >= request.chunkSize: raise genai_core.types.CommonError("Invalid chunk overlap") - return genai_core.workspaces.create_workspace_aurora( - workspace_name=workspace_name, - embeddings_model_provider=request.embeddingsModelProvider, - embeddings_model_name=request.embeddingsModelName, - embeddings_model_dimensions=embeddings_model_dimensions, - cross_encoder_model_provider=request.crossEncoderModelProvider, - cross_encoder_model_name=request.crossEncoderModelName, - languages=request.languages, - metric=request.metric, - has_index=request.index, - hybrid_search=request.hybridSearch, - chunking_strategy=request.chunking_strategy, - chunk_size=request.chunkSize, - chunk_overlap=request.chunkOverlap, + return _convert_workspace( + genai_core.workspaces.create_workspace_aurora( + workspace_name=workspace_name, + embeddings_model_provider=request.embeddingsModelProvider, + embeddings_model_name=request.embeddingsModelName, + embeddings_model_dimensions=embeddings_model_dimensions, + cross_encoder_model_provider=request.crossEncoderModelProvider, + cross_encoder_model_name=request.crossEncoderModelName, + languages=request.languages, + metric=request.metric, + has_index=request.index, + hybrid_search=request.hybridSearch, + chunking_strategy=request.chunkingStrategy, + chunk_size=request.chunkSize, + chunk_overlap=request.chunkOverlap, + ) ) @@ -225,7 +234,7 @@ def _create_workspace_open_search( if len(request.languages) == 0 or len(request.languages) > 3: raise genai_core.types.CommonError("Invalid languages") - if request.chunking_strategy not in ["recursive"]: + if request.chunkingStrategy not in ["recursive"]: raise genai_core.types.CommonError("Invalid chunking strategy") if request.chunkSize < 100 or request.chunkSize > 10000: @@ -234,18 +243,20 @@ def _create_workspace_open_search( if request.chunkOverlap < 0 or request.chunkOverlap >= request.chunkSize: raise genai_core.types.CommonError("Invalid chunk overlap") - return genai_core.workspaces.create_workspace_open_search( - workspace_name=workspace_name, - embeddings_model_provider=request.embeddingsModelProvider, - embeddings_model_name=request.embeddingsModelName, - embeddings_model_dimensions=embeddings_model_dimensions, - cross_encoder_model_provider=request.crossEncoderModelProvider, - cross_encoder_model_name=request.crossEncoderModelName, - languages=request.languages, - hybrid_search=request.hybridSearch, - chunking_strategy=request.chunking_strategy, - chunk_size=request.chunkSize, - chunk_overlap=request.chunkOverlap, + return _convert_workspace( + genai_core.workspaces.create_workspace_open_search( + workspace_name=workspace_name, + embeddings_model_provider=request.embeddingsModelProvider, + embeddings_model_name=request.embeddingsModelName, + embeddings_model_dimensions=embeddings_model_dimensions, + cross_encoder_model_provider=request.crossEncoderModelProvider, + cross_encoder_model_name=request.crossEncoderModelName, + languages=request.languages, + hybrid_search=request.hybridSearch, + chunking_strategy=request.chunkingStrategy, + chunk_size=request.chunkSize, + chunk_overlap=request.chunkOverlap, + ) ) @@ -271,10 +282,12 @@ def _create_workspace_kendra(request: CreateWorkspaceKendraRequest, config: dict if kendra_index is None: raise genai_core.types.CommonError("Kendra index not found") - return genai_core.workspaces.create_workspace_kendra( - workspace_name=workspace_name, - kendra_index=kendra_index, - use_all_data=request.useAllData, + return _convert_workspace( + genai_core.workspaces.create_workspace_kendra( + workspace_name=workspace_name, + kendra_index=kendra_index, + use_all_data=request.useAllData, + ) ) @@ -298,8 +311,11 @@ def _convert_workspace(workspace: dict): "chunkingStrategy": workspace.get("chunking_strategy"), "chunkSize": workspace.get("chunk_size"), "chunkOverlap": workspace.get("chunk_overlap"), - "vectors": workspace.get("vectors"), - "documents": workspace.get("documents"), + "vectors": workspace.get("vectors", 0), + "documents": workspace.get("documents", 0), + "aossEngine": workspace.get("aoss_engine"), + "hasIndex": workspace.get("has_index"), + "formatVersion": workspace.get("format_version"), "sizeInBytes": workspace.get("size_in_bytes"), "kendraIndexId": workspace.get("kendra_index_id"), "kendraIndexExternal": kendra_index_external, diff --git a/lib/chatbot-api/functions/authorizer/index.py b/lib/chatbot-api/functions/authorizer/index.py deleted file mode 100644 index 1aae71821..000000000 --- a/lib/chatbot-api/functions/authorizer/index.py +++ /dev/null @@ -1,48 +0,0 @@ -import os - -import boto3 -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.utilities.typing import LambdaContext -from botocore.exceptions import ClientError - -tracer = Tracer() -logger = Logger(log_uncaught_exceptions=True) - -cognito_client = boto3.client("cognito-idp", region_name=os.environ["AWS_REGION"]) - - -@tracer.capture_lambda_handler -@logger.inject_lambda_context(log_event=True) -def handler(event, context: LambdaContext): - connection_id = event["requestContext"]["connectionId"] - logger.set_correlation_id(connection_id) - tracer.put_annotation(key="ConnectionId", value=connection_id) - id_token = event["queryStringParameters"].get("token") - if not id_token: - return generate_policy("Deny", event["methodArn"]) - try: - response = cognito_client.get_user(AccessToken=id_token) - logger.debug(response) - except ClientError as e: - logger.exception(e) - return generate_policy("Deny", event["methodArn"]) - - tracer.put_annotation(key="UserId", value=response["Username"]) - policy = generate_policy("Allow", event["methodArn"], response["Username"]) - policy["context"] = {"username": response["Username"]} - - logger.debug(policy) - return policy - - -def generate_policy(effect, resource, username="username"): - policy = { - "principalId": username, - "policyDocument": { - "Version": "2012-10-17", - "Statement": [ - {"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource} - ], - }, - } - return policy diff --git a/lib/chatbot-api/functions/connection-handler/index.py b/lib/chatbot-api/functions/connection-handler/index.py deleted file mode 100644 index 543cf0574..000000000 --- a/lib/chatbot-api/functions/connection-handler/index.py +++ /dev/null @@ -1,67 +0,0 @@ -import json -import os - -import boto3 -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.utilities.typing import LambdaContext - -tracer = Tracer() -logger = Logger(log_uncaught_exceptions=True) - -dynamodb = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"]) -table = dynamodb.Table(os.environ["CONNECTIONS_TABLE_NAME"]) - - -@tracer.capture_lambda_handler -@logger.inject_lambda_context(log_event=True) -def handler(event, context: LambdaContext): - user_id = event["requestContext"]["authorizer"]["username"] - connection_id = event["requestContext"]["connectionId"] - event_type = event["requestContext"]["eventType"] - logger.set_correlation_id(connection_id) - tracer.put_annotation(key="ConnectionId", value=connection_id) - tracer.put_annotation(key="UserId", value=user_id) - - logger.debug(f"user_id: {user_id}") - logger.debug(f"connection_id: {connection_id}") - logger.debug(f"event_type: {event_type}") - - if event_type == "CONNECT": - logger.info(f"Adding connection {connection_id} for user {user_id}") - table.put_item( - Item={ - "connectionId": connection_id, - "userId": user_id, - } - ) - logger.info(f"Added connection {connection_id} for user {user_id}") - - return { - "statusCode": 200, - "body": json.dumps( - { - "message": event_type, - "userId": user_id, - "connectionId": connection_id, - } - ), - } - - if event_type == "DISCONNECT": - logger.info(f"Removing connection {connection_id} for user {user_id}") - table.delete_item(Key={"connectionId": connection_id}) - logger.info(f"Removed connection {connection_id} for user {user_id}") - return { - "statusCode": 200, - "body": { - "message": event_type, - "connectionId": connection_id, - }, - } - - error_message = f"Unhandled event type {event_type}" - logger.info(error_message) - return { - "statusCode": 400, - "body": json.dumps({"message": error_message, "error": True}), - } diff --git a/lib/chatbot-api/functions/incoming-message-handler/index.py b/lib/chatbot-api/functions/incoming-message-handler/index.py deleted file mode 100644 index 6bf42f428..000000000 --- a/lib/chatbot-api/functions/incoming-message-handler/index.py +++ /dev/null @@ -1,71 +0,0 @@ -import os -import boto3 -import json -from datetime import datetime -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.utilities.typing import LambdaContext - -tracer = Tracer() -logger = Logger(log_uncaught_exceptions=True) -sns = boto3.client("sns", region_name=os.environ["AWS_REGION"]) - -api_gateway_management_api = boto3.client( - "apigatewaymanagementapi", - endpoint_url=os.environ["WEBSOCKET_API_ENDPOINT"], -) - - -def handle_message(connection_id, user_id, body): - action = body["action"] - model_interface = body.get("modelInterface", "langchain") - data = body.get("data", {}) - - return handle_request(connection_id, user_id, action, model_interface, data) - - -def handle_request(connection_id, user_id, action, model_interface, data): - message = { - "action": action, - "modelInterface": model_interface, - "direction": "IN", - "connectionId": connection_id, - "timestamp": str(int(round(datetime.now().timestamp()))), - "userId": user_id, - "data": data, - } - logger.info(message) - response = sns.publish( - TopicArn=os.environ["MESSAGES_TOPIC_ARN"], - Message=json.dumps(message), - ) - - return {"statusCode": 200, "body": json.dumps(response)} - - -@tracer.capture_lambda_handler -@logger.inject_lambda_context(log_event=True) -def handler(event, context: LambdaContext): - event_type = event["requestContext"]["eventType"] - connection_id = event["requestContext"]["connectionId"] - user_id = event["requestContext"]["authorizer"]["username"] - - logger.set_correlation_id(connection_id) - tracer.put_annotation(key="ConnectionId", value=connection_id) - tracer.put_annotation(key="UserId", value=user_id) - - if event_type == "MESSAGE": - message = json.loads(event["body"]) - return handle_message(connection_id, user_id, message) - - return { - "statusCode": 400, - "body": json.dumps({"message": f"Unhandled event type {event_type}"}), - } - - -def send_message(connection_id, message): - print(f"Sending message {message} to {connection_id}") - - api_gateway_management_api.post_to_connection( - ConnectionId=connection_id, Data=json.dumps(message) - ) diff --git a/lib/chatbot-api/functions/outgoing-message-appsync/graphql.ts b/lib/chatbot-api/functions/outgoing-message-appsync/graphql.ts new file mode 100644 index 000000000..8bf5104c4 --- /dev/null +++ b/lib/chatbot-api/functions/outgoing-message-appsync/graphql.ts @@ -0,0 +1,42 @@ +import * as crypto from "@aws-crypto/sha256-js"; +import { defaultProvider } from "@aws-sdk/credential-provider-node"; +import { SignatureV4 } from "@aws-sdk/signature-v4"; +import { HttpRequest } from "@aws-sdk/protocol-http"; + +const { Sha256 } = crypto; +const AWS_REGION = process.env.AWS_REGION || "eu-west-1"; + +const endpoint = new URL(process.env.GRAPHQL_ENDPOINT ?? ""); + +export const graphQlQuery = async (query: string) => { + const signer = new SignatureV4({ + credentials: defaultProvider(), + region: AWS_REGION, + service: "appsync", + sha256: Sha256, + }); + + const requestToBeSigned = new HttpRequest({ + method: "POST", + headers: { + "Content-Type": "application/json", + host: endpoint.host, + }, + hostname: endpoint.host, + body: JSON.stringify({ query }), + path: endpoint.pathname, + }); + + const signed = await signer.sign(requestToBeSigned); + const request = new Request(endpoint, signed); + + let body; + + try { + const response = await fetch(request); + body = await response.json(); + } catch (error) { + throw error; + } + return body; +}; diff --git a/lib/chatbot-api/functions/outgoing-message-appsync/index.ts b/lib/chatbot-api/functions/outgoing-message-appsync/index.ts new file mode 100644 index 000000000..f901a1e46 --- /dev/null +++ b/lib/chatbot-api/functions/outgoing-message-appsync/index.ts @@ -0,0 +1,58 @@ +import { + BatchProcessor, + EventType, + processPartialResponse, +} from "@aws-lambda-powertools/batch"; +import { Logger } from "@aws-lambda-powertools/logger"; +import type { + SQSEvent, + SQSRecord, + Context, + SQSBatchResponse, +} from "aws-lambda"; +import { graphQlQuery } from "./graphql"; + +const processor = new BatchProcessor(EventType.SQS); +const logger = new Logger(); + +const recordHandler = async (record: SQSRecord): Promise => { + const payload = record.body; + if (payload) { + const item = JSON.parse(payload); + logger.info("Processed item", { item }); + const req = JSON.parse(item.Message); + /*** + * Payload format + * + payload: str = record.body + message: dict = json.loads(payload) + detail: dict = json.loads(message["Message"]) + logger.info(detail) + user_id = detail["userId"] + */ + + const query = /* GraphQL */ ` + mutation Mutation { + publishResponse (data: ${JSON.stringify(item.Message)}, sessionId: "${ + req.data.sessionId + }", userId: "${req.userId}") { + data + sessionId + userId + } + } + `; + logger.info(query); + const resp = await graphQlQuery(query); + logger.info(resp); + } +}; + +export const handler = async ( + event: SQSEvent, + context: Context +): Promise => { + return processPartialResponse(event, recordHandler, processor, { + context, + }); +}; diff --git a/lib/chatbot-api/functions/outgoing-message-handler/index.py b/lib/chatbot-api/functions/outgoing-message-handler/index.py deleted file mode 100644 index 0b96db090..000000000 --- a/lib/chatbot-api/functions/outgoing-message-handler/index.py +++ /dev/null @@ -1,55 +0,0 @@ -import json -import os - -import boto3 -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.utilities.batch import BatchProcessor, EventType -from aws_lambda_powertools.utilities.batch.exceptions import BatchProcessingError -from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord -from aws_lambda_powertools.utilities.typing import LambdaContext - -dynamodb = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"]) -table = dynamodb.Table(os.environ["CONNECTIONS_TABLE_NAME"]) - -processor = BatchProcessor(event_type=EventType.SQS) -tracer = Tracer() -logger = Logger() - -api_gateway_management_api = boto3.client( - "apigatewaymanagementapi", - endpoint_url=os.environ["WEBSOCKET_API_ENDPOINT"], -) - - -@tracer.capture_method -def record_handler(record: SQSRecord): - payload: str = record.body - message: dict = json.loads(payload) - detail: dict = json.loads(message["Message"]) - logger.info(detail) - user_id = detail["userId"] - connection_id = detail["connectionId"] - - # get current connectionIds - try: - api_gateway_management_api.post_to_connection( - ConnectionId=connection_id, Data=json.dumps(detail) - ) - except Exception as e: - logger.info( - f"Exception while sending message to connection {connection_id} for user {user_id}: {e}" - ) - - -@logger.inject_lambda_context(log_event=True) -@tracer.capture_lambda_handler -def handler(event, context: LambdaContext): - batch = event["Records"] - try: - with processor(records=batch, handler=record_handler): - processed_messages = processor.process() - except BatchProcessingError as e: - logger.error(e) - - logger.info(processed_messages) - return processor.response() diff --git a/lib/chatbot-api/functions/resolvers/lambda-resolver.js b/lib/chatbot-api/functions/resolvers/lambda-resolver.js new file mode 100644 index 000000000..6be7fda16 --- /dev/null +++ b/lib/chatbot-api/functions/resolvers/lambda-resolver.js @@ -0,0 +1,22 @@ +import { util } from "@aws-appsync/utils"; + +export function request(ctx) { + const { source, args } = ctx; + return { + operation: "Invoke", + payload: { + fieldName: ctx.info.fieldName, + arguments: args, + identity: ctx.identity, + source, + }, + }; +} + +export function response(ctx) { + const { result, error } = ctx; + if (error) { + util.error(error.message, error.type, result); + } + return result; +} diff --git a/lib/chatbot-api/functions/resolvers/publish-response-resolver.js b/lib/chatbot-api/functions/resolvers/publish-response-resolver.js new file mode 100644 index 000000000..1bef703f3 --- /dev/null +++ b/lib/chatbot-api/functions/resolvers/publish-response-resolver.js @@ -0,0 +1,15 @@ +import { util } from "@aws-appsync/utils"; + +export function request(ctx) { + return { + payload: { + data: ctx.arguments.data, + sessionId: ctx.arguments.sessionId, + userId: ctx.arguments.userId, + }, + }; +} + +export function response(ctx) { + return ctx.result; +} diff --git a/lib/chatbot-api/functions/resolvers/send-query-lambda-resolver/index.py b/lib/chatbot-api/functions/resolvers/send-query-lambda-resolver/index.py new file mode 100644 index 000000000..4151f1453 --- /dev/null +++ b/lib/chatbot-api/functions/resolvers/send-query-lambda-resolver/index.py @@ -0,0 +1,35 @@ +import boto3 +import os +import json +from datetime import datetime +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger(log_uncaught_exceptions=True) + +sns = boto3.client("sns") +TOPIC_ARN=os.environ.get("SNS_TOPIC_ARN", "") + +@tracer.capture_lambda_handler +@logger.inject_lambda_context(log_event=True) +def handler(event, context: LambdaContext): + print(event["arguments"]["data"]) + print(event["identity"]) + request = json.loads(event["arguments"]["data"]) + message = { + "action": request["action"], + "modelInterface": request["modelInterface"], + "direction": "IN", + "timestamp": str(int(round(datetime.now().timestamp()))), + "userId": event["identity"]["sub"], + "data": request.get("data", {}), + } + print(message) + + response = sns.publish( + TopicArn=TOPIC_ARN, Message=json.dumps(message) + ) + + return response + \ No newline at end of file diff --git a/lib/chatbot-api/functions/resolvers/subscribe-resolver.js b/lib/chatbot-api/functions/resolvers/subscribe-resolver.js new file mode 100644 index 000000000..dcb10309c --- /dev/null +++ b/lib/chatbot-api/functions/resolvers/subscribe-resolver.js @@ -0,0 +1,13 @@ +import { util, extensions } from "@aws-appsync/utils"; + +export function request(ctx) { + return { + payload: null, + }; +} + +export function response(ctx) { + const filter = { and: [{ userId: { eq: ctx.identity.sub } }] }; + extensions.setSubscriptionFilter(util.transform.toSubscriptionFilter(filter)); + return null; +} diff --git a/lib/chatbot-api/index.ts b/lib/chatbot-api/index.ts index f1dda76ed..cc5a700fd 100644 --- a/lib/chatbot-api/index.ts +++ b/lib/chatbot-api/index.ts @@ -1,18 +1,21 @@ -import * as apigwv2 from "@aws-cdk/aws-apigatewayv2-alpha"; -import * as apigateway from "aws-cdk-lib/aws-apigateway"; import * as cognito from "aws-cdk-lib/aws-cognito"; import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as sns from "aws-cdk-lib/aws-sns"; import * as ssm from "aws-cdk-lib/aws-ssm"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as cdk from "aws-cdk-lib"; +import * as path from "path"; import { Construct } from "constructs"; import { RagEngines } from "../rag-engines"; import { Shared } from "../shared"; import { SageMakerModelEndpoint, SystemConfig } from "../shared/types"; import { ChatBotDynamoDBTables } from "./chatbot-dynamodb-tables"; import { ChatBotS3Buckets } from "./chatbot-s3-buckets"; -import { RestApi } from "./rest-api"; -import { WebSocketApi } from "./websocket-api"; +import { ApiResolvers } from "./rest-api"; +import { RealtimeGraphqlApiBackend } from "./websocket-api"; +import * as appsync from "aws-cdk-lib/aws-appsync"; +import { RetentionDays } from "aws-cdk-lib/aws-logs"; export interface ChatBotApiProps { readonly shared: Shared; @@ -24,12 +27,11 @@ export interface ChatBotApiProps { } export class ChatBotApi extends Construct { - public readonly restApi: apigateway.RestApi; - public readonly webSocketApi: apigwv2.WebSocketApi; public readonly messagesTopic: sns.Topic; public readonly sessionsTable: dynamodb.Table; public readonly byUserIdIndex: string; public readonly filesBucket: s3.Bucket; + public readonly graphqlApi: appsync.GraphqlApi; constructor(scope: Construct, id: string, props: ChatBotApiProps) { super(scope, id); @@ -37,19 +39,80 @@ export class ChatBotApi extends Construct { const chatTables = new ChatBotDynamoDBTables(this, "ChatDynamoDBTables"); const chatBuckets = new ChatBotS3Buckets(this, "ChatBuckets"); - const restApi = new RestApi(this, "RestApi", { + const loggingRole = new iam.Role(this, "apiLoggingRole", { + assumedBy: new iam.ServicePrincipal("appsync.amazonaws.com"), + inlinePolicies: { + loggingPolicy: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["logs:*"], + resources: ["*"], + }), + ], + }), + }, + }); + + const api = new appsync.GraphqlApi(this, "ChatbotApi", { + name: "ChatbotGraphqlApi", + definition: appsync.Definition.fromFile( + path.join(__dirname, "schema/schema.graphql") + ), + authorizationConfig: { + additionalAuthorizationModes: [ + { + authorizationType: appsync.AuthorizationType.IAM, + }, + { + authorizationType: appsync.AuthorizationType.USER_POOL, + userPoolConfig: { + userPool: props.userPool, + }, + }, + ], + }, + logConfig: { + fieldLogLevel: appsync.FieldLogLevel.ALL, + retention: RetentionDays.ONE_WEEK, + role: loggingRole, + }, + xrayEnabled: true, + }); + + new ApiResolvers(this, "RestApi", { ...props, sessionsTable: chatTables.sessionsTable, byUserIdIndex: chatTables.byUserIdIndex, + api, + }); + + const realtimeBackend = new RealtimeGraphqlApiBackend(this, "Realtime", { + ...props, + api, + }); + + realtimeBackend.resolvers.outgoingMessageHandler.addEnvironment( + "GRAPHQL_ENDPOINT", + api.graphqlUrl + ); + + api.grantMutation(realtimeBackend.resolvers.outgoingMessageHandler); + + // Prints out URL + new cdk.CfnOutput(this, "GraphqlAPIURL", { + value: api.graphqlUrl, }); - const webSocketApi = new WebSocketApi(this, "WebSocketApi", props); + // Prints out the AppSync GraphQL API key to the terminal + new cdk.CfnOutput(this, "Graphql-apiId", { + value: api.apiId || "", + }); - this.restApi = restApi.api; - this.webSocketApi = webSocketApi.api; - this.messagesTopic = webSocketApi.messagesTopic; + this.messagesTopic = realtimeBackend.messagesTopic; this.sessionsTable = chatTables.sessionsTable; this.byUserIdIndex = chatTables.byUserIdIndex; this.filesBucket = chatBuckets.filesBucket; + this.graphqlApi = api; } } diff --git a/lib/chatbot-api/rest-api.ts b/lib/chatbot-api/rest-api.ts index 9bc4b2744..6a984d92b 100644 --- a/lib/chatbot-api/rest-api.ts +++ b/lib/chatbot-api/rest-api.ts @@ -12,8 +12,11 @@ import * as lambda from "aws-cdk-lib/aws-lambda"; import * as logs from "aws-cdk-lib/aws-logs"; import * as ssm from "aws-cdk-lib/aws-ssm"; import { Shared } from "../shared"; +import * as appsync from "aws-cdk-lib/aws-appsync"; +import { parse } from "graphql"; +import { readFileSync } from "fs"; -export interface RestApiProps { +export interface ApiResolversProps { readonly shared: Shared; readonly config: SystemConfig; readonly ragEngines?: RagEngines; @@ -22,302 +25,302 @@ export interface RestApiProps { readonly byUserIdIndex: string; readonly modelsParameter: ssm.StringParameter; readonly models: SageMakerModelEndpoint[]; + readonly api: appsync.GraphqlApi; } -export class RestApi extends Construct { - public readonly api: apigateway.RestApi; - - constructor(scope: Construct, id: string, props: RestApiProps) { +export class ApiResolvers extends Construct { + constructor(scope: Construct, id: string, props: ApiResolversProps) { super(scope, id); const apiSecurityGroup = new ec2.SecurityGroup(this, "ApiSecurityGroup", { vpc: props.shared.vpc, }); - const apiHandler = new lambda.Function(this, "ApiHandler", { - code: props.shared.sharedCode.bundleWithLambdaAsset( - path.join(__dirname, "./functions/api-handler") - ), - handler: "index.handler", - runtime: props.shared.pythonRuntime, - architecture: props.shared.lambdaArchitecture, - timeout: cdk.Duration.minutes(10), - memorySize: 512, - tracing: lambda.Tracing.ACTIVE, - logRetention: logs.RetentionDays.ONE_WEEK, - layers: [props.shared.powerToolsLayer, props.shared.commonLayer], - vpc: props.shared.vpc, - securityGroups: [apiSecurityGroup], - vpcSubnets: props.shared.vpc.privateSubnets as ec2.SubnetSelection, - environment: { - ...props.shared.defaultEnvironmentVariables, - CONFIG_PARAMETER_NAME: props.shared.configParameter.parameterName, - MODELS_PARAMETER_NAME: props.modelsParameter.parameterName, - X_ORIGIN_VERIFY_SECRET_ARN: props.shared.xOriginVerifySecret.secretArn, - API_KEYS_SECRETS_ARN: props.shared.apiKeysSecret.secretArn, - SESSIONS_TABLE_NAME: props.sessionsTable.tableName, - SESSIONS_BY_USER_ID_INDEX_NAME: props.byUserIdIndex, - UPLOAD_BUCKET_NAME: props.ragEngines?.uploadBucket?.bucketName ?? "", - PROCESSING_BUCKET_NAME: - props.ragEngines?.processingBucket?.bucketName ?? "", - AURORA_DB_SECRET_ID: props.ragEngines?.auroraPgVector?.database?.secret - ?.secretArn as string, - WORKSPACES_TABLE_NAME: - props.ragEngines?.workspacesTable.tableName ?? "", - WORKSPACES_BY_OBJECT_TYPE_INDEX_NAME: - props.ragEngines?.workspacesByObjectTypeIndexName ?? "", - DOCUMENTS_TABLE_NAME: props.ragEngines?.documentsTable.tableName ?? "", - DOCUMENTS_BY_COMPOUND_KEY_INDEX_NAME: - props.ragEngines?.documentsByCompountKeyIndexName ?? "", - DOCUMENTS_BY_STATUS_INDEX: - props.ragEngines?.documentsByStatusIndexName ?? "", - SAGEMAKER_RAG_MODELS_ENDPOINT: - props.ragEngines?.sageMakerRagModels?.model.endpoint - ?.attrEndpointName ?? "", - DELETE_WORKSPACE_WORKFLOW_ARN: - props.ragEngines?.deleteWorkspaceWorkflow?.stateMachineArn ?? "", - CREATE_AURORA_WORKSPACE_WORKFLOW_ARN: - props.ragEngines?.auroraPgVector?.createAuroraWorkspaceWorkflow - ?.stateMachineArn ?? "", - CREATE_OPEN_SEARCH_WORKSPACE_WORKFLOW_ARN: - props.ragEngines?.openSearchVector?.createOpenSearchWorkspaceWorkflow - ?.stateMachineArn ?? "", - CREATE_KENDRA_WORKSPACE_WORKFLOW_ARN: - props.ragEngines?.kendraRetrieval?.createKendraWorkspaceWorkflow - ?.stateMachineArn ?? "", - FILE_IMPORT_WORKFLOW_ARN: - props.ragEngines?.fileImportWorkflow?.stateMachineArn ?? "", - WEBSITE_CRAWLING_WORKFLOW_ARN: - props.ragEngines?.websiteCrawlingWorkflow?.stateMachineArn ?? "", - OPEN_SEARCH_COLLECTION_ENDPOINT: - props.ragEngines?.openSearchVector?.openSearchCollectionEndpoint ?? - "", - DEFAULT_KENDRA_INDEX_ID: - props.ragEngines?.kendraRetrieval?.kendraIndex?.attrId ?? "", - DEFAULT_KENDRA_INDEX_NAME: - props.ragEngines?.kendraRetrieval?.kendraIndex?.name ?? "", - DEFAULT_KENDRA_S3_DATA_SOURCE_ID: - props.ragEngines?.kendraRetrieval?.kendraS3DataSource?.attrId ?? "", - DEFAULT_KENDRA_S3_DATA_SOURCE_BUCKET_NAME: - props.ragEngines?.kendraRetrieval?.kendraS3DataSourceBucket - ?.bucketName ?? "", - RSS_FEED_INGESTOR_FUNCTION: - props.ragEngines?.dataImport.rssIngestorFunction?.functionArn ?? "", - }, - }); - - if (props.ragEngines?.workspacesTable) { - props.ragEngines.workspacesTable.grantReadWriteData(apiHandler); - } - - if (props.ragEngines?.documentsTable) { - props.ragEngines.documentsTable.grantReadWriteData(apiHandler); - props.ragEngines?.dataImport.rssIngestorFunction?.grantInvoke(apiHandler); - } - - if (props.ragEngines?.auroraPgVector) { - props.ragEngines.auroraPgVector.database.secret?.grantRead(apiHandler); - props.ragEngines.auroraPgVector.database.connections.allowDefaultPortFrom( - apiHandler - ); - - props.ragEngines.auroraPgVector.createAuroraWorkspaceWorkflow.grantStartExecution( - apiHandler - ); - } - - if (props.ragEngines?.openSearchVector) { - apiHandler.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["aoss:APIAccessAll"], - resources: [ - props.ragEngines?.openSearchVector.openSearchCollection.attrArn, - ], - }) - ); + const appSyncLambdaResolver = new lambda.Function( + this, + "GraphQLApiHandler", + { + code: props.shared.sharedCode.bundleWithLambdaAsset( + path.join(__dirname, "./functions/api-handler") + ), + handler: "index.handler", + runtime: props.shared.pythonRuntime, + architecture: props.shared.lambdaArchitecture, + timeout: cdk.Duration.minutes(10), + memorySize: 512, + tracing: lambda.Tracing.ACTIVE, + logRetention: logs.RetentionDays.ONE_WEEK, + layers: [props.shared.powerToolsLayer, props.shared.commonLayer], + vpc: props.shared.vpc, + securityGroups: [apiSecurityGroup], + vpcSubnets: props.shared.vpc.privateSubnets as ec2.SubnetSelection, + environment: { + ...props.shared.defaultEnvironmentVariables, + CONFIG_PARAMETER_NAME: props.shared.configParameter.parameterName, + MODELS_PARAMETER_NAME: props.modelsParameter.parameterName, + X_ORIGIN_VERIFY_SECRET_ARN: + props.shared.xOriginVerifySecret.secretArn, + API_KEYS_SECRETS_ARN: props.shared.apiKeysSecret.secretArn, + SESSIONS_TABLE_NAME: props.sessionsTable.tableName, + SESSIONS_BY_USER_ID_INDEX_NAME: props.byUserIdIndex, + UPLOAD_BUCKET_NAME: props.ragEngines?.uploadBucket?.bucketName ?? "", + PROCESSING_BUCKET_NAME: + props.ragEngines?.processingBucket?.bucketName ?? "", + AURORA_DB_SECRET_ID: props.ragEngines?.auroraPgVector?.database + ?.secret?.secretArn as string, + WORKSPACES_TABLE_NAME: + props.ragEngines?.workspacesTable.tableName ?? "", + WORKSPACES_BY_OBJECT_TYPE_INDEX_NAME: + props.ragEngines?.workspacesByObjectTypeIndexName ?? "", + DOCUMENTS_TABLE_NAME: + props.ragEngines?.documentsTable.tableName ?? "", + DOCUMENTS_BY_COMPOUND_KEY_INDEX_NAME: + props.ragEngines?.documentsByCompountKeyIndexName ?? "", + DOCUMENTS_BY_STATUS_INDEX: + props.ragEngines?.documentsByStatusIndexName ?? "", + SAGEMAKER_RAG_MODELS_ENDPOINT: + props.ragEngines?.sageMakerRagModels?.model.endpoint + ?.attrEndpointName ?? "", + DELETE_WORKSPACE_WORKFLOW_ARN: + props.ragEngines?.deleteWorkspaceWorkflow?.stateMachineArn ?? "", + CREATE_AURORA_WORKSPACE_WORKFLOW_ARN: + props.ragEngines?.auroraPgVector?.createAuroraWorkspaceWorkflow + ?.stateMachineArn ?? "", + CREATE_OPEN_SEARCH_WORKSPACE_WORKFLOW_ARN: + props.ragEngines?.openSearchVector + ?.createOpenSearchWorkspaceWorkflow?.stateMachineArn ?? "", + CREATE_KENDRA_WORKSPACE_WORKFLOW_ARN: + props.ragEngines?.kendraRetrieval?.createKendraWorkspaceWorkflow + ?.stateMachineArn ?? "", + FILE_IMPORT_WORKFLOW_ARN: + props.ragEngines?.fileImportWorkflow?.stateMachineArn ?? "", + WEBSITE_CRAWLING_WORKFLOW_ARN: + props.ragEngines?.websiteCrawlingWorkflow?.stateMachineArn ?? "", + OPEN_SEARCH_COLLECTION_ENDPOINT: + props.ragEngines?.openSearchVector?.openSearchCollectionEndpoint ?? + "", + DEFAULT_KENDRA_INDEX_ID: + props.ragEngines?.kendraRetrieval?.kendraIndex?.attrId ?? "", + DEFAULT_KENDRA_INDEX_NAME: + props.ragEngines?.kendraRetrieval?.kendraIndex?.name ?? "", + DEFAULT_KENDRA_S3_DATA_SOURCE_ID: + props.ragEngines?.kendraRetrieval?.kendraS3DataSource?.attrId ?? "", + DEFAULT_KENDRA_S3_DATA_SOURCE_BUCKET_NAME: + props.ragEngines?.kendraRetrieval?.kendraS3DataSourceBucket + ?.bucketName ?? "", + RSS_FEED_INGESTOR_FUNCTION: + props.ragEngines?.dataImport.rssIngestorFunction?.functionArn ?? "", + }, + } + ); - props.ragEngines.openSearchVector.addToAccessPolicy( - "rest-api", - [apiHandler.role?.roleArn], - ["aoss:DescribeIndex", "aoss:ReadDocument", "aoss:WriteDocument"] - ); + function addPermissions(apiHandler: lambda.Function) { + if (props.ragEngines?.workspacesTable) { + props.ragEngines.workspacesTable.grantReadWriteData(apiHandler); + } - props.ragEngines.openSearchVector.createOpenSearchWorkspaceWorkflow.grantStartExecution( - apiHandler - ); - } + if (props.ragEngines?.documentsTable) { + props.ragEngines.documentsTable.grantReadWriteData(apiHandler); + props.ragEngines?.dataImport.rssIngestorFunction?.grantInvoke( + apiHandler + ); + } - if (props.ragEngines?.kendraRetrieval) { - props.ragEngines.kendraRetrieval.createKendraWorkspaceWorkflow.grantStartExecution( - apiHandler - ); + if (props.ragEngines?.auroraPgVector) { + props.ragEngines.auroraPgVector.database.secret?.grantRead(apiHandler); + props.ragEngines.auroraPgVector.database.connections.allowDefaultPortFrom( + apiHandler + ); - props.ragEngines?.kendraRetrieval?.kendraS3DataSourceBucket?.grantReadWrite( - apiHandler - ); + props.ragEngines.auroraPgVector.createAuroraWorkspaceWorkflow.grantStartExecution( + apiHandler + ); + } - if (props.ragEngines.kendraRetrieval.kendraIndex) { + if (props.ragEngines?.openSearchVector) { apiHandler.addToRolePolicy( new iam.PolicyStatement({ - actions: [ - "kendra:Retrieve", - "kendra:Query", - "kendra:BatchDeleteDocument", - "kendra:BatchPutDocument", - "kendra:StartDataSourceSyncJob", - "kendra:DescribeDataSourceSyncJob", - "kendra:StopDataSourceSyncJob", - "kendra:ListDataSourceSyncJobs", - "kendra:ListDataSources", - "kendra:DescribeIndex", - ], + actions: ["aoss:APIAccessAll"], resources: [ - props.ragEngines.kendraRetrieval.kendraIndex.attrArn, - `${props.ragEngines.kendraRetrieval.kendraIndex.attrArn}/*`, + props.ragEngines?.openSearchVector.openSearchCollection.attrArn, ], }) ); + + props.ragEngines.openSearchVector.createOpenSearchWorkspaceWorkflow.grantStartExecution( + apiHandler + ); } - for (const item of props.config.rag.engines.kendra.external || []) { - if (item.roleArn) { - apiHandler.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["sts:AssumeRole"], - resources: [item.roleArn], - }) - ); - } else { + if (props.ragEngines?.kendraRetrieval) { + props.ragEngines.kendraRetrieval.createKendraWorkspaceWorkflow.grantStartExecution( + apiHandler + ); + + props.ragEngines?.kendraRetrieval?.kendraS3DataSourceBucket?.grantReadWrite( + apiHandler + ); + + if (props.ragEngines.kendraRetrieval.kendraIndex) { apiHandler.addToRolePolicy( new iam.PolicyStatement({ - actions: ["kendra:Retrieve", "kendra:Query"], + actions: [ + "kendra:Retrieve", + "kendra:Query", + "kendra:BatchDeleteDocument", + "kendra:BatchPutDocument", + "kendra:StartDataSourceSyncJob", + "kendra:DescribeDataSourceSyncJob", + "kendra:StopDataSourceSyncJob", + "kendra:ListDataSourceSyncJobs", + "kendra:ListDataSources", + "kendra:DescribeIndex", + ], resources: [ - `arn:${cdk.Aws.PARTITION}:kendra:${ - item.region ?? cdk.Aws.REGION - }:${cdk.Aws.ACCOUNT_ID}:index/${item.kendraId}`, + props.ragEngines.kendraRetrieval.kendraIndex.attrArn, + `${props.ragEngines.kendraRetrieval.kendraIndex.attrArn}/*`, ], }) ); } - } - } - if (props.ragEngines?.fileImportWorkflow) { - props.ragEngines.fileImportWorkflow.grantStartExecution(apiHandler); - } - - if (props.ragEngines?.websiteCrawlingWorkflow) { - props.ragEngines.websiteCrawlingWorkflow.grantStartExecution(apiHandler); - } + for (const item of props.config.rag.engines.kendra.external ?? []) { + if (item.roleArn) { + apiHandler.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["sts:AssumeRole"], + resources: [item.roleArn], + }) + ); + } else { + apiHandler.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["kendra:Retrieve", "kendra:Query"], + resources: [ + `arn:${cdk.Aws.PARTITION}:kendra:${ + item.region ?? cdk.Aws.REGION + }:${cdk.Aws.ACCOUNT_ID}:index/${item.kendraId}`, + ], + }) + ); + } + } + } - if (props.ragEngines?.deleteWorkspaceWorkflow) { - props.ragEngines.deleteWorkspaceWorkflow.grantStartExecution(apiHandler); - } + if (props.ragEngines?.fileImportWorkflow) { + props.ragEngines.fileImportWorkflow.grantStartExecution(apiHandler); + } - if (props.ragEngines?.sageMakerRagModels) { - apiHandler.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["sagemaker:InvokeEndpoint"], - resources: [props.ragEngines.sageMakerRagModels.model.endpoint.ref], - }) - ); - } + if (props.ragEngines?.websiteCrawlingWorkflow) { + props.ragEngines.websiteCrawlingWorkflow.grantStartExecution( + apiHandler + ); + } - for (const model of props.models) { - apiHandler.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["sagemaker:InvokeEndpoint"], - resources: [model.endpoint.ref], - }) - ); - } + if (props.ragEngines?.deleteWorkspaceWorkflow) { + props.ragEngines.deleteWorkspaceWorkflow.grantStartExecution( + apiHandler + ); + } - apiHandler.addToRolePolicy( - new iam.PolicyStatement({ - actions: [ - "comprehend:DetectDominantLanguage", - "comprehend:DetectSentiment", - ], - resources: ["*"], - }) - ); + if (props.ragEngines?.sageMakerRagModels) { + apiHandler.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["sagemaker:InvokeEndpoint"], + resources: [props.ragEngines.sageMakerRagModels.model.endpoint.ref], + }) + ); + } - props.shared.xOriginVerifySecret.grantRead(apiHandler); - props.shared.apiKeysSecret.grantRead(apiHandler); - props.shared.configParameter.grantRead(apiHandler); - props.modelsParameter.grantRead(apiHandler); - props.sessionsTable.grantReadWriteData(apiHandler); - props.ragEngines?.uploadBucket.grantReadWrite(apiHandler); - props.ragEngines?.processingBucket.grantReadWrite(apiHandler); + for (const model of props.models) { + apiHandler.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["sagemaker:InvokeEndpoint"], + resources: [model.endpoint.ref], + }) + ); + } - if (props.config.bedrock?.enabled) { apiHandler.addToRolePolicy( new iam.PolicyStatement({ actions: [ - "bedrock:ListFoundationModels", - "bedrock:ListCustomModels", - "bedrock:InvokeModel", - "bedrock:InvokeModelWithResponseStream", + "comprehend:DetectDominantLanguage", + "comprehend:DetectSentiment", ], resources: ["*"], }) ); - if (props.config.bedrock?.roleArn) { + props.shared.xOriginVerifySecret.grantRead(apiHandler); + props.shared.apiKeysSecret.grantRead(apiHandler); + props.shared.configParameter.grantRead(apiHandler); + props.modelsParameter.grantRead(apiHandler); + props.sessionsTable.grantReadWriteData(apiHandler); + props.ragEngines?.uploadBucket.grantReadWrite(apiHandler); + props.ragEngines?.processingBucket.grantReadWrite(apiHandler); + + if (props.config.bedrock?.enabled) { apiHandler.addToRolePolicy( new iam.PolicyStatement({ - actions: ["sts:AssumeRole"], - resources: [props.config.bedrock.roleArn], + actions: [ + "bedrock:ListFoundationModels", + "bedrock:ListCustomModels", + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + ], + resources: ["*"], }) ); + + if (props.config.bedrock?.roleArn) { + apiHandler.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["sts:AssumeRole"], + resources: [props.config.bedrock.roleArn], + }) + ); + } } } - const chatBotApi = new apigateway.RestApi(this, "ChatBotApi", { - endpointTypes: [apigateway.EndpointType.REGIONAL], - cloudWatchRole: true, - defaultCorsPreflightOptions: { - allowOrigins: apigateway.Cors.ALL_ORIGINS, - allowMethods: apigateway.Cors.ALL_METHODS, - allowHeaders: ["Content-Type", "Authorization", "X-Amz-Date"], - maxAge: cdk.Duration.minutes(10), - }, - deploy: true, - deployOptions: { - stageName: "api", - loggingLevel: apigateway.MethodLoggingLevel.INFO, - tracingEnabled: true, - metricsEnabled: true, - throttlingRateLimit: 2500, - }, - }); + addPermissions(appSyncLambdaResolver); - const cognitoAuthorizer = new apigateway.CfnAuthorizer( - this, - "ApiGatewayCognitoAuthorizer", - { - name: "CognitoAuthorizer", - identitySource: "method.request.header.Authorization", - providerArns: [props.userPool.userPoolArn], - restApiId: chatBotApi.restApiId, - type: apigateway.AuthorizationType.COGNITO, - } + props.ragEngines?.openSearchVector?.addToAccessPolicy( + "graphql-api", + [appSyncLambdaResolver.role?.roleArn], + ["aoss:DescribeIndex", "aoss:ReadDocument", "aoss:WriteDocument"] ); - const v1Resource = chatBotApi.root.addResource("v1", { - defaultMethodOptions: { - authorizationType: apigateway.AuthorizationType.COGNITO, - authorizer: { authorizerId: cognitoAuthorizer.ref }, - }, - }); - const v1ProxyResource = v1Resource.addResource("{proxy+}"); - v1ProxyResource.addMethod( - "ANY", - new apigateway.LambdaIntegration(apiHandler, { - proxy: true, - }) + const functionDataSource = props.api.addLambdaDataSource( + "proxyResolverFunction", + appSyncLambdaResolver + ); + + const schema = parse( + readFileSync("lib/chatbot-api/schema/schema.graphql", "utf8") ); - this.api = chatBotApi; + function addResolvers(operationType: string) { + const fieldNames = ( + schema.definitions + .filter((x) => x.kind == "ObjectTypeDefinition") + .filter((y: any) => y.name.value == operationType)[0] as any + ).fields.map((z: any) => z.name.value); + + for (const fieldName of fieldNames) { + // These resolvers are added by the Realtime API + if (fieldName == "sendQuery" || fieldName == "publishResponse") { + continue; + } + props.api.createResolver(`${fieldName}-resolver`, { + typeName: operationType, + fieldName: fieldName, + dataSource: functionDataSource, + }); + } + } + + addResolvers("Query"); + addResolvers("Mutation"); } } diff --git a/lib/chatbot-api/schema/schema.graphql b/lib/chatbot-api/schema/schema.graphql new file mode 100644 index 000000000..bb10fd60d --- /dev/null +++ b/lib/chatbot-api/schema/schema.graphql @@ -0,0 +1,351 @@ +# Workspaces + +input CreateWorkspaceAuroraInput { + name: String! + kind: String! + embeddingsModelProvider: String! + embeddingsModelName: String! + crossEncoderModelProvider: String! + crossEncoderModelName: String! + languages: [String!]! + metric: String! + index: Boolean! + hybridSearch: Boolean! + chunkingStrategy: String! + chunkSize: Int! + chunkOverlap: Int! +} + +input CreateWorkspaceKendraInput { + name: String! + kind: String! + kendraIndexId: String! + useAllData: Boolean! +} + +input CreateWorkspaceOpenSearchInput { + name: String! + kind: String! + embeddingsModelProvider: String! + embeddingsModelName: String! + crossEncoderModelProvider: String! + crossEncoderModelName: String! + languages: [String!]! + hybridSearch: Boolean! + chunkingStrategy: String! + chunkSize: Int! + chunkOverlap: Int! +} + +input CalculateEmbeddingsInput { + provider: String! + model: String! + passages: [String]! +} + +type CrawlerProperties @aws_cognito_user_pools { + followLinks: Boolean + limit: Int +} + +type CrossEncoderData @aws_cognito_user_pools { + provider: String! + name: String! + default: Boolean! +} + +type DeleteSessionResult @aws_cognito_user_pools { + id: String + deleted: Boolean! +} + +type DetectedLanguage @aws_cognito_user_pools { + code: String! + score: Float! +} + +type Document @aws_cognito_user_pools { + workspaceId: String! + id: String! + type: String! + subType: String + status: String + title: String + path: String + sizeInBytes: Int + vectors: Int + subDocuments: Int + crawlerProperties: CrawlerProperties + errors: [String!] + createdAt: AWSDateTime! + updatedAt: AWSDateTime + rssFeedId: String + rssLastCheckedAt: AWSDateTime +} + +type DocumentResult @aws_cognito_user_pools { + workspaceId: String! + documentId: String! + status: String +} + +input DocumentSubscriptionStatusInput { + workspaceId: String! + documentId: String! + status: String! +} + +type DocumentsResult @aws_cognito_user_pools { + items: [Document]! + lastDocumentId: String +} + +type Embedding @aws_cognito_user_pools { + passage: String + vector: [Float!]! +} + +type EmbeddingModel @aws_cognito_user_pools { + provider: String! + name: String! + dimensions: Int! + default: Boolean +} + +input FileUploadInput { + workspaceId: String! + fileName: String! +} + +type FileUploadResult @aws_cognito_user_pools { + url: String! + fields: String +} + +input GetDocumentInput { + workspaceId: String! + documentId: String! +} + +input GetRSSPostsInput { + workspaceId: String! + documentId: String! + lastDocumentId: String +} + +type KendraIndex @aws_cognito_user_pools { + id: String! + name: String! + external: Boolean! +} + +input ListDocumentsInput { + workspaceId: String! + documentType: String! + lastDocumentId: String +} + +type Model @aws_cognito_user_pools { + name: String! + provider: String! + interface: String! + ragSupported: Boolean! + inputModalities: [String!]! + outputModalities: [String!]! + streaming: Boolean! +} + +type PassageRank @aws_cognito_user_pools { + score: Float! + passage: String! +} + +input QnADocumentInput { + workspaceId: String! + question: String! + answer: String! +} + +type RagEngine @aws_cognito_user_pools { + id: String! + name: String! + enabled: Boolean! +} + +input RankPassagesInput { + provider: String! + model: String! + reference: String! + passages: [String]! +} + +input RssFeedInput { + workspaceId: String! + address: String! + limit: Int! + title: String + followLinks: Boolean! +} + +input SemanticSearchInput { + workspaceId: String! + query: String! +} + +type SemanticSearchItem @aws_cognito_user_pools { + sources: [String] + chunkId: String + workspaceId: ID! + documentId: String + documentSubId: String + documentSubType: String + documentType: String! + path: String + language: String + title: String + content: String + contentComplement: String + vectorSearchScore: Float + keywordSearchScore: Float + score: Float +} + +type SemanticSearchResult @aws_cognito_user_pools { + engine: String! + workspaceId: String! + queryLanguage: String + supportedLanguages: [String!] + detectedLanguages: [DetectedLanguage!] + items: [SemanticSearchItem!] + vectorSearchMetric: String + vectorSearchItems: [SemanticSearchItem!] + keywordSearchItems: [SemanticSearchItem!] +} + +type Session @aws_cognito_user_pools { + id: String! + title: String + startTime: AWSDateTime! + history: [SessionHistoryItem] +} + +type SessionHistoryItem @aws_cognito_user_pools { + type: String! + content: String! + metadata: String +} + +input TextDocumentInput { + workspaceId: String! + title: String! + content: String! +} + +input WebsiteInput { + workspaceId: String! + sitemap: Boolean! + address: String! + followLinks: Boolean! + limit: Int! +} + +type Workspace @aws_cognito_user_pools { + id: String! + name: String! + formatVersion: Int + engine: String! + status: String + aossEngine: String + languages: [String] + hasIndex: Boolean + embeddingsModelProvider: String + embeddingsModelName: String + embeddingsModelDimensions: Int + crossEncoderModelName: String + crossEncoderModelProvider: String + metric: String + index: Boolean + hybridSearch: Boolean + chunkingStrategy: String + chunkSize: Int + chunkOverlap: Int + vectors: Int + documents: Int + sizeInBytes: Int + kendraIndexId: String + kendraIndexExternal: Boolean + kendraUseAllData: Boolean + createdAt: AWSDateTime! + updatedAt: AWSDateTime! +} + +type Channel @aws_iam @aws_cognito_user_pools { + data: String + sessionId: String + userId: String +} + +type Mutation { + createKendraWorkspace(input: CreateWorkspaceKendraInput!): Workspace! + @aws_cognito_user_pools + createOpenSearchWorkspace(input: CreateWorkspaceOpenSearchInput!): Workspace! + @aws_cognito_user_pools + createAuroraWorkspace(input: CreateWorkspaceAuroraInput!): Workspace! + @aws_cognito_user_pools + startKendraDataSync(workspaceId: String!): Boolean @aws_cognito_user_pools + deleteWorkspace(workspaceId: String!): Boolean @aws_cognito_user_pools + addTextDocument(input: TextDocumentInput!): DocumentResult + @aws_cognito_user_pools + addQnADocument(input: QnADocumentInput!): DocumentResult + @aws_cognito_user_pools + setDocumentSubscriptionStatus( + input: DocumentSubscriptionStatusInput! + ): DocumentResult @aws_cognito_user_pools + addWebsite(input: WebsiteInput!): DocumentResult @aws_cognito_user_pools + addRssFeed(input: RssFeedInput!): DocumentResult @aws_cognito_user_pools + updateRssFeed(input: RssFeedInput!): DocumentResult @aws_cognito_user_pools + deleteUserSessions: [DeleteSessionResult!] @aws_cognito_user_pools + deleteSession(id: String!): DeleteSessionResult @aws_cognito_user_pools + # Real-time + sendQuery(data: String): String @aws_cognito_user_pools + publishResponse(sessionId: String, userId: String, data: String): Channel + @aws_iam +} + +type Query { + checkHealth: Boolean @aws_cognito_user_pools + getUploadFileURL(input: FileUploadInput!): FileUploadResult + @aws_cognito_user_pools + listModels: [Model!]! @aws_cognito_user_pools + listWorkspaces: [Workspace!]! @aws_cognito_user_pools + getWorkspace(workspaceId: String!): Workspace @aws_cognito_user_pools + listRagEngines: [RagEngine!]! @aws_cognito_user_pools + performSemanticSearch(input: SemanticSearchInput!): SemanticSearchResult! + @aws_cognito_user_pools + listSessions: [Session!]! @aws_cognito_user_pools + listEmbeddingModels: [EmbeddingModel!]! @aws_cognito_user_pools + calculateEmbeddings(input: CalculateEmbeddingsInput!): [Embedding]! + @aws_cognito_user_pools + getSession(id: String!): Session @aws_cognito_user_pools + listKendraIndexes: [KendraIndex!]! @aws_cognito_user_pools + isKendraDataSynching(workspaceId: String!): Boolean @aws_cognito_user_pools + listDocuments(input: ListDocumentsInput!): DocumentsResult! + @aws_cognito_user_pools + getDocument(input: GetDocumentInput!): Document @aws_cognito_user_pools + getRSSPosts(input: GetRSSPostsInput!): DocumentsResult @aws_cognito_user_pools + listCrossEncoders: [CrossEncoderData!] @aws_cognito_user_pools + rankPassages(input: RankPassagesInput!): [PassageRank!]! + @aws_cognito_user_pools +} + +type Subscription { + receiveMessages(sessionId: String): Channel + @aws_subscribe(mutations: ["publishResponse"]) + @aws_cognito_user_pools +} + +schema { + query: Query + mutation: Mutation + subscription: Subscription +} diff --git a/lib/chatbot-api/websocket-api.ts b/lib/chatbot-api/websocket-api.ts index e3379d3df..2baee9c76 100644 --- a/lib/chatbot-api/websocket-api.ts +++ b/lib/chatbot-api/websocket-api.ts @@ -1,187 +1,36 @@ -import * as apigwv2 from "@aws-cdk/aws-apigatewayv2-alpha"; -import { WebSocketLambdaAuthorizer } from "@aws-cdk/aws-apigatewayv2-authorizers-alpha"; -import { WebSocketLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; import * as cdk from "aws-cdk-lib"; -import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import * as iam from "aws-cdk-lib/aws-iam"; -import * as lambda from "aws-cdk-lib/aws-lambda"; -import * as lambdaEventSources from "aws-cdk-lib/aws-lambda-event-sources"; + import * as sns from "aws-cdk-lib/aws-sns"; import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions"; import * as sqs from "aws-cdk-lib/aws-sqs"; import { Construct } from "constructs"; -import * as path from "path"; + import { Shared } from "../shared"; import { Direction } from "../shared/types"; +import { RealtimeResolvers } from "./appsync-ws"; +import { UserPool } from "aws-cdk-lib/aws-cognito"; +import * as appsync from "aws-cdk-lib/aws-appsync"; -interface WebSocketApiProps { +interface RealtimeGraphqlApiBackendProps { readonly shared: Shared; + readonly userPool: UserPool; + readonly api: appsync.GraphqlApi; } -export class WebSocketApi extends Construct { - public readonly api: apigwv2.WebSocketApi; +export class RealtimeGraphqlApiBackend extends Construct { public readonly messagesTopic: sns.Topic; + public readonly resolvers: RealtimeResolvers; - constructor(scope: Construct, id: string, props: WebSocketApiProps) { + constructor( + scope: Construct, + id: string, + props: RealtimeGraphqlApiBackendProps + ) { super(scope, id); - // Create the main Message Topic acting as a message bus const messagesTopic = new sns.Topic(this, "MessagesTopic"); - const connectionsTable = new dynamodb.Table(this, "ConnectionsTable", { - partitionKey: { - name: "connectionId", - type: dynamodb.AttributeType.STRING, - }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - encryption: dynamodb.TableEncryption.AWS_MANAGED, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - connectionsTable.addGlobalSecondaryIndex({ - indexName: "byUser", - partitionKey: { name: "userId", type: dynamodb.AttributeType.STRING }, - }); - - const connectionHandlerFunction = new lambda.Function( - this, - "ConnectionHandlerFunction", - { - code: lambda.Code.fromAsset( - path.join(__dirname, "./functions/connection-handler") - ), - handler: "index.handler", - runtime: props.shared.pythonRuntime, - architecture: props.shared.lambdaArchitecture, - tracing: lambda.Tracing.ACTIVE, - layers: [props.shared.powerToolsLayer], - environment: { - ...props.shared.defaultEnvironmentVariables, - CONNECTIONS_TABLE_NAME: connectionsTable.tableName, - }, - } - ); - - connectionsTable.grantReadWriteData(connectionHandlerFunction); - - const authorizerFunction = new lambda.Function(this, "AuthorizerFunction", { - code: lambda.Code.fromAsset( - path.join(__dirname, "./functions/authorizer") - ), - handler: "index.handler", - runtime: props.shared.pythonRuntime, - architecture: props.shared.lambdaArchitecture, - tracing: lambda.Tracing.ACTIVE, - layers: [props.shared.powerToolsLayer], - environment: { - ...props.shared.defaultEnvironmentVariables, - }, - }); - - const webSocketApi = new apigwv2.WebSocketApi(this, "WebSocketApi", { - connectRouteOptions: { - authorizer: new WebSocketLambdaAuthorizer( - "Authorizer", - authorizerFunction, - { - identitySource: ["route.request.querystring.token"], - } - ), - integration: new WebSocketLambdaIntegration( - "ConnectIntegration", - connectionHandlerFunction - ), - }, - disconnectRouteOptions: { - integration: new WebSocketLambdaIntegration( - "DisconnectIntegration", - connectionHandlerFunction - ), - }, - }); - - const stage = new apigwv2.WebSocketStage(this, "WebSocketApiStage", { - webSocketApi, - stageName: "socket", - autoDeploy: true, - }); - - const incomingMessageHandlerFunction = new lambda.Function( - this, - "IncomingMessageHandlerFunction", - { - code: lambda.Code.fromAsset( - path.join(__dirname, "./functions/incoming-message-handler") - ), - handler: "index.handler", - runtime: props.shared.pythonRuntime, - architecture: props.shared.lambdaArchitecture, - tracing: lambda.Tracing.ACTIVE, - layers: [props.shared.powerToolsLayer], - environment: { - ...props.shared.defaultEnvironmentVariables, - MESSAGES_TOPIC_ARN: messagesTopic.topicArn, - WEBSOCKET_API_ENDPOINT: stage.callbackUrl, - }, - } - ); - - messagesTopic.grantPublish(incomingMessageHandlerFunction); - incomingMessageHandlerFunction.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["events:PutEvents"], - resources: [ - `arn:${cdk.Aws.PARTITION}:events:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:event-bus/default`, - ], - }) - ); - - incomingMessageHandlerFunction.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["execute-api:ManageConnections"], - resources: [ - `arn:${cdk.Aws.PARTITION}:execute-api:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:${webSocketApi.apiId}/${stage.stageName}/*/*`, - ], - }) - ); - - webSocketApi.addRoute("$default", { - integration: new WebSocketLambdaIntegration( - "DefaultIntegration", - incomingMessageHandlerFunction - ), - }); - - const outgoingMessageHandlerFunction = new lambda.Function( - this, - "OutgoingMessageFunction", - { - code: lambda.Code.fromAsset( - path.join(__dirname, "./functions/outgoing-message-handler") - ), - handler: "index.handler", - runtime: props.shared.pythonRuntime, - architecture: props.shared.lambdaArchitecture, - tracing: lambda.Tracing.ACTIVE, - layers: [props.shared.powerToolsLayer], - environment: { - ...props.shared.defaultEnvironmentVariables, - WEBSOCKET_API_ENDPOINT: stage.callbackUrl, - CONNECTIONS_TABLE_NAME: connectionsTable.tableName, - }, - } - ); - - connectionsTable.grantReadData(outgoingMessageHandlerFunction); - outgoingMessageHandlerFunction.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["execute-api:ManageConnections"], - resources: [ - `arn:${cdk.Aws.PARTITION}:execute-api:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:${webSocketApi.apiId}/${stage.stageName}/*/*`, - ], - }) - ); - const deadLetterQueue = new sqs.Queue(this, "OutgoingMessagesDLQ"); const queue = new sqs.Queue(this, "OutgoingMessagesQueue", { @@ -204,9 +53,13 @@ export class WebSocketApi extends Construct { }) ); - outgoingMessageHandlerFunction.addEventSource( - new lambdaEventSources.SqsEventSource(queue) - ); + const resolvers = new RealtimeResolvers(this, "Resolvers", { + queue: queue, + topic: messagesTopic, + userPool: props.userPool, + shared: props.shared, + api: props.api, + }); // Route all outgoing messages to the websocket interface queue messagesTopic.addSubscription( @@ -221,7 +74,7 @@ export class WebSocketApi extends Construct { }) ); - this.api = webSocketApi; this.messagesTopic = messagesTopic; + this.resolvers = resolvers; } } diff --git a/lib/model-interfaces/idefics/functions/request-handler/index.py b/lib/model-interfaces/idefics/functions/request-handler/index.py index 2848cb762..3dc2a3023 100644 --- a/lib/model-interfaces/idefics/functions/request-handler/index.py +++ b/lib/model-interfaces/idefics/functions/request-handler/index.py @@ -24,7 +24,6 @@ def handle_run(record): - connection_id = record["connectionId"] user_id = record["userId"] data = record["data"] provider = data["provider"] @@ -139,7 +138,6 @@ def handle_run(record): { "type": "text", "action": ChatbotAction.FINAL_RESPONSE.value, - "connectionId": connection_id, "timestamp": str(int(round(datetime.now().timestamp()))), "userId": user_id, "data": response, @@ -165,7 +163,6 @@ def handle_failed_records(records): message: dict = json.loads(payload) detail: dict = json.loads(message["Message"]) logger.info(detail) - connection_id = detail["connectionId"] user_id = detail["userId"] data = detail.get("data", {}) session_id = data.get("sessionId", "") @@ -174,7 +171,6 @@ def handle_failed_records(records): { "type": "text", "action": ChatbotAction.FINAL_RESPONSE.value, - "connectionId": connection_id, "userId": user_id, "timestamp": str(int(round(datetime.now().timestamp()))), "data": { diff --git a/lib/model-interfaces/langchain/functions/request-handler/index.py b/lib/model-interfaces/langchain/functions/request-handler/index.py index f37c892db..cf46ba3fe 100644 --- a/lib/model-interfaces/langchain/functions/request-handler/index.py +++ b/lib/model-interfaces/langchain/functions/request-handler/index.py @@ -24,7 +24,7 @@ def on_llm_new_token( - connection_id, user_id, session_id, self, token, run_id, *args, **kwargs + user_id, session_id, self, token, run_id, *args, **kwargs ): global sequence_number sequence_number += 1 @@ -34,7 +34,6 @@ def on_llm_new_token( { "type": "text", "action": ChatbotAction.LLM_NEW_TOKEN.value, - "connectionId": connection_id, "userId": user_id, "timestamp": str(int(round(datetime.now().timestamp()))), "data": { @@ -50,22 +49,23 @@ def on_llm_new_token( def handle_heartbeat(record): - connection_id = record["connectionId"] user_id = record["userId"] + session_id = record["data"]["sessionId"] send_to_client( { "type": "text", "action": ChatbotAction.HEARTBEAT.value, - "connectionId": connection_id, "timestamp": str(int(round(datetime.now().timestamp()))), "userId": user_id, + "data": { + "sessionId": session_id, + }, } ) def handle_run(record): - connection_id = record["connectionId"] user_id = record["userId"] data = record["data"] provider = data["provider"] @@ -81,7 +81,7 @@ def handle_run(record): adapter = registry.get_adapter(f"{provider}.{model_id}") adapter.on_llm_new_token = lambda *args, **kwargs: on_llm_new_token( - connection_id, user_id, session_id, *args, **kwargs + user_id, session_id, *args, **kwargs ) model = adapter( @@ -103,7 +103,6 @@ def handle_run(record): { "type": "text", "action": ChatbotAction.FINAL_RESPONSE.value, - "connectionId": connection_id, "timestamp": str(int(round(datetime.now().timestamp()))), "userId": user_id, "data": response, @@ -131,7 +130,6 @@ def handle_failed_records(records): message: dict = json.loads(payload) detail: dict = json.loads(message["Message"]) logger.info(detail) - connection_id = detail["connectionId"] user_id = detail["userId"] data = detail.get("data", {}) session_id = data.get("sessionId", "") @@ -141,7 +139,6 @@ def handle_failed_records(records): "type": "text", "action": "error", "direction": "OUT", - "connectionId": connection_id, "userId": user_id, "timestamp": str(int(round(datetime.now().timestamp()))), "data": { diff --git a/lib/rag-engines/opensearch-vector/index.ts b/lib/rag-engines/opensearch-vector/index.ts index 7577c8359..6656d3ef6 100644 --- a/lib/rag-engines/opensearch-vector/index.ts +++ b/lib/rag-engines/opensearch-vector/index.ts @@ -45,7 +45,10 @@ export class OpenSearchVector extends Construct { const cfnVpcEndpoint = new oss.CfnVpcEndpoint(this, "VpcEndpoint", { name: Utils.getName(props.config, "genaichatbot-vpce"), // Make sure the subnets are not in the same availability zone. - subnetIds: props.shared.vpc.selectSubnets({onePerAz: true, subnetType: ec2.SubnetType.PRIVATE_ISOLATED}).subnetIds, + subnetIds: props.shared.vpc.selectSubnets({ + onePerAz: true, + subnetType: ec2.SubnetType.PRIVATE_ISOLATED, + }).subnetIds, vpcId: props.shared.vpc.vpcId, securityGroupIds: [sg.securityGroupId], }); diff --git a/lib/shared/layers/python-sdk/python/README.md b/lib/shared/layers/python-sdk/python/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lib/shared/layers/python-sdk/python/genai_core/auth.py b/lib/shared/layers/python-sdk/python/genai_core/auth.py index e48fd6042..f95483512 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/auth.py +++ b/lib/shared/layers/python-sdk/python/genai_core/auth.py @@ -1,9 +1,4 @@ def get_user_id(router): - user_id = ( - router.current_event.get("requestContext", {}) - .get("authorizer", {}) - .get("claims", {}) - .get("cognito:username") - ) + user_id = router.current_event.get("identity", {}).get("sub") return user_id diff --git a/lib/shared/layers/python-sdk/python/genai_core/sessions.py b/lib/shared/layers/python-sdk/python/genai_core/sessions.py index 8933bdd33..b11ee1f88 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/sessions.py +++ b/lib/shared/layers/python-sdk/python/genai_core/sessions.py @@ -67,9 +67,9 @@ def delete_session(session_id, user_id): else: print(error) - return {"deleted": False} + return {"id": session_id, "deleted": False} - return {"deleted": True} + return {"id": session_id, "deleted": True} def delete_user_sessions(user_id): diff --git a/lib/shared/layers/python-sdk/python/genai_core/upload.py b/lib/shared/layers/python-sdk/python/genai_core/upload.py index 4da396328..fd899afe6 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/upload.py +++ b/lib/shared/layers/python-sdk/python/genai_core/upload.py @@ -30,6 +30,6 @@ def generate_presigned_post(workspace_id: str, file_name: str, expiration=3600): if not response: return None - response["url"] = (f"https://{UPLOAD_BUCKET_NAME}.s3-accelerate.amazonaws.com",) + response["url"] = f"https://{UPLOAD_BUCKET_NAME}.s3-accelerate.amazonaws.com" return response diff --git a/lib/shared/layers/python-sdk/python/genai_core/utils/json.py b/lib/shared/layers/python-sdk/python/genai_core/utils/json.py index 79d282dc0..64c67104e 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/utils/json.py +++ b/lib/shared/layers/python-sdk/python/genai_core/utils/json.py @@ -6,10 +6,9 @@ class CustomEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, decimal.Decimal): - if obj % 1 > 0: + if "." in str(obj): return float(obj) - else: - return int(obj) + return int(obj) if isinstance(obj, uuid.UUID): return str(obj) diff --git a/lib/shared/layers/python-sdk/python/genai_core/workspaces.py b/lib/shared/layers/python-sdk/python/genai_core/workspaces.py index 272b25fab..07c212498 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/workspaces.py +++ b/lib/shared/layers/python-sdk/python/genai_core/workspaces.py @@ -154,9 +154,7 @@ def create_workspace_aurora( print(response) - return { - "id": workspace_id, - } + return item def create_workspace_open_search( @@ -223,9 +221,7 @@ def create_workspace_open_search( print(response) - return { - "id": workspace_id, - } + return item def create_workspace_kendra( @@ -268,9 +264,7 @@ def create_workspace_kendra( print(response) - return { - "id": workspace_id, - } + return item def delete_workspace(workspace_id: str): diff --git a/lib/shared/layers/python-sdk/python/pyproject.toml b/lib/shared/layers/python-sdk/python/pyproject.toml new file mode 100644 index 000000000..b7070c4c2 --- /dev/null +++ b/lib/shared/layers/python-sdk/python/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "genai-core" +version = "0.1.0" +description = "" +authors = ["Amazon Web Services"] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/lib/user-interface/index.ts b/lib/user-interface/index.ts index 1189418ee..ace7cf989 100644 --- a/lib/user-interface/index.ts +++ b/lib/user-interface/index.ts @@ -1,7 +1,5 @@ -import * as apigwv2 from "@aws-cdk/aws-apigatewayv2-alpha"; import * as cognitoIdentityPool from "@aws-cdk/aws-cognito-identitypool-alpha"; import * as cdk from "aws-cdk-lib"; -import * as apigateway from "aws-cdk-lib/aws-apigateway"; import * as cf from "aws-cdk-lib/aws-cloudfront"; import * as iam from "aws-cdk-lib/aws-iam"; import * as s3 from "aws-cdk-lib/aws-s3"; @@ -15,6 +13,7 @@ import * as path from "node:path"; import { Shared } from "../shared"; import { SystemConfig } from "../shared/types"; import { Utils } from "../shared/utils"; +import { ChatBotApi } from "../chatbot-api"; export interface UserInterfaceProps { readonly config: SystemConfig; @@ -22,8 +21,7 @@ export interface UserInterfaceProps { readonly userPoolId: string; readonly userPoolClientId: string; readonly identityPool: cognitoIdentityPool.IdentityPool; - readonly restApi: apigateway.RestApi; - readonly webSocketApi: apigwv2.WebSocketApi; + readonly api: ChatBotApi; readonly chatbotFilesBucket: s3.Bucket; readonly crossEncodersEnabled: boolean; readonly sagemakerEmbeddingsEnabled: boolean; @@ -50,7 +48,7 @@ export class UserInterface extends Construct { const distribution = new cf.CloudFrontWebDistribution( this, - "Distirbution", + "Distribution", { viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, priceClass: cf.PriceClass.PRICE_CLASS_ALL, @@ -63,63 +61,6 @@ export class UserInterface extends Construct { originAccessIdentity, }, }, - { - behaviors: [ - { - pathPattern: "/api/*", - allowedMethods: cf.CloudFrontAllowedMethods.ALL, - viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - defaultTtl: cdk.Duration.seconds(0), - forwardedValues: { - queryString: true, - headers: [ - "Referer", - "Origin", - "Authorization", - "Content-Type", - "x-forwarded-user", - "Access-Control-Request-Headers", - "Access-Control-Request-Method", - ], - }, - }, - ], - customOriginSource: { - domainName: `${props.restApi.restApiId}.execute-api.${cdk.Aws.REGION}.${cdk.Aws.URL_SUFFIX}`, - originHeaders: { - "X-Origin-Verify": props.shared.xOriginVerifySecret - .secretValueFromJson("headerValue") - .unsafeUnwrap(), - }, - }, - }, - { - behaviors: [ - { - pathPattern: "/socket", - allowedMethods: cf.CloudFrontAllowedMethods.ALL, - viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - forwardedValues: { - queryString: true, - headers: [ - "Sec-WebSocket-Key", - "Sec-WebSocket-Version", - "Sec-WebSocket-Protocol", - "Sec-WebSocket-Accept", - "Sec-WebSocket-Extensions", - ], - }, - }, - ], - customOriginSource: { - domainName: `${props.webSocketApi.apiId}.execute-api.${cdk.Aws.REGION}.${cdk.Aws.URL_SUFFIX}`, - originHeaders: { - "X-Origin-Verify": props.shared.xOriginVerifySecret - .secretValueFromJson("headerValue") - .unsafeUnwrap(), - }, - }, - }, { behaviors: [ { @@ -164,6 +105,16 @@ export class UserInterface extends Construct { aws_user_pools_id: props.userPoolId, aws_user_pools_web_client_id: props.userPoolClientId, aws_cognito_identity_pool_id: props.identityPool.identityPoolId, + Auth: { + region: cdk.Aws.REGION, + userPoolId: props.userPoolId, + userPoolWebClientId: props.userPoolClientId, + identityPoolId: props.identityPool.identityPoolId, + }, + aws_appsync_graphqlEndpoint: props.api.graphqlApi.graphqlUrl, + aws_appsync_region: cdk.Aws.REGION, + aws_appsync_authenticationType: "AMAZON_COGNITO_USER_POOLS", + aws_appsync_apiKey: props.api.graphqlApi?.apiKey, Storage: { AWSS3: { bucket: props.chatbotFilesBucket.bucketName, @@ -171,8 +122,6 @@ export class UserInterface extends Construct { }, }, config: { - api_endpoint: `https://${distribution.distributionDomainName}/api`, - websocket_endpoint: `wss://${distribution.distributionDomainName}/socket`, rag_enabled: props.config.rag.enabled, cross_encoders_enabled: props.crossEncodersEnabled, sagemaker_embeddings_enabled: props.sagemakerEmbeddingsEnabled, diff --git a/lib/user-interface/react-app/README.md b/lib/user-interface/react-app/README.md index c77e44a14..453bdbc94 100644 --- a/lib/user-interface/react-app/README.md +++ b/lib/user-interface/react-app/README.md @@ -6,15 +6,13 @@ You can run this vite react app locally following these steps. ### Deploy infrastructure to AWS -Follow instructions on the root folder README to deploy the cdk app. +Follow instructions on the root folder README to deploy the cdk app. You will need the CloudFormation Output values displayed after completion in the following step. -### Define environment variables +### Obtain environment configuration -See `.env.template` for the variables, and replace with the Output values from the previous step. - -Alternatively you can grab the `aws-exports.json` from the CloudFront distribution endpoint you obtained from the CDK Output, and save it into `./lib/user-interface/react-app/public/` folder. +Grab the `aws-exports.json` from the CloudFront distribution endpoint you obtained from the CDK Output, and save it into `./lib/user-interface/react-app/public/` folder. The URL is something like: diff --git a/lib/user-interface/react-app/schema.json b/lib/user-interface/react-app/schema.json new file mode 100644 index 000000000..31400f6dd --- /dev/null +++ b/lib/user-interface/react-app/schema.json @@ -0,0 +1,4568 @@ +{ + "data" : { + "__schema" : { + "queryType" : { + "name" : "Query" + }, + "mutationType" : { + "name" : "Mutation" + }, + "subscriptionType" : { + "name" : "Subscription" + }, + "types" : [ { + "kind" : "OBJECT", + "name" : "Query", + "description" : null, + "fields" : [ { + "name" : "none", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "checkHealth", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "getUploadFileURL", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "FileUploadInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "FileUploadResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listModels", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Model", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listWorkspaces", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Workspace", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "getWorkspace", + "description" : null, + "args" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "Workspace", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listRagEngines", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "RagEngine", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "performSemanticSearch", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "SemanticSearchInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "SemanticSearchResult", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listSessions", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Session", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listEmbeddingModels", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "EmbeddingModel", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "calculateEmbeddings", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "CalculateEmbeddingsInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Embedding", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "getSession", + "description" : null, + "args" : [ { + "name" : "id", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "Session", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listKendraIndexes", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "KendraIndex", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "isKendraDataSynching", + "description" : null, + "args" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listDocuments", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "ListDocumentsInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "DocumentsResult", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "getDocument", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "GetDocumentInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "Document", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "getRSSPosts", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "GetRSSPostsInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DocumentsResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "listCrossEncoders", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "CrossEncoderData", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "rankPassages", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "RankPassagesInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "PassageRank", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "SCALAR", + "name" : "String", + "description" : "Built-in String", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "SCALAR", + "name" : "Boolean", + "description" : "Built-in Boolean", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "FileUploadResult", + "description" : null, + "fields" : [ { + "name" : "url", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "fields", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "FileUploadInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "fileName", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Model", + "description" : null, + "fields" : [ { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "provider", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "interface", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "ragSupported", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "inputModalities", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "outputModalities", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "streaming", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Workspace", + "description" : null, + "fields" : [ { + "name" : "id", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "formatVersion", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "engine", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "status", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "aossEngine", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "languages", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "hasIndex", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "embeddingsModelProvider", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "embeddingsModelName", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "embeddingsModelDimensions", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "crossEncoderModelName", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "crossEncoderModelProvider", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "metric", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "index", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "hybridSearch", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "chunkingStrategy", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "chunkSize", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "chunkOverlap", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "vectors", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "documents", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "sizeInBytes", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "kendraIndexId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "kendraIndexExternal", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "kendraUseAllData", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "createdAt", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "AWSDateTime", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "updatedAt", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "AWSDateTime", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "SCALAR", + "name" : "Int", + "description" : "Built-in Int", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "SCALAR", + "name" : "AWSDateTime", + "description" : "The `AWSDateTime` scalar type provided by AWS AppSync, represents a valid ***extended*** [ISO 8601 DateTime](https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations) string. In other words, this scalar type accepts datetime strings of the form `YYYY-MM-DDThh:mm:ss.SSSZ`. The scalar can also accept \"negative years\" of the form `-YYYY` which correspond to years before `0000`. For example, \"**-2017-01-01T00:00Z**\" and \"**-9999-01-01T00:00Z**\" are both valid datetime strings. The field after the two digit seconds field is a nanoseconds field. It can accept between 1 and 9 digits. So, for example, \"**1970-01-01T12:00:00.2Z**\", \"**1970-01-01T12:00:00.277Z**\" and \"**1970-01-01T12:00:00.123456789Z**\" are all valid datetime strings. The seconds and nanoseconds fields are optional (the seconds field must be specified if the nanoseconds field is to be used). The [time zone offset](https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators) is compulsory for this scalar. The time zone offset must either be `Z` (representing the UTC time zone) or be in the format `±hh:mm:ss`. The seconds field in the timezone offset will be considered valid even though it is not part of the ISO 8601 standard.", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "RagEngine", + "description" : null, + "fields" : [ { + "name" : "id", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "enabled", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "SemanticSearchResult", + "description" : null, + "fields" : [ { + "name" : "engine", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "workspaceId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "queryLanguage", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "supportedLanguages", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "detectedLanguages", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "DetectedLanguage", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "items", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "SemanticSearchItem", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "vectorSearchMetric", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "vectorSearchItems", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "SemanticSearchItem", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "keywordSearchItems", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "SemanticSearchItem", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "DetectedLanguage", + "description" : null, + "fields" : [ { + "name" : "code", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "score", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Float", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "SCALAR", + "name" : "Float", + "description" : "Built-in Float", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "SemanticSearchItem", + "description" : null, + "fields" : [ { + "name" : "sources", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "chunkId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "workspaceId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "ID", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "documentId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "documentSubId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "documentSubType", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "documentType", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "path", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "language", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "title", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "content", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "contentComplement", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "vectorSearchScore", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Float", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "keywordSearchScore", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Float", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "score", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Float", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "SCALAR", + "name" : "ID", + "description" : "Built-in ID", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "SemanticSearchInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "query", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Session", + "description" : null, + "fields" : [ { + "name" : "id", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "title", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "startTime", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "AWSDateTime", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "history", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "SessionHistoryItem", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "SessionHistoryItem", + "description" : null, + "fields" : [ { + "name" : "type", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "content", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "metadata", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "EmbeddingModel", + "description" : null, + "fields" : [ { + "name" : "provider", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "dimensions", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "default", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Embedding", + "description" : null, + "fields" : [ { + "name" : "passage", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "vector", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Float", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "CalculateEmbeddingsInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "provider", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "model", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "passages", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "KendraIndex", + "description" : null, + "fields" : [ { + "name" : "id", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "external", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "DocumentsResult", + "description" : null, + "fields" : [ { + "name" : "items", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Document", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "lastDocumentId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Document", + "description" : null, + "fields" : [ { + "name" : "workspaceId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "id", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "type", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "subType", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "status", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "title", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "path", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "sizeInBytes", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "vectors", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "subDocuments", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "crawlerProperties", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "OBJECT", + "name" : "CrawlerProperties", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "errors", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "createdAt", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "AWSDateTime", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "updatedAt", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "AWSDateTime", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "rssFeedId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "rssLastCheckedAt", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "AWSDateTime", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "CrawlerProperties", + "description" : null, + "fields" : [ { + "name" : "followLinks", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "limit", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "ListDocumentsInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "documentType", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "lastDocumentId", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "GetDocumentInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "documentId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "GetRSSPostsInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "documentId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "lastDocumentId", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "CrossEncoderData", + "description" : null, + "fields" : [ { + "name" : "provider", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "default", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "PassageRank", + "description" : null, + "fields" : [ { + "name" : "score", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Float", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "passage", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "RankPassagesInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "provider", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "model", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "reference", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "passages", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Mutation", + "description" : null, + "fields" : [ { + "name" : "sendQuery", + "description" : null, + "args" : [ { + "name" : "data", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "publishResponse", + "description" : null, + "args" : [ { + "name" : "sessionId", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + }, { + "name" : "userId", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + }, { + "name" : "data", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "Channel", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "createKendraWorkspace", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "CreateWorkspaceKendraInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Workspace", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "createOpenSearchWorkspace", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "CreateWorkspaceOpenSearchInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Workspace", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "createAuroraWorkspace", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "CreateWorkspaceAuroraInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "Workspace", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "startKendraDataSync", + "description" : null, + "args" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "deleteWorkspace", + "description" : null, + "args" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "addTextDocument", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "TextDocumentInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DocumentResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "addQnADocument", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "QnADocumentInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DocumentResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "setDocumentSubscriptionStatus", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "DocumentSubscriptionStatusInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DocumentResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "addWebsite", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "WebsiteInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DocumentResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "addRssFeed", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "RssFeedInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DocumentResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "updateRssFeed", + "description" : null, + "args" : [ { + "name" : "input", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "INPUT_OBJECT", + "name" : "RssFeedInput", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DocumentResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "deleteUserSessions", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "DeleteSessionResult", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "deleteSession", + "description" : null, + "args" : [ { + "name" : "id", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "DeleteSessionResult", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Channel", + "description" : null, + "fields" : [ { + "name" : "data", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "sessionId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "userId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "CreateWorkspaceKendraInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "name", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "kind", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "kendraIndexId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "useAllData", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "CreateWorkspaceOpenSearchInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "name", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "kind", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "embeddingsModelProvider", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "embeddingsModelName", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "crossEncoderModelProvider", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "crossEncoderModelName", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "languages", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + } + }, + "defaultValue" : null + }, { + "name" : "hybridSearch", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "chunkingStrategy", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "chunkSize", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "chunkOverlap", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "CreateWorkspaceAuroraInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "name", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "kind", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "embeddingsModelProvider", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "embeddingsModelName", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "crossEncoderModelProvider", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "crossEncoderModelName", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "languages", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + } + } + }, + "defaultValue" : null + }, { + "name" : "metric", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "index", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "hybridSearch", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "chunkingStrategy", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "chunkSize", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "chunkOverlap", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "DocumentResult", + "description" : null, + "fields" : [ { + "name" : "workspaceId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "documentId", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "status", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "TextDocumentInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "title", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "content", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "QnADocumentInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "question", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "answer", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "DocumentSubscriptionStatusInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "documentId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "status", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "WebsiteInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "sitemap", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "address", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "followLinks", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "limit", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "INPUT_OBJECT", + "name" : "RssFeedInput", + "description" : null, + "fields" : null, + "inputFields" : [ { + "name" : "workspaceId", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "address", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "limit", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Int", + "ofType" : null + } + }, + "defaultValue" : null + }, { + "name" : "title", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + }, { + "name" : "followLinks", + "description" : null, + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "DeleteSessionResult", + "description" : null, + "fields" : [ { + "name" : "id", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "deleted", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "Subscription", + "description" : null, + "fields" : [ { + "name" : "receiveMessages", + "description" : null, + "args" : [ { + "name" : "sessionId", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + } ], + "type" : { + "kind" : "OBJECT", + "name" : "Channel", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "__Schema", + "description" : "A GraphQL Introspection defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, the entry points for query, mutation, and subscription operations.", + "fields" : [ { + "name" : "types", + "description" : "A list of all types supported by this server.", + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "queryType", + "description" : "The type that query operations will be rooted at.", + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "mutationType", + "description" : "If this server supports mutation, the type that mutation operations will be rooted at.", + "args" : [ ], + "type" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "directives", + "description" : "'A list of all directives supported by this server.", + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Directive", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "subscriptionType", + "description" : "'If this server support subscription, the type that subscription operations will be rooted at.", + "args" : [ ], + "type" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "__Type", + "description" : null, + "fields" : [ { + "name" : "kind", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "ENUM", + "name" : "__TypeKind", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "description", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "fields", + "description" : null, + "args" : [ { + "name" : "includeDeprecated", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "defaultValue" : "false" + } ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Field", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "interfaces", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "possibleTypes", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "enumValues", + "description" : null, + "args" : [ { + "name" : "includeDeprecated", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "defaultValue" : "false" + } ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__EnumValue", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "inputFields", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__InputValue", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "ofType", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "ENUM", + "name" : "__TypeKind", + "description" : "An enum describing what kind of type a given __Type is", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : [ { + "name" : "SCALAR", + "description" : "Indicates this type is a scalar.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "OBJECT", + "description" : "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "INTERFACE", + "description" : "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "UNION", + "description" : "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "ENUM", + "description" : "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "INPUT_OBJECT", + "description" : "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "LIST", + "description" : "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "NON_NULL", + "description" : "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated" : false, + "deprecationReason" : null + } ], + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "__Field", + "description" : null, + "fields" : [ { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "description", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "args", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__InputValue", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "type", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "isDeprecated", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "deprecationReason", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "__InputValue", + "description" : null, + "fields" : [ { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "description", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "type", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__Type", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "defaultValue", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "__EnumValue", + "description" : null, + "fields" : [ { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "description", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "isDeprecated", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "deprecationReason", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "OBJECT", + "name" : "__Directive", + "description" : null, + "fields" : [ { + "name" : "name", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "description", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "locations", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "ENUM", + "name" : "__DirectiveLocation", + "ofType" : null + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "args", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "OBJECT", + "name" : "__InputValue", + "ofType" : null + } + } + } + }, + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "onOperation", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : true, + "deprecationReason" : "Use `locations`." + }, { + "name" : "onFragment", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : true, + "deprecationReason" : "Use `locations`." + }, { + "name" : "onField", + "description" : null, + "args" : [ ], + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "isDeprecated" : true, + "deprecationReason" : "Use `locations`." + } ], + "inputFields" : null, + "interfaces" : [ ], + "enumValues" : null, + "possibleTypes" : null + }, { + "kind" : "ENUM", + "name" : "__DirectiveLocation", + "description" : "An enum describing valid locations where a directive can be placed", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : [ { + "name" : "QUERY", + "description" : "Indicates the directive is valid on queries.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "MUTATION", + "description" : "Indicates the directive is valid on mutations.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "FIELD", + "description" : "Indicates the directive is valid on fields.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "FRAGMENT_DEFINITION", + "description" : "Indicates the directive is valid on fragment definitions.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "FRAGMENT_SPREAD", + "description" : "Indicates the directive is valid on fragment spreads.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "INLINE_FRAGMENT", + "description" : "Indicates the directive is valid on inline fragments.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "SCHEMA", + "description" : "Indicates the directive is valid on a schema SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "SCALAR", + "description" : "Indicates the directive is valid on a scalar SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "OBJECT", + "description" : "Indicates the directive is valid on an object SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "FIELD_DEFINITION", + "description" : "Indicates the directive is valid on a field SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "ARGUMENT_DEFINITION", + "description" : "Indicates the directive is valid on a field argument SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "INTERFACE", + "description" : "Indicates the directive is valid on an interface SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "UNION", + "description" : "Indicates the directive is valid on an union SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "ENUM", + "description" : "Indicates the directive is valid on an enum SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "ENUM_VALUE", + "description" : "Indicates the directive is valid on an enum value SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "INPUT_OBJECT", + "description" : "Indicates the directive is valid on an input object SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + }, { + "name" : "INPUT_FIELD_DEFINITION", + "description" : "Indicates the directive is valid on an input object field SDL definition.", + "isDeprecated" : false, + "deprecationReason" : null + } ], + "possibleTypes" : null + } ], + "directives" : [ { + "name" : "include", + "description" : "Directs the executor to include this field or fragment only when the `if` argument is true", + "locations" : [ "FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT" ], + "args" : [ { + "name" : "if", + "description" : "Included when true.", + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "onOperation" : false, + "onFragment" : true, + "onField" : true + }, { + "name" : "skip", + "description" : "Directs the executor to skip this field or fragment when the `if`'argument is true.", + "locations" : [ "FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT" ], + "args" : [ { + "name" : "if", + "description" : "Skipped when true.", + "type" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "onOperation" : false, + "onFragment" : true, + "onField" : true + }, { + "name" : "defer", + "description" : "This directive allows results to be deferred during execution", + "locations" : [ "FIELD" ], + "args" : [ ], + "onOperation" : false, + "onFragment" : false, + "onField" : true + }, { + "name" : "aws_oidc", + "description" : "Tells the service this field/object has access authorized by an OIDC token.", + "locations" : [ "OBJECT", "FIELD_DEFINITION" ], + "args" : [ ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "deprecated", + "description" : null, + "locations" : [ "FIELD_DEFINITION", "ENUM_VALUE" ], + "args" : [ { + "name" : "reason", + "description" : null, + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : "\"No longer supported\"" + } ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "aws_lambda", + "description" : "Tells the service this field/object has access authorized by a Lambda Authorizer.", + "locations" : [ "OBJECT", "FIELD_DEFINITION" ], + "args" : [ ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "aws_subscribe", + "description" : "Tells the service which mutation triggers this subscription.", + "locations" : [ "FIELD_DEFINITION" ], + "args" : [ { + "name" : "mutations", + "description" : "List of mutations which will trigger this subscription when they are called.", + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "aws_cognito_user_pools", + "description" : "Tells the service this field/object has access authorized by a Cognito User Pools token.", + "locations" : [ "OBJECT", "FIELD_DEFINITION" ], + "args" : [ { + "name" : "cognito_groups", + "description" : "List of cognito user pool groups which have access on this field", + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "aws_iam", + "description" : "Tells the service this field/object has access authorized by sigv4 signing.", + "locations" : [ "OBJECT", "FIELD_DEFINITION" ], + "args" : [ ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "aws_publish", + "description" : "Tells the service which subscriptions will be published to when this mutation is called. This directive is deprecated use @aws_susbscribe directive instead.", + "locations" : [ "FIELD_DEFINITION" ], + "args" : [ { + "name" : "subscriptions", + "description" : "List of subscriptions which will be published to when this mutation is called.", + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "aws_auth", + "description" : "Directs the schema to enforce authorization on a field", + "locations" : [ "FIELD_DEFINITION" ], + "args" : [ { + "name" : "cognito_groups", + "description" : "List of cognito user pool groups which have access on this field", + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + } + }, + "defaultValue" : null + } ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + }, { + "name" : "aws_api_key", + "description" : "Tells the service this field/object has access authorized by an API key.", + "locations" : [ "OBJECT", "FIELD_DEFINITION" ], + "args" : [ ], + "onOperation" : false, + "onFragment" : false, + "onField" : false + } ] + } + } +} \ No newline at end of file diff --git a/lib/user-interface/react-app/src/API.ts b/lib/user-interface/react-app/src/API.ts new file mode 100644 index 000000000..75dd2c56d --- /dev/null +++ b/lib/user-interface/react-app/src/API.ts @@ -0,0 +1,977 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +export type Channel = { + __typename: "Channel", + data?: string | null, + sessionId?: string | null, + userId?: string | null, +}; + +export type CreateWorkspaceKendraInput = { + name: string, + kind: string, + kendraIndexId: string, + useAllData: boolean, +}; + +export type Workspace = { + __typename: "Workspace", + id: string, + name: string, + formatVersion?: number | null, + engine: string, + status?: string | null, + aossEngine?: string | null, + languages?: Array< string | null > | null, + hasIndex?: boolean | null, + embeddingsModelProvider?: string | null, + embeddingsModelName?: string | null, + embeddingsModelDimensions?: number | null, + crossEncoderModelName?: string | null, + crossEncoderModelProvider?: string | null, + metric?: string | null, + index?: boolean | null, + hybridSearch?: boolean | null, + chunkingStrategy?: string | null, + chunkSize?: number | null, + chunkOverlap?: number | null, + vectors?: number | null, + documents?: number | null, + sizeInBytes?: number | null, + kendraIndexId?: string | null, + kendraIndexExternal?: string | null, + kendraUseAllData?: boolean | null, + createdAt: string, + updatedAt: string, +}; + +export type CreateWorkspaceOpenSearchInput = { + name: string, + kind: string, + embeddingsModelProvider: string, + embeddingsModelName: string, + crossEncoderModelProvider: string, + crossEncoderModelName: string, + languages: Array< string >, + hybridSearch: boolean, + chunkingStrategy: string, + chunkSize: number, + chunkOverlap: number, +}; + +export type CreateWorkspaceAuroraInput = { + name: string, + kind: string, + embeddingsModelProvider: string, + embeddingsModelName: string, + crossEncoderModelProvider: string, + crossEncoderModelName: string, + languages: Array< string >, + metric: string, + index: boolean, + hybridSearch: boolean, + chunkingStrategy: string, + chunkSize: number, + chunkOverlap: number, +}; + +export type TextDocumentInput = { + workspaceId: string, + title: string, + content: string, +}; + +export type DocumentResult = { + __typename: "DocumentResult", + workspaceId: string, + documentId: string, + status?: string | null, +}; + +export type QnADocumentInput = { + workspaceId: string, + question: string, + answer: string, +}; + +export type DocumentSubscriptionStatusInput = { + workspaceId: string, + documentId: string, + status: string, +}; + +export type WebsiteInput = { + workspaceId: string, + sitemap: boolean, + address: string, + followLinks: boolean, + limit: number, +}; + +export type RssFeedInput = { + workspaceId: string, + address: string, + limit: number, + title?: string | null, + followLinks: boolean, +}; + +export type DeleteSessionResult = { + __typename: "DeleteSessionResult", + id?: string | null, + deleted: boolean, +}; + +export type FileUploadInput = { + workspaceId: string, + fileName: string, +}; + +export type FileUploadResult = { + __typename: "FileUploadResult", + url: string, + fields?: string | null, +}; + +export type Model = { + __typename: "Model", + name: string, + provider: string, + interface: string, + ragSupported: boolean, + inputModalities: Array< string >, + outputModalities: Array< string >, + streaming: boolean, +}; + +export type RagEngine = { + __typename: "RagEngine", + id: string, + name: string, + enabled: boolean, +}; + +export type SemanticSearchInput = { + workspaceId: string, + query: string, +}; + +export type SemanticSearchResult = { + __typename: "SemanticSearchResult", + engine: string, + workspaceId: string, + queryLanguage?: string | null, + supportedLanguages?: Array< string > | null, + detectedLanguages?: Array | null, + items?: Array | null, + vectorSearchMetric?: string | null, + vectorSearchItems?: Array | null, + keywordSearchItems?: Array | null, +}; + +export type DetectedLanguage = { + __typename: "DetectedLanguage", + code: string, + score: number, +}; + +export type SemanticSearchItem = { + __typename: "SemanticSearchItem", + sources?: Array< string | null > | null, + chunkId?: string | null, + workspaceId: string, + documentId?: string | null, + documentSubId?: string | null, + documentSubType?: string | null, + documentType: string, + path?: string | null, + language?: string | null, + title?: string | null, + content?: string | null, + contentComplement?: string | null, + vectorSearchScore?: number | null, + keywordSearchScore?: number | null, + score?: number | null, +}; + +export type Session = { + __typename: "Session", + id: string, + title?: string | null, + startTime: string, + history?: Array | null, +}; + +export type SessionHistoryItem = { + __typename: "SessionHistoryItem", + type: string, + content: string, + metadata?: string | null, +}; + +export type EmbeddingModel = { + __typename: "EmbeddingModel", + provider: string, + name: string, + dimensions: number, + default?: boolean | null, +}; + +export type CalculateEmbeddingsInput = { + provider: string, + model: string, + passages: Array< string | null >, +}; + +export type Embedding = { + __typename: "Embedding", + passage?: string | null, + vector: Array< number >, +}; + +export type KendraIndex = { + __typename: "KendraIndex", + id: string, + name: string, + external: boolean, +}; + +export type ListDocumentsInput = { + workspaceId: string, + documentType: string, + lastDocumentId?: string | null, +}; + +export type DocumentsResult = { + __typename: "DocumentsResult", + items: Array, + lastDocumentId?: string | null, +}; + +export type Document = { + __typename: "Document", + workspaceId: string, + id: string, + type: string, + subType?: string | null, + status?: string | null, + title?: string | null, + path?: string | null, + sizeInBytes?: number | null, + vectors?: number | null, + subDocuments?: number | null, + crawlerProperties?: CrawlerProperties | null, + errors?: Array< string > | null, + createdAt: string, + updatedAt?: string | null, + rssFeedId?: string | null, + rssLastCheckedAt?: string | null, +}; + +export type CrawlerProperties = { + __typename: "CrawlerProperties", + followLinks?: boolean | null, + limit?: number | null, +}; + +export type GetDocumentInput = { + workspaceId: string, + documentId: string, +}; + +export type GetRSSPostsInput = { + workspaceId: string, + documentId: string, + lastDocumentId?: string | null, +}; + +export type CrossEncoderData = { + __typename: "CrossEncoderData", + provider: string, + name: string, + default: boolean, +}; + +export type RankPassagesInput = { + provider: string, + model: string, + reference: string, + passages: Array< string | null >, +}; + +export type PassageRank = { + __typename: "PassageRank", + score: number, + passage: string, +}; + +export type SendQueryMutationVariables = { + data?: string | null, +}; + +export type SendQueryMutation = { + sendQuery?: string | null, +}; + +export type PublishResponseMutationVariables = { + sessionId?: string | null, + userId?: string | null, + data?: string | null, +}; + +export type PublishResponseMutation = { + publishResponse?: { + __typename: "Channel", + data?: string | null, + sessionId?: string | null, + userId?: string | null, + } | null, +}; + +export type CreateKendraWorkspaceMutationVariables = { + input: CreateWorkspaceKendraInput, +}; + +export type CreateKendraWorkspaceMutation = { + createKendraWorkspace: { + __typename: "Workspace", + id: string, + name: string, + formatVersion?: number | null, + engine: string, + status?: string | null, + aossEngine?: string | null, + languages?: Array< string | null > | null, + hasIndex?: boolean | null, + embeddingsModelProvider?: string | null, + embeddingsModelName?: string | null, + embeddingsModelDimensions?: number | null, + crossEncoderModelName?: string | null, + crossEncoderModelProvider?: string | null, + metric?: string | null, + index?: boolean | null, + hybridSearch?: boolean | null, + chunkingStrategy?: string | null, + chunkSize?: number | null, + chunkOverlap?: number | null, + vectors?: number | null, + documents?: number | null, + sizeInBytes?: number | null, + kendraIndexId?: string | null, + kendraIndexExternal?: string | null, + kendraUseAllData?: boolean | null, + createdAt: string, + updatedAt: string, + }, +}; + +export type CreateOpenSearchWorkspaceMutationVariables = { + input: CreateWorkspaceOpenSearchInput, +}; + +export type CreateOpenSearchWorkspaceMutation = { + createOpenSearchWorkspace: { + __typename: "Workspace", + id: string, + name: string, + formatVersion?: number | null, + engine: string, + status?: string | null, + aossEngine?: string | null, + languages?: Array< string | null > | null, + hasIndex?: boolean | null, + embeddingsModelProvider?: string | null, + embeddingsModelName?: string | null, + embeddingsModelDimensions?: number | null, + crossEncoderModelName?: string | null, + crossEncoderModelProvider?: string | null, + metric?: string | null, + index?: boolean | null, + hybridSearch?: boolean | null, + chunkingStrategy?: string | null, + chunkSize?: number | null, + chunkOverlap?: number | null, + vectors?: number | null, + documents?: number | null, + sizeInBytes?: number | null, + kendraIndexId?: string | null, + kendraIndexExternal?: string | null, + kendraUseAllData?: boolean | null, + createdAt: string, + updatedAt: string, + }, +}; + +export type CreateAuroraWorkspaceMutationVariables = { + input: CreateWorkspaceAuroraInput, +}; + +export type CreateAuroraWorkspaceMutation = { + createAuroraWorkspace: { + __typename: "Workspace", + id: string, + name: string, + formatVersion?: number | null, + engine: string, + status?: string | null, + aossEngine?: string | null, + languages?: Array< string | null > | null, + hasIndex?: boolean | null, + embeddingsModelProvider?: string | null, + embeddingsModelName?: string | null, + embeddingsModelDimensions?: number | null, + crossEncoderModelName?: string | null, + crossEncoderModelProvider?: string | null, + metric?: string | null, + index?: boolean | null, + hybridSearch?: boolean | null, + chunkingStrategy?: string | null, + chunkSize?: number | null, + chunkOverlap?: number | null, + vectors?: number | null, + documents?: number | null, + sizeInBytes?: number | null, + kendraIndexId?: string | null, + kendraIndexExternal?: string | null, + kendraUseAllData?: boolean | null, + createdAt: string, + updatedAt: string, + }, +}; + +export type StartKendraDataSyncMutationVariables = { + workspaceId: string, +}; + +export type StartKendraDataSyncMutation = { + startKendraDataSync?: boolean | null, +}; + +export type DeleteWorkspaceMutationVariables = { + workspaceId: string, +}; + +export type DeleteWorkspaceMutation = { + deleteWorkspace?: boolean | null, +}; + +export type AddTextDocumentMutationVariables = { + input: TextDocumentInput, +}; + +export type AddTextDocumentMutation = { + addTextDocument?: { + __typename: "DocumentResult", + workspaceId: string, + documentId: string, + status?: string | null, + } | null, +}; + +export type AddQnADocumentMutationVariables = { + input: QnADocumentInput, +}; + +export type AddQnADocumentMutation = { + addQnADocument?: { + __typename: "DocumentResult", + workspaceId: string, + documentId: string, + status?: string | null, + } | null, +}; + +export type SetDocumentSubscriptionStatusMutationVariables = { + input: DocumentSubscriptionStatusInput, +}; + +export type SetDocumentSubscriptionStatusMutation = { + setDocumentSubscriptionStatus?: { + __typename: "DocumentResult", + workspaceId: string, + documentId: string, + status?: string | null, + } | null, +}; + +export type AddWebsiteMutationVariables = { + input: WebsiteInput, +}; + +export type AddWebsiteMutation = { + addWebsite?: { + __typename: "DocumentResult", + workspaceId: string, + documentId: string, + status?: string | null, + } | null, +}; + +export type AddRssFeedMutationVariables = { + input: RssFeedInput, +}; + +export type AddRssFeedMutation = { + addRssFeed?: { + __typename: "DocumentResult", + workspaceId: string, + documentId: string, + status?: string | null, + } | null, +}; + +export type UpdateRssFeedMutationVariables = { + input: RssFeedInput, +}; + +export type UpdateRssFeedMutation = { + updateRssFeed?: { + __typename: "DocumentResult", + workspaceId: string, + documentId: string, + status?: string | null, + } | null, +}; + +export type DeleteUserSessionsMutationVariables = { +}; + +export type DeleteUserSessionsMutation = { + deleteUserSessions?: Array< { + __typename: "DeleteSessionResult", + id?: string | null, + deleted: boolean, + } > | null, +}; + +export type DeleteSessionMutationVariables = { + id: string, +}; + +export type DeleteSessionMutation = { + deleteSession?: { + __typename: "DeleteSessionResult", + id?: string | null, + deleted: boolean, + } | null, +}; + +export type NoneQueryVariables = { +}; + +export type NoneQuery = { + none?: string | null, +}; + +export type CheckHealthQueryVariables = { +}; + +export type CheckHealthQuery = { + checkHealth?: boolean | null, +}; + +export type GetUploadFileURLQueryVariables = { + input: FileUploadInput, +}; + +export type GetUploadFileURLQuery = { + getUploadFileURL?: { + __typename: "FileUploadResult", + url: string, + fields?: string | null, + } | null, +}; + +export type ListModelsQueryVariables = { +}; + +export type ListModelsQuery = { + listModels: Array< { + __typename: "Model", + name: string, + provider: string, + interface: string, + ragSupported: boolean, + inputModalities: Array< string >, + outputModalities: Array< string >, + streaming: boolean, + } >, +}; + +export type ListWorkspacesQueryVariables = { +}; + +export type ListWorkspacesQuery = { + listWorkspaces: Array< { + __typename: "Workspace", + id: string, + name: string, + formatVersion?: number | null, + engine: string, + status?: string | null, + aossEngine?: string | null, + languages?: Array< string | null > | null, + hasIndex?: boolean | null, + embeddingsModelProvider?: string | null, + embeddingsModelName?: string | null, + embeddingsModelDimensions?: number | null, + crossEncoderModelName?: string | null, + crossEncoderModelProvider?: string | null, + metric?: string | null, + index?: boolean | null, + hybridSearch?: boolean | null, + chunkingStrategy?: string | null, + chunkSize?: number | null, + chunkOverlap?: number | null, + vectors?: number | null, + documents?: number | null, + sizeInBytes?: number | null, + kendraIndexId?: string | null, + kendraIndexExternal?: string | null, + kendraUseAllData?: boolean | null, + createdAt: string, + updatedAt: string, + } >, +}; + +export type GetWorkspaceQueryVariables = { + workspaceId: string, +}; + +export type GetWorkspaceQuery = { + getWorkspace?: { + __typename: "Workspace", + id: string, + name: string, + formatVersion?: number | null, + engine: string, + status?: string | null, + aossEngine?: string | null, + languages?: Array< string | null > | null, + hasIndex?: boolean | null, + embeddingsModelProvider?: string | null, + embeddingsModelName?: string | null, + embeddingsModelDimensions?: number | null, + crossEncoderModelName?: string | null, + crossEncoderModelProvider?: string | null, + metric?: string | null, + index?: boolean | null, + hybridSearch?: boolean | null, + chunkingStrategy?: string | null, + chunkSize?: number | null, + chunkOverlap?: number | null, + vectors?: number | null, + documents?: number | null, + sizeInBytes?: number | null, + kendraIndexId?: string | null, + kendraIndexExternal?: string | null, + kendraUseAllData?: boolean | null, + createdAt: string, + updatedAt: string, + } | null, +}; + +export type ListRagEnginesQueryVariables = { +}; + +export type ListRagEnginesQuery = { + listRagEngines: Array< { + __typename: "RagEngine", + id: string, + name: string, + enabled: boolean, + } >, +}; + +export type PerformSemanticSearchQueryVariables = { + input: SemanticSearchInput, +}; + +export type PerformSemanticSearchQuery = { + performSemanticSearch: { + __typename: "SemanticSearchResult", + engine: string, + workspaceId: string, + queryLanguage?: string | null, + supportedLanguages?: Array< string > | null, + detectedLanguages?: Array< { + __typename: "DetectedLanguage", + code: string, + score: number, + } > | null, + items?: Array< { + __typename: "SemanticSearchItem", + sources?: Array< string | null > | null, + chunkId?: string | null, + workspaceId: string, + documentId?: string | null, + documentSubId?: string | null, + documentSubType?: string | null, + documentType: string, + path?: string | null, + language?: string | null, + title?: string | null, + content?: string | null, + contentComplement?: string | null, + vectorSearchScore?: number | null, + keywordSearchScore?: number | null, + score?: number | null, + } > | null, + vectorSearchMetric?: string | null, + vectorSearchItems?: Array< { + __typename: "SemanticSearchItem", + sources?: Array< string | null > | null, + chunkId?: string | null, + workspaceId: string, + documentId?: string | null, + documentSubId?: string | null, + documentSubType?: string | null, + documentType: string, + path?: string | null, + language?: string | null, + title?: string | null, + content?: string | null, + contentComplement?: string | null, + vectorSearchScore?: number | null, + keywordSearchScore?: number | null, + score?: number | null, + } > | null, + keywordSearchItems?: Array< { + __typename: "SemanticSearchItem", + sources?: Array< string | null > | null, + chunkId?: string | null, + workspaceId: string, + documentId?: string | null, + documentSubId?: string | null, + documentSubType?: string | null, + documentType: string, + path?: string | null, + language?: string | null, + title?: string | null, + content?: string | null, + contentComplement?: string | null, + vectorSearchScore?: number | null, + keywordSearchScore?: number | null, + score?: number | null, + } > | null, + }, +}; + +export type ListSessionsQueryVariables = { +}; + +export type ListSessionsQuery = { + listSessions: Array< { + __typename: "Session", + id: string, + title?: string | null, + startTime: string, + history?: Array< { + __typename: "SessionHistoryItem", + type: string, + content: string, + metadata?: string | null, + } | null > | null, + } >, +}; + +export type ListEmbeddingModelsQueryVariables = { +}; + +export type ListEmbeddingModelsQuery = { + listEmbeddingModels: Array< { + __typename: "EmbeddingModel", + provider: string, + name: string, + dimensions: number, + default?: boolean | null, + } >, +}; + +export type CalculateEmbeddingsQueryVariables = { + input: CalculateEmbeddingsInput, +}; + +export type CalculateEmbeddingsQuery = { + calculateEmbeddings: Array< { + __typename: "Embedding", + passage?: string | null, + vector: Array< number >, + } | null >, +}; + +export type GetSessionQueryVariables = { + id: string, +}; + +export type GetSessionQuery = { + getSession?: { + __typename: "Session", + id: string, + title?: string | null, + startTime: string, + history?: Array< { + __typename: "SessionHistoryItem", + type: string, + content: string, + metadata?: string | null, + } | null > | null, + } | null, +}; + +export type ListKendraIndexesQueryVariables = { +}; + +export type ListKendraIndexesQuery = { + listKendraIndexes: Array< { + __typename: "KendraIndex", + id: string, + name: string, + external: boolean, + } >, +}; + +export type IsKendraDataSynchingQueryVariables = { + workspaceId: string, +}; + +export type IsKendraDataSynchingQuery = { + isKendraDataSynching?: boolean | null, +}; + +export type ListDocumentsQueryVariables = { + input: ListDocumentsInput, +}; + +export type ListDocumentsQuery = { + listDocuments: { + __typename: "DocumentsResult", + items: Array< { + __typename: "Document", + workspaceId: string, + id: string, + type: string, + subType?: string | null, + status?: string | null, + title?: string | null, + path?: string | null, + sizeInBytes?: number | null, + vectors?: number | null, + subDocuments?: number | null, + crawlerProperties?: { + __typename: "CrawlerProperties", + followLinks?: boolean | null, + limit?: number | null, + } | null, + errors?: Array< string > | null, + createdAt: string, + updatedAt?: string | null, + rssFeedId?: string | null, + rssLastCheckedAt?: string | null, + } | null >, + lastDocumentId?: string | null, + }, +}; + +export type GetDocumentQueryVariables = { + input: GetDocumentInput, +}; + +export type GetDocumentQuery = { + getDocument?: { + __typename: "Document", + workspaceId: string, + id: string, + type: string, + subType?: string | null, + status?: string | null, + title?: string | null, + path?: string | null, + sizeInBytes?: number | null, + vectors?: number | null, + subDocuments?: number | null, + crawlerProperties?: { + __typename: "CrawlerProperties", + followLinks?: boolean | null, + limit?: number | null, + } | null, + errors?: Array< string > | null, + createdAt: string, + updatedAt?: string | null, + rssFeedId?: string | null, + rssLastCheckedAt?: string | null, + } | null, +}; + +export type GetRSSPostsQueryVariables = { + input: GetRSSPostsInput, +}; + +export type GetRSSPostsQuery = { + getRSSPosts?: { + __typename: "DocumentsResult", + items: Array< { + __typename: "Document", + workspaceId: string, + id: string, + type: string, + subType?: string | null, + status?: string | null, + title?: string | null, + path?: string | null, + sizeInBytes?: number | null, + vectors?: number | null, + subDocuments?: number | null, + crawlerProperties?: { + __typename: "CrawlerProperties", + followLinks?: boolean | null, + limit?: number | null, + } | null, + errors?: Array< string > | null, + createdAt: string, + updatedAt?: string | null, + rssFeedId?: string | null, + rssLastCheckedAt?: string | null, + } | null >, + lastDocumentId?: string | null, + } | null, +}; + +export type ListCrossEncodersQueryVariables = { +}; + +export type ListCrossEncodersQuery = { + listCrossEncoders?: Array< { + __typename: "CrossEncoderData", + provider: string, + name: string, + default: boolean, + } > | null, +}; + +export type RankPassagesQueryVariables = { + input: RankPassagesInput, +}; + +export type RankPassagesQuery = { + rankPassages: Array< { + __typename: "PassageRank", + score: number, + passage: string, + } >, +}; + +export type ReceiveMessagesSubscriptionVariables = { + sessionId?: string | null, +}; + +export type ReceiveMessagesSubscription = { + receiveMessages?: { + __typename: "Channel", + data?: string | null, + sessionId?: string | null, + userId?: string | null, + } | null, +}; diff --git a/lib/user-interface/react-app/src/app.tsx b/lib/user-interface/react-app/src/app.tsx index db142a2db..0fb4b3992 100644 --- a/lib/user-interface/react-app/src/app.tsx +++ b/lib/user-interface/react-app/src/app.tsx @@ -10,7 +10,7 @@ import CrossEncoders from "./pages/rag/cross-encoders/cross-encoders"; import Welcome from "./pages/welcome"; import Playground from "./pages/chatbot/playground/playground"; import Models from "./pages/chatbot/models/models"; -import Workspace from "./pages/rag/workspace/workspace"; +import WorkspacePane from "./pages/rag/workspace/workspace"; import SemanticSearch from "./pages/rag/semantic-search/semantic-search"; import AddData from "./pages/rag/add-data/add-data"; import "./styles/app.scss"; @@ -40,7 +40,10 @@ function App() { } /> } /> } /> - } /> + } + /> } diff --git a/lib/user-interface/react-app/src/common/api-client/api-client-base.ts b/lib/user-interface/react-app/src/common/api-client/api-client-base.ts deleted file mode 100644 index 9ca498938..000000000 --- a/lib/user-interface/react-app/src/common/api-client/api-client-base.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Auth } from "aws-amplify"; -import { ApiErrorResult, AppConfig } from "../types"; - -export abstract class ApiClientBase { - constructor(protected _appConfig: AppConfig) {} - - protected getApiUrl(url: string) { - let apiEndpoint = this._appConfig.config.api_endpoint; - if (apiEndpoint.endsWith("/")) { - apiEndpoint = apiEndpoint.slice(0, -1); - } - - return `${apiEndpoint}/v1${url}`; - } - - protected async getHeaders() { - return { - Authorization: `Bearer ${await this.getIdToken()}`, - }; - } - - protected async getIdToken() { - const session = await Auth.currentSession(); - return session.getIdToken().getJwtToken(); - } - - protected error(error: unknown): ApiErrorResult { - console.error(error); - if (error instanceof Error) { - return { error: true, message: error.message }; - } else { - return { error: true, message: error!.toString() }; - } - } -} diff --git a/lib/user-interface/react-app/src/common/api-client/api-client.ts b/lib/user-interface/react-app/src/common/api-client/api-client.ts index 26b91a709..fe68c544f 100644 --- a/lib/user-interface/react-app/src/common/api-client/api-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/api-client.ts @@ -24,7 +24,7 @@ export class ApiClient { public get health() { if (!this._healthClient) { - this._healthClient = new HealthClient(this._appConfig); + this._healthClient = new HealthClient(); } return this._healthClient; @@ -32,7 +32,7 @@ export class ApiClient { public get ragEngines() { if (!this._ragEnginesClient) { - this._ragEnginesClient = new RagEnginesClient(this._appConfig); + this._ragEnginesClient = new RagEnginesClient(); } return this._ragEnginesClient; @@ -40,7 +40,7 @@ export class ApiClient { public get embeddings() { if (!this._embeddingsClient) { - this._embeddingsClient = new EmbeddingsClient(this._appConfig); + this._embeddingsClient = new EmbeddingsClient(); } return this._embeddingsClient; @@ -48,7 +48,7 @@ export class ApiClient { public get crossEncoders() { if (!this._crossEncodersClient) { - this._crossEncodersClient = new CrossEncodersClient(this._appConfig); + this._crossEncodersClient = new CrossEncodersClient(); } return this._crossEncodersClient; @@ -56,7 +56,7 @@ export class ApiClient { public get models() { if (!this._modelsClient) { - this._modelsClient = new ModelsClient(this._appConfig); + this._modelsClient = new ModelsClient(); } return this._modelsClient; @@ -64,7 +64,7 @@ export class ApiClient { public get workspaces() { if (!this._workspacesClient) { - this._workspacesClient = new WorkspacesClient(this._appConfig); + this._workspacesClient = new WorkspacesClient(); } return this._workspacesClient; @@ -72,7 +72,7 @@ export class ApiClient { public get sessions() { if (!this._sessionsClient) { - this._sessionsClient = new SessionsClient(this._appConfig); + this._sessionsClient = new SessionsClient(); } return this._sessionsClient; @@ -80,7 +80,7 @@ export class ApiClient { public get semanticSearch() { if (!this._semanticSearchClient) { - this._semanticSearchClient = new SemanticSearchClient(this._appConfig); + this._semanticSearchClient = new SemanticSearchClient(); } return this._semanticSearchClient; @@ -88,7 +88,7 @@ export class ApiClient { public get documents() { if (!this._documentsClient) { - this._documentsClient = new DocumentsClient(this._appConfig); + this._documentsClient = new DocumentsClient(); } return this._documentsClient; @@ -96,7 +96,7 @@ export class ApiClient { public get kendra() { if (!this._kendraClient) { - this._kendraClient = new KendraClient(this._appConfig); + this._kendraClient = new KendraClient(); } return this._kendraClient; diff --git a/lib/user-interface/react-app/src/common/api-client/cross-encoders-client.ts b/lib/user-interface/react-app/src/common/api-client/cross-encoders-client.ts index bfdf4bf56..f55cc7889 100644 --- a/lib/user-interface/react-app/src/common/api-client/cross-encoders-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/cross-encoders-client.ts @@ -1,18 +1,16 @@ -import { ApiResult, CrossEncoderModelItem } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { listCrossEncoders, rankPassages } from "../../graphql/queries"; +import { ListCrossEncodersQuery, RankPassagesQuery } from "../../API"; -export class CrossEncodersClient extends ApiClientBase { - async getModels(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/cross-encoders/models"), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } +export class CrossEncodersClient { + async getModels(): Promise< + GraphQLResult> + > { + const result = await API.graphql>({ + query: listCrossEncoders, + }); + return result; } async getRanking( @@ -20,18 +18,18 @@ export class CrossEncodersClient extends ApiClientBase { model: string, input: string, passages: string[] - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/cross-encoders"), { - method: "POST", - headers, - body: JSON.stringify({ provider, model, input, passages }), - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = await API.graphql>({ + query: rankPassages, + variables: { + input: { + model: model, + passages: passages, + provider: provider, + reference: input, + }, + }, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/documents-client.ts b/lib/user-interface/react-app/src/common/api-client/documents-client.ts index 7646bbe4f..ac0b440f0 100644 --- a/lib/user-interface/react-app/src/common/api-client/documents-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/documents-client.ts @@ -1,123 +1,117 @@ +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; import { - AddDocumentResult, - ApiResult, - DocumentResult, - DocumentSubscriptionToggleResult, - FileUploadItem, - RagDocumentType, -} from "../types"; -import { ApiClientBase } from "./api-client-base"; - -export class DocumentsClient extends ApiClientBase { + getDocument, + listDocuments, + getUploadFileURL, + getRSSPosts, +} from "../../graphql/queries"; +import { + addQnADocument, + addRssFeed, + addTextDocument, + addWebsite, + setDocumentSubscriptionStatus, +} from "../../graphql/mutations"; +import { + AddQnADocumentMutation, + AddRssFeedMutation, + AddTextDocumentMutation, + AddWebsiteMutation, + SetDocumentSubscriptionStatusMutation, + GetDocumentQuery, + ListDocumentsQuery, + GetRSSPostsQuery, + GetUploadFileURLQuery, + UpdateRssFeedMutation, +} from "../../API"; +import { RagDocumentType } from "../types"; + +export class DocumentsClient { async presignedFileUploadPost( workspaceId: string, fileName: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl(`/workspaces/${workspaceId}/documents/file-upload`), - { - method: "POST", - headers, - body: JSON.stringify({ fileName }), - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: getUploadFileURL, + variables: { + input: { + workspaceId, + fileName, + }, + }, + }); + return result; } async getDocuments( workspaceId: string, documentType: RagDocumentType, lastDocumentId?: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - lastDocumentId - ? this.getApiUrl( - `/workspaces/${workspaceId}/documents/${documentType}?lastDocumentId=${lastDocumentId}` - ) - : this.getApiUrl( - `/workspaces/${workspaceId}/documents/${documentType}` - ), - { - headers, - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: listDocuments, + variables: { + input: { + workspaceId, + documentType, + lastDocumentId, + }, + }, + }); + return result; } async getDocumentDetails( workspaceId: string, documentId: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl( - `/workspaces/${workspaceId}/documents/${documentId}/detail` - ), - { - headers, - } - ); - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: getDocument, + variables: { + input: { + workspaceId, + documentId, + }, + }, + }); + return result; } async addTextDocument( workspaceId: string, title: string, content: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl(`/workspaces/${workspaceId}/documents/text`), - { - method: "POST", - headers, - body: JSON.stringify({ title, content }), - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: addTextDocument, + variables: { + input: { + workspaceId, + title, + content, + }, + }, + }); + return result; } async addQnADocument( workspaceId: string, question: string, answer: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl(`/workspaces/${workspaceId}/documents/qna`), - { - method: "POST", - headers, - body: JSON.stringify({ question, answer }), - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: addQnADocument, + variables: { + input: { + workspaceId, + question, + answer, + }, + }, + }); + return result; } async addWebsiteDocument( @@ -126,22 +120,20 @@ export class DocumentsClient extends ApiClientBase { address: string, followLinks: boolean, limit: number - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl(`/workspaces/${workspaceId}/documents/website`), - { - method: "POST", - headers, - body: JSON.stringify({ sitemap, address, followLinks, limit }), - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: addWebsite, + variables: { + input: { + workspaceId, + sitemap, + address, + followLinks, + limit, + }, + }, + }); + return result; } async addRssFeedSubscription( @@ -150,85 +142,80 @@ export class DocumentsClient extends ApiClientBase { title: string, limit: number, followLinks: boolean - ): Promise> { - try { - const headers = await this.getHeaders(); - const results = await fetch( - this.getApiUrl(`/workspaces/${workspaceId}/documents/rssfeed`), - { - headers: headers, - method: "POST", - body: JSON.stringify({ address, title, limit, followLinks }), - } - ); - return results.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: addRssFeed, + variables: { + input: { + workspaceId, + address, + title, + limit, + followLinks, + }, + }, + }); + return result; } async getRssSubscriptionPosts( workspaceId: string, feedId: string, lastDocumentId?: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - lastDocumentId - ? this.getApiUrl( - `/workspaces/${workspaceId}/documents/${feedId}/posts?lastDocumentId=${lastDocumentId}` - ) - : this.getApiUrl( - `/workspaces/${workspaceId}/documents/${feedId}/posts` - ), - { - headers, - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: getRSSPosts, + variables: { + input: { + workspaceId, + documentId: feedId, + lastDocumentId, + }, + }, + }); + return result; } async disableRssSubscription( workspaceId: string, feedId: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const results = await fetch( - this.getApiUrl( - `/workspaces/${workspaceId}/documents/${feedId}/disable` - ), - { - headers: headers, - } - ); - return results.json(); - } catch (error) { - return this.error(error); - } + ): Promise< + GraphQLResult> + > { + const result = API.graphql< + GraphQLQuery + >({ + query: setDocumentSubscriptionStatus, + variables: { + input: { + workspaceId, + documentId: feedId, + status: "disabled", + }, + }, + }); + return result; } async enableRssSubscription( workspaceId: string, feedId: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const results = await fetch( - this.getApiUrl(`/workspaces/${workspaceId}/documents/${feedId}/enable`), - { - headers: headers, - } - ); - return results.json(); - } catch (error) { - return this.error(error); - } + ): Promise< + GraphQLResult> + > { + const result = API.graphql< + GraphQLQuery + >({ + query: setDocumentSubscriptionStatus, + variables: { + input: { + workspaceId, + documentId: feedId, + status: "enabled", + }, + }, + }); + return result; } async updateRssSubscriptionCrawler( @@ -236,20 +223,18 @@ export class DocumentsClient extends ApiClientBase { feedId: string, followLinks: boolean, limit: number - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl(`/workspaces/${workspaceId}/documents/${feedId}`), - { - method: "PATCH", - headers: headers, - body: JSON.stringify({ followLinks, limit, documentType: "rssfeed" }), - } - ); - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: addRssFeed, + variables: { + input: { + workspaceId, + documentId: feedId, + followLinks, + limit, + }, + }, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/embeddings-client.ts b/lib/user-interface/react-app/src/common/api-client/embeddings-client.ts index b4b514f80..4cc9526fd 100644 --- a/lib/user-interface/react-app/src/common/api-client/embeddings-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/embeddings-client.ts @@ -1,36 +1,36 @@ -import { ApiResult, EmbeddingsModelItem } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { + listEmbeddingModels, + calculateEmbeddings, +} from "../../graphql/queries"; +import { ListEmbeddingModelsQuery, CalculateEmbeddingsQuery } from "../../API"; -export class EmbeddingsClient extends ApiClientBase { - async getModels(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/embeddings/models"), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } +export class EmbeddingsClient { + async getModels(): Promise< + GraphQLResult> + > { + const result = await API.graphql>({ + query: listEmbeddingModels, + }); + return result; } async getEmbeddings( provider: string, model: string, input: string[] - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/embeddings"), { - method: "POST", - headers, - body: JSON.stringify({ provider, model, input }), - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = API.graphql>({ + query: calculateEmbeddings, + variables: { + input: { + provider: provider, + model: model, + passages: input, + }, + }, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/health-client.ts b/lib/user-interface/react-app/src/common/api-client/health-client.ts index 28f24caaa..524946b11 100644 --- a/lib/user-interface/react-app/src/common/api-client/health-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/health-client.ts @@ -1,17 +1,13 @@ -import { ApiResult } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { checkHealth } from "../../graphql/queries"; +import { CheckHealthQuery } from "../../API"; -export class HealthClient extends ApiClientBase { - async health(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/health"), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } +export class HealthClient { + async health(): Promise>> { + const result = await API.graphql>({ + query: checkHealth, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/kendra-client.ts b/lib/user-interface/react-app/src/common/api-client/kendra-client.ts index a2a06d404..3ef4139a2 100644 --- a/lib/user-interface/react-app/src/common/api-client/kendra-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/kendra-client.ts @@ -1,55 +1,46 @@ -import { ApiResult, KendraIndexItem } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { listKendraIndexes, isKendraDataSynching } from "../../graphql/queries"; +import { startKendraDataSync } from "../../graphql/mutations"; +import { + ListKendraIndexesQuery, + IsKendraDataSynchingQuery, + StartKendraDataSyncMutation, +} from "../../API"; -export class KendraClient extends ApiClientBase { - async getKendraIndexes(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl("/rag/engines/kendra/indexes"), - { - headers, - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } +export class KendraClient { + async getKendraIndexes(): Promise< + GraphQLResult> + > { + const result = await API.graphql>({ + query: listKendraIndexes, + }); + return result; } - async startKendraDataSync(workspaceId: string): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl("/rag/engines/kendra/data-sync"), - { - headers, - method: "POST", - body: JSON.stringify({ workspaceId }), - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + async startKendraDataSync( + workspaceId: string + ): Promise>> { + const result = await API.graphql>( + { + query: startKendraDataSync, + variables: { + workspaceId, + }, + } + ); + return result; } - async kendraIsSyncing(workspaceId: string): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch( - this.getApiUrl(`/rag/engines/kendra/data-sync/${workspaceId}`), - { - headers, - method: "GET", - } - ); - - return result.json(); - } catch (error) { - return this.error(error); - } + async kendraIsSyncing( + workspaceId: string + ): Promise>> { + const result = await API.graphql>({ + query: isKendraDataSynching, + variables: { + workspaceId, + }, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/models-client.ts b/lib/user-interface/react-app/src/common/api-client/models-client.ts index d715d3713..b0cf488c9 100644 --- a/lib/user-interface/react-app/src/common/api-client/models-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/models-client.ts @@ -1,17 +1,14 @@ -import { ApiResult, ModelItem } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { listModels } from "../../graphql/queries"; +import { ListModelsQuery } from "../../API"; -export class ModelsClient extends ApiClientBase { - async getModels(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/models"), { - headers, - }); +export class ModelsClient { + async getModels(): Promise>> { + const result = await API.graphql>({ + query: listModels, + }); - return result.json(); - } catch (error) { - return this.error(error); - } + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/rag-engines-client.ts b/lib/user-interface/react-app/src/common/api-client/rag-engines-client.ts index 279cf55a9..360af10e7 100644 --- a/lib/user-interface/react-app/src/common/api-client/rag-engines-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/rag-engines-client.ts @@ -1,17 +1,15 @@ -import { ApiResult, EngineItem } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { listRagEngines } from "../../graphql/queries"; +import { ListRagEnginesQuery } from "../../API"; -export class RagEnginesClient extends ApiClientBase { - async getRagEngines(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/rag/engines"), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } +export class RagEnginesClient { + async getRagEngines(): Promise< + GraphQLResult> + > { + const result = await API.graphql>({ + query: listRagEngines, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/semantic-search-client.ts b/lib/user-interface/react-app/src/common/api-client/semantic-search-client.ts index 1d58ae2c2..e13904f11 100644 --- a/lib/user-interface/react-app/src/common/api-client/semantic-search-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/semantic-search-client.ts @@ -1,22 +1,16 @@ -import { ApiResult, SemanticSearchResult } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { performSemanticSearch } from "../../graphql/queries"; +import { PerformSemanticSearchQuery } from "../../API"; -export class SemanticSearchClient extends ApiClientBase { +export class SemanticSearchClient { async query( workspaceId: string, query: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/semantic-search"), { - method: "POST", - headers, - body: JSON.stringify({ workspaceId, query }), - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + return API.graphql({ + query: performSemanticSearch, + variables: { input: { workspaceId, query } }, + }); } } diff --git a/lib/user-interface/react-app/src/common/api-client/sessions-client.ts b/lib/user-interface/react-app/src/common/api-client/sessions-client.ts index ec6df474f..727c2dbee 100644 --- a/lib/user-interface/react-app/src/common/api-client/sessions-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/sessions-client.ts @@ -1,65 +1,52 @@ -import { ApiResult, SessionItem } from "../types"; -import { ApiClientBase } from "./api-client-base"; - -export class SessionsClient extends ApiClientBase { - async getSessions(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/sessions"), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { listSessions, getSession } from "../../graphql/queries"; +import { deleteSession, deleteUserSessions } from "../../graphql/mutations"; +import { + ListSessionsQuery, + GetSessionQuery, + DeleteSessionMutation, + DeleteUserSessionsMutation, +} from "../../API"; + +export class SessionsClient { + async getSessions(): Promise>> { + const result = await API.graphql>({ + query: listSessions, + }); + return result; } - async getSession(sessionId: string): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl(`/sessions/${sessionId}`), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + async getSession( + sessionId: string + ): Promise>> { + const result = await API.graphql>({ + query: getSession, + variables: { + id: sessionId, + }, + }); + return result; } async deleteSession( sessionId: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl(`/sessions/${sessionId}`), { - method: "DELETE", - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = await API.graphql>({ + query: deleteSession, + variables: { + id: sessionId, + }, + }); + return result; } async deleteSessions(): Promise< - ApiResult<{ - id: string; - deleted: boolean; - }> + GraphQLResult> > { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/sessions"), { - method: "DELETE", - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + const result = await API.graphql>({ + query: deleteUserSessions, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts b/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts index b2fd01e6a..58124abc8 100644 --- a/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts @@ -1,49 +1,53 @@ -import { ApiResult, WorkspaceItem } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { API } from "aws-amplify"; +import { GraphQLQuery, GraphQLResult } from "@aws-amplify/api"; +import { listWorkspaces, getWorkspace } from "../../graphql/queries"; +import { + createAuroraWorkspace, + createKendraWorkspace, + createOpenSearchWorkspace, + deleteWorkspace, +} from "../../graphql/mutations"; +import { + ListWorkspacesQuery, + GetWorkspaceQuery, + CreateAuroraWorkspaceMutation, + CreateKendraWorkspaceMutation, + CreateOpenSearchWorkspaceMutation, + DeleteWorkspaceMutation, +} from "../../API"; -export class WorkspacesClient extends ApiClientBase { - async getWorkspaces(): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/workspaces"), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } +export class WorkspacesClient { + async getWorkspaces(): Promise< + GraphQLResult> + > { + const result = await API.graphql>({ + query: listWorkspaces, + }); + return result; } async getWorkspace( workspaceId: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl(`/workspaces/${workspaceId}`), { - headers, - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = await API.graphql>({ + query: getWorkspace, + variables: { + workspaceId: workspaceId, + }, + }); + return result; } async deleteWorkspace( workspaceId: string - ): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl(`/workspaces/${workspaceId}`), { - headers, - method: "DELETE", - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + ): Promise>> { + const result = await API.graphql>({ + query: deleteWorkspace, + variables: { + workspaceId: workspaceId, + }, + }); + return result; } async createAuroraWorkspace(params: { @@ -56,25 +60,17 @@ export class WorkspacesClient extends ApiClientBase { metric: string; index: boolean; hybridSearch: boolean; - chunking_strategy: string; + chunkingStrategy: string; chunkSize: number; chunkOverlap: number; - }): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/workspaces"), { - method: "PUT", - headers, - body: JSON.stringify({ - ...params, - kind: "aurora", - }), - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + }): Promise>> { + const result = API.graphql>({ + query: createAuroraWorkspace, + variables: { + input: { ...params, kind: "aurora" }, + }, + }); + return result; } async createOpenSearchWorkspace(params: { @@ -85,46 +81,32 @@ export class WorkspacesClient extends ApiClientBase { crossEncoderModelName: string; languages: string[]; hybridSearch: boolean; - chunking_strategy: string; + chunkingStrategy: string; chunkSize: number; chunkOverlap: number; - }): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/workspaces"), { - method: "PUT", - headers, - body: JSON.stringify({ - ...params, - kind: "opensearch", - }), - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + }): Promise>> { + const result = API.graphql>( + { + query: createOpenSearchWorkspace, + variables: { + input: { ...params, kind: "aoss" }, + }, + } + ); + return result; } async createKendraWorkspace(params: { name: string; kendraIndexId: string; useAllData: boolean; - }): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/workspaces"), { - method: "PUT", - headers, - body: JSON.stringify({ - ...params, - kind: "kendra", - }), - }); - - return result.json(); - } catch (error) { - return this.error(error); - } + }): Promise>> { + const result = API.graphql>({ + query: createKendraWorkspace, + variables: { + input: { ...params, kind: "kendra" }, + }, + }); + return result; } } diff --git a/lib/user-interface/react-app/src/common/constants.ts b/lib/user-interface/react-app/src/common/constants.ts index a17ee8d33..be302ac8f 100644 --- a/lib/user-interface/react-app/src/common/constants.ts +++ b/lib/user-interface/react-app/src/common/constants.ts @@ -1,5 +1,5 @@ import { StatusIndicatorProps } from "@cloudscape-design/components"; -import { SemanticSearchResult } from "./types"; +import { SemanticSearchResult } from "../API"; export const languageList = [ { value: "simple", label: "Simple" }, @@ -69,13 +69,13 @@ export abstract class Labels { enabled: "Enabled", }; - static distainceFunctionScoreMapAurora: Record = { + static distanceFunctionScoreMapAurora: Record = { inner: "Negative inner product", cosine: "Cosine distance", l2: "Euclidean distance / L2 norm", }; - static distainceFunctionScoreMapOpenSearch: Record = { + static distanceFunctionScoreMapOpenSearch: Record = { l2: "1 divided by 1 + L2 norm", }; @@ -95,10 +95,10 @@ export abstract class Labels { static getDistanceFunctionScoreName(result: SemanticSearchResult) { if (result.engine === "aurora") { - return Labels.distainceFunctionScoreMapAurora[result.vectorSearchMetric]; + return Labels.distanceFunctionScoreMapAurora[result.vectorSearchMetric!]; } else if (result.engine === "opensearch") { - return Labels.distainceFunctionScoreMapOpenSearch[ - result.vectorSearchMetric + return Labels.distanceFunctionScoreMapOpenSearch[ + result.vectorSearchMetric! ]; } diff --git a/lib/user-interface/react-app/src/common/file-uploader.ts b/lib/user-interface/react-app/src/common/file-uploader.ts index 986c4ef59..7fff2a729 100644 --- a/lib/user-interface/react-app/src/common/file-uploader.ts +++ b/lib/user-interface/react-app/src/common/file-uploader.ts @@ -1,19 +1,22 @@ -import { FileUploadItem } from "./types"; +import { FileUploadResult } from "../API"; export class FileUploader { upload( file: File, - signature: FileUploadItem, + signature: FileUploadResult, onProgress: (uploaded: number) => void ): Promise { return new Promise((resolve, reject) => { const formData = new FormData(); - Object.keys(signature.fields).forEach((key) => { - formData.append(key, signature.fields[key]); + const fields = signature.fields!.replace("{", "").replace("}", ""); + fields.split(",").forEach((f) => { + const sepIdx = f.indexOf("="); + const k = f.slice(0, sepIdx); + const v = f.slice(sepIdx + 1); + formData.append(k, v); }); formData.append("file", file); - const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { diff --git a/lib/user-interface/react-app/src/common/helpers/embeddings-model-helper.ts b/lib/user-interface/react-app/src/common/helpers/embeddings-model-helper.ts index 2ffc24220..93902dccd 100644 --- a/lib/user-interface/react-app/src/common/helpers/embeddings-model-helper.ts +++ b/lib/user-interface/react-app/src/common/helpers/embeddings-model-helper.ts @@ -1,5 +1,5 @@ import { SelectProps } from "@cloudscape-design/components"; -import { EmbeddingsModelItem } from "../types"; +import { EmbeddingModel } from "../../API"; export abstract class EmbeddingsModelHelper { static getSelectOption(model?: string): SelectProps.Option | null { @@ -32,8 +32,8 @@ export abstract class EmbeddingsModelHelper { }; } - static getSelectOptions(embeddingsModels: EmbeddingsModelItem[]) { - const modelsMap = new Map(); + static getSelectOptions(embeddingsModels: EmbeddingModel[]) { + const modelsMap = new Map(); embeddingsModels.forEach((model) => { let items = modelsMap.get(model.provider); if (!items) { diff --git a/lib/user-interface/react-app/src/common/types.ts b/lib/user-interface/react-app/src/common/types.ts index 882c68f63..c13601aa6 100644 --- a/lib/user-interface/react-app/src/common/types.ts +++ b/lib/user-interface/react-app/src/common/types.ts @@ -1,6 +1,5 @@ import { SelectProps } from "@cloudscape-design/components"; import { CognitoHostedUIIdentityProvider } from "@aws-amplify/auth"; -import { ChatBotHistoryItem } from "../components/chatbot/types"; export interface AppConfig { aws_project_region: string; @@ -36,27 +35,6 @@ export interface NavigationPanelState { collapsedSections?: Record; } -export type ApiResult = ApiErrorResult | ApiOKResult; -export abstract class ResultValue { - static ok(apiResult: ApiResult): apiResult is ApiOKResult { - return (apiResult as ApiOKResult).ok === true; - } - - static error(apiResult: ApiResult): apiResult is ApiErrorResult { - return (apiResult as ApiErrorResult).error === true; - } -} - -export interface ApiOKResult { - ok: true; - data: T; -} - -export interface ApiErrorResult { - error: true; - message?: string | string[]; -} - export type LoadingStatus = "pending" | "loading" | "finished" | "error"; export type ModelProvider = "sagemaker" | "bedrock" | "openai"; export type RagDocumentType = @@ -69,89 +47,6 @@ export type RagDocumentType = export type Modality = "TEXT" | "IMAGE"; export type ModelInterface = "langchain" | "idefics"; -export interface WorkspaceItem { - id: string; - name: string; - engine: string; - status: string; - languages: string[]; - embeddingsModelProvider: string; - embeddingsModelName: string; - embeddingsModelDimensions: number; - crossEncoderModelProvider: string; - crossEncoderModelName: string; - metric: string; - index: boolean; - hybridSearch: boolean; - chunkingStrategy: string; - chunkSize: number; - chunkOverlap: number; - vectors: number; - documents: number; - kendraIndexId?: string; - kendraIndexExternal?: boolean; - kendraUseAllData?: boolean; - sizeInBytes: number; - createdAt: string; -} - -export interface EngineItem { - id: string; - name: string; - enabled: boolean; -} - -export interface EmbeddingsModelItem { - provider: ModelProvider; - name: string; - dimensions: number; - default?: boolean; -} - -export interface CrossEncoderModelItem { - provider: ModelProvider; - name: string; - default?: boolean; -} - -export interface ModelItem { - provider: ModelProvider; - name: string; - streaming: boolean; - inputModalities: Modality[]; - outputModalities: Modality[]; - interface: ModelInterface; - ragSupported: boolean; -} - -export interface SessionItem { - id: string; - title: string; - startTime: string; - history?: ChatBotHistoryItem[]; -} - -export interface DocumentItem { - id: string; - workspaceId?: string; - type: RagDocumentType; - subType?: string; - status: string; - title?: string; - path: string; - sizeInBytes: number; - vectors: number; - subDocuments: number; - createdAt: string; - updatedAt: string; - rssFeedId?: string; - rssLastCheckedAt?: string; - crawlerProperties?: { - followLinks: boolean; - limit: number; - }; -} - export interface DocumentSubscriptionToggleResult { id: string; workspaceId: string; @@ -164,53 +59,6 @@ export enum DocumentSubscriptionStatus { UNKNOWN = "unknown", DEFAULT = UNKNOWN, } -export interface DocumentResult { - items: DocumentItem[]; - lastDocumentId?: string; -} - -export interface AddDocumentResult { - workspaceId: string; - documentId: string; -} - -export interface FileUploadItem { - url: string; - fields: Record; -} - -export interface SemanticSearchResultItem { - sources: string[]; - chunkId: string; - workspaceId: string; - documentId: string; - documentSubId: string; - documentType: string; - documentSubType: string; - path: string; - language: string; - title: string; - content: string; - contentComplement: string; - vectorSearchScore?: number; - keywordSearchScore?: number; - score?: number; -} - -export interface SemanticSearchResult { - engine: string; - workspaceId: string; - queryLanguage?: string; - supportedLanguages?: string[]; - detectedLanguages?: { - code: string; - score: number; - }[]; - vectorSearchMetric: string; - items: SemanticSearchResultItem[]; - vectorSearchItems: SemanticSearchResultItem[]; - keywordSearchItems: SemanticSearchResultItem[]; -} export interface AuroraWorkspaceCreateInput { name: string; @@ -239,9 +87,3 @@ export interface KendraWorkspaceCreateInput { kendraIndex: SelectProps.Option | null; useAllData: boolean; } - -export interface KendraIndexItem { - id: string; - name: string; - external: boolean; -} diff --git a/lib/user-interface/react-app/src/common/utils.ts b/lib/user-interface/react-app/src/common/utils.ts index 729291372..148050410 100644 --- a/lib/user-interface/react-app/src/common/utils.ts +++ b/lib/user-interface/react-app/src/common/utils.ts @@ -1,5 +1,3 @@ -import { ApiErrorResult } from "./types"; - export class Utils { static isDevelopment() { return import.meta.env.MODE === "development"; @@ -80,11 +78,9 @@ export class Utils { return null; } - static getErrorMessage(error: ApiErrorResult) { - if (Array.isArray(error.message)) { - return error.message.join(", "); - } else if (typeof error.message === "string") { - return error.message; + static getErrorMessage(error: any) { + if (error.errors) { + return error.errors.map((e: any) => e.message).join(", "); } return "Unknown error"; diff --git a/lib/user-interface/react-app/src/components/chatbot/chat-input-panel.tsx b/lib/user-interface/react-app/src/components/chatbot/chat-input-panel.tsx index fda0584ed..52797c35b 100644 --- a/lib/user-interface/react-app/src/components/chatbot/chat-input-panel.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/chat-input-panel.tsx @@ -8,13 +8,13 @@ import { Spinner, StatusIndicator, } from "@cloudscape-design/components"; -import { Auth } from "aws-amplify"; import { Dispatch, SetStateAction, useContext, useEffect, useLayoutEffect, + useRef, useState, } from "react"; import { useNavigate } from "react-router-dom"; @@ -22,46 +22,47 @@ import SpeechRecognition, { useSpeechRecognition, } from "react-speech-recognition"; import TextareaAutosize from "react-textarea-autosize"; -import useWebSocket, { ReadyState } from "react-use-websocket"; +import { ReadyState } from "react-use-websocket"; import { ApiClient } from "../../common/api-client/api-client"; import { AppContext } from "../../common/app-context"; import { OptionsHelper } from "../../common/helpers/options-helper"; import { StorageHelper } from "../../common/helpers/storage-helper"; -import { - ApiResult, - ModelItem, - ResultValue, - WorkspaceItem, -} from "../../common/types"; +import { API } from "aws-amplify"; +import { GraphQLSubscription, GraphQLResult } from "@aws-amplify/api"; +import { Model, ReceiveMessagesSubscription, Workspace } from "../../API"; +import { LoadingStatus, ModelInterface } from "../../common/types"; import styles from "../../styles/chat.module.scss"; import ConfigDialog from "./config-dialog"; import ImageDialog from "./image-dialog"; import { ChabotInputModality, + ChatBotHeartbeatRequest, ChatBotAction, ChatBotConfiguration, - ChatBotHeartbeatRequest, ChatBotHistoryItem, ChatBotMessageResponse, ChatBotMessageType, ChatBotMode, - ChatBotModelInterface, ChatBotRunRequest, ChatInputState, ImageFile, + ChatBotModelInterface, } from "./types"; +import { sendQuery } from "../../graphql/mutations"; import { getSelectedModelMetadata, getSignedUrl, - updateMessageHistory, + updateMessageHistoryRef, } from "./utils"; +import { receiveMessages } from "../../graphql/subscriptions"; +import { Utils } from "../../common/utils"; export interface ChatInputPanelProps { running: boolean; setRunning: Dispatch>; session: { id: string; loading: boolean }; messageHistory: ChatBotHistoryItem[]; - setMessageHistory: Dispatch>; + setMessageHistory: (history: ChatBotHistoryItem[]) => void; configuration: ChatBotConfiguration; setConfiguration: Dispatch>; } @@ -101,40 +102,97 @@ export default function ChatInputPanel(props: ChatInputPanelProps) { const [configDialogVisible, setConfigDialogVisible] = useState(false); const [imageDialogVisible, setImageDialogVisible] = useState(false); const [files, setFiles] = useState([]); - const [socketUrl, setSocketUrl] = useState(null); - const { sendJsonMessage, readyState } = useWebSocket(socketUrl, { - share: true, - shouldReconnect: () => true, - onOpen: () => { - const request: ChatBotHeartbeatRequest = { - action: ChatBotAction.Heartbeat, - modelInterface: ChatBotModelInterface.Langchain, - }; - - sendJsonMessage(request); - }, - onMessage: (payload: { data: string }) => { - const response: ChatBotMessageResponse = JSON.parse(payload.data); - if (response.action === ChatBotAction.Heartbeat) { - return; - } + const [readyState, setReadyState] = useState( + ReadyState.UNINSTANTIATED + ); - updateMessageHistory( - props.session.id, - props.messageHistory, - props.setMessageHistory, - response, - setState - ); + const messageHistoryRef = useRef([]); - if ( - response.action === ChatBotAction.FinalResponse || - response.action === ChatBotAction.Error - ) { - props.setRunning(false); - } - }, - }); + useEffect(() => { + messageHistoryRef.current = props.messageHistory; + }, [props.messageHistory]); + + useEffect(() => { + async function subscribe() { + console.log("Subscribing to AppSync"); + setReadyState(ReadyState.CONNECTING); + const sub = await API.graphql< + GraphQLSubscription + >({ + query: receiveMessages, + variables: { + sessionId: props.session.id, + }, + authMode: "AMAZON_COGNITO_USER_POOLS", + }).subscribe({ + next: ({ value }) => { + console.log(`Graphql message:`); + console.log(value); + const data = value.data!.receiveMessages?.data; + if (data !== undefined && data !== null) { + const response: ChatBotMessageResponse = JSON.parse(data); + console.log(response); + if (response.action === ChatBotAction.Heartbeat) { + console.log("Heartbeat pong!"); + return; + } + updateMessageHistoryRef( + props.session.id, + messageHistoryRef.current, + response + ); + + if ( + response.action === ChatBotAction.FinalResponse || + response.action === ChatBotAction.Error + ) { + console.log("Final message received"); + props.setRunning(false); + } + props.setMessageHistory([...messageHistoryRef.current]); + } + }, + error: (error) => console.warn(error), + }); + return sub; + } + + const sub = subscribe(); + sub + .then(() => { + setReadyState(ReadyState.OPEN); + console.log(`Subscribed to session ${props.session.id}`); + const request: ChatBotHeartbeatRequest = { + action: ChatBotAction.Heartbeat, + modelInterface: ChatBotModelInterface.Langchain, + data: { + sessionId: props.session.id, + }, + }; + const result = API.graphql({ + query: sendQuery, + variables: { + data: JSON.stringify(request), + }, + }); + Promise.all([result]) + .then((x) => console.log(`Query successful`, x)) + .catch((err) => console.log(err)); + }) + .catch((err) => { + console.log(err); + setReadyState(ReadyState.CLOSED); + }); + + return () => { + sub + .then((s) => { + console.log(`Unsubscribing from ${props.session.id}`); + s.unsubscribe(); + }) + .catch((err) => console.log(err)); + }; + }, [props.session.id]); useEffect(() => { if (transcript) { @@ -147,51 +205,51 @@ export default function ChatInputPanel(props: ChatInputPanelProps) { (async () => { const apiClient = new ApiClient(appContext); - const [session, modelsResult, workspacesResult] = await Promise.all([ - Auth.currentSession(), - apiClient.models.getModels(), - appContext?.config.rag_enabled - ? apiClient.workspaces.getWorkspaces() - : Promise.resolve>({ - ok: true, - data: [], - }), - ]); - - const jwtToken = session.getAccessToken().getJwtToken(); - - if (jwtToken) { - setSocketUrl( - `${appContext.config.websocket_endpoint}?token=${jwtToken}` - ); - } + let workspaces: Workspace[] = []; + let workspacesStatus: LoadingStatus = "finished"; + let modelsResult: GraphQLResult; + let workspacesResult: GraphQLResult; + try { + if (appContext?.config.rag_enabled) { + [modelsResult, workspacesResult] = await Promise.all([ + apiClient.models.getModels(), + apiClient.workspaces.getWorkspaces(), + ]); + workspaces = workspacesResult.data?.listWorkspaces; + workspacesStatus = + workspacesResult.errors === undefined ? "finished" : "error"; + } else { + modelsResult = await apiClient.models.getModels(); + } - const models = ResultValue.ok(modelsResult) ? modelsResult.data : []; - const workspaces = ResultValue.ok(workspacesResult) - ? workspacesResult.data - : []; + const models = modelsResult.data ? modelsResult.data.listModels : []; - const selectedModelOption = getSelectedModelOption(models); - const selectedModelMetadata = getSelectedModelMetadata( - models, - selectedModelOption - ); - const selectedWorkspaceOption = appContext?.config.rag_enabled - ? getSelectedWorkspaceOption(workspaces) - : workspaceDefaultOptions[0]; - - setState((state) => ({ - ...state, - models, - workspaces, - selectedModel: selectedModelOption, - selectedModelMetadata, - selectedWorkspace: selectedWorkspaceOption, - modelsStatus: ResultValue.ok(modelsResult) ? "finished" : "error", - workspacesStatus: ResultValue.ok(workspacesResult) - ? "finished" - : "error", - })); + const selectedModelOption = getSelectedModelOption(models); + const selectedModelMetadata = getSelectedModelMetadata( + models, + selectedModelOption + ); + const selectedWorkspaceOption = appContext?.config.rag_enabled + ? getSelectedWorkspaceOption(workspaces) + : workspaceDefaultOptions[0]; + + setState((state) => ({ + ...state, + models, + workspaces, + selectedModel: selectedModelOption, + selectedModelMetadata, + selectedWorkspace: selectedWorkspaceOption, + modelsStatus: "finished", + workspacesStatus: workspacesStatus, + })); + } catch (error) { + console.log(Utils.getErrorMessage(error)); + setState((state) => ({ + ...state, + modelsStatus: "error", + })); + } })(); }, [appContext, state.modelsStatus]); @@ -272,7 +330,7 @@ export default function ChatInputPanel(props: ChatInputPanelProps) { const value = state.value.trim(); const request: ChatBotRunRequest = { action: ChatBotAction.Run, - modelInterface: state.selectedModelMetadata!.interface, + modelInterface: state.selectedModelMetadata!.interface as ModelInterface, data: { mode: ChatBotMode.Chain, text: value, @@ -302,25 +360,33 @@ export default function ChatInputPanel(props: ChatInputPanelProps) { }); props.setRunning(true); - - props.setMessageHistory((prev) => - prev.concat( - { - type: ChatBotMessageType.Human, - content: value, - metadata: { - ...props.configuration, - }, + messageHistoryRef.current = [ + ...messageHistoryRef.current, + + { + type: ChatBotMessageType.Human, + content: value, + metadata: { + ...props.configuration, }, - { - type: ChatBotMessageType.AI, - content: "", - metadata: {}, - } - ) - ); + tokens: [], + }, + { + type: ChatBotMessageType.AI, + tokens: [], + content: "", + metadata: {}, + }, + ]; + + props.setMessageHistory(messageHistoryRef.current); - return sendJsonMessage(request); + API.graphql({ + query: sendQuery, + variables: { + data: JSON.stringify(request), + }, + }); }; const connectionStatus = { @@ -551,7 +617,7 @@ export default function ChatInputPanel(props: ChatInputPanelProps) { } function getSelectedWorkspaceOption( - workspaces: WorkspaceItem[] + workspaces: Workspace[] ): SelectProps.Option | null { let selectedWorkspaceOption: SelectProps.Option | null = null; @@ -573,9 +639,7 @@ function getSelectedWorkspaceOption( return selectedWorkspaceOption; } -function getSelectedModelOption( - models: ModelItem[] -): SelectProps.Option | null { +function getSelectedModelOption(models: Model[]): SelectProps.Option | null { let selectedModelOption: SelectProps.Option | null = null; const savedModel = StorageHelper.getSelectedLLM(); @@ -594,7 +658,7 @@ function getSelectedModelOption( } } - let candidate: ModelItem | undefined = undefined; + let candidate: Model | undefined = undefined; if (!selectedModelOption) { const bedrockModels = models.filter((m) => m.provider === "bedrock"); const sageMakerModels = models.filter((m) => m.provider === "sagemaker"); diff --git a/lib/user-interface/react-app/src/components/chatbot/chat.tsx b/lib/user-interface/react-app/src/components/chatbot/chat.tsx index 4638ca002..1dd1701e6 100644 --- a/lib/user-interface/react-app/src/components/chatbot/chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/chat.tsx @@ -1,10 +1,13 @@ import { useContext, useEffect, useState } from "react"; -import { ChatBotConfiguration, ChatBotHistoryItem } from "./types"; +import { + ChatBotConfiguration, + ChatBotHistoryItem, + ChatBotMessageType, +} from "./types"; import { SpaceBetween, StatusIndicator } from "@cloudscape-design/components"; import { v4 as uuidv4 } from "uuid"; import { AppContext } from "../../common/app-context"; import { ApiClient } from "../../common/api-client/api-client"; -import { ResultValue } from "../../common/types"; import ChatMessage from "./chat-message"; import ChatInputPanel, { ChatScrollState } from "./chat-input-panel"; import styles from "../../styles/chat.module.scss"; @@ -44,19 +47,31 @@ export default function Chat(props: { sessionId?: string }) { setSession({ id: props.sessionId, loading: true }); const apiClient = new ApiClient(appContext); - const result = await apiClient.sessions.getSession(props.sessionId); + try { + const result = await apiClient.sessions.getSession(props.sessionId); - if (ResultValue.ok(result)) { - if (result.data?.history) { + if (result.data?.getSession?.history) { + console.log(result.data.getSession); ChatScrollState.skipNextHistoryUpdate = true; ChatScrollState.skipNextScrollEvent = true; - setMessageHistory(result.data.history); + console.log("History", result.data.getSession.history); + setMessageHistory( + result + .data!.getSession!.history.filter((x) => x !== null) + .map((x) => ({ + type: x!.type as ChatBotMessageType, + metadata: JSON.parse(x!.metadata!), + content: x!.content, + })) + ); window.scrollTo({ top: 0, behavior: "instant", }); } + } catch (error) { + console.log(error); } setSession({ id: props.sessionId, loading: false }); @@ -71,7 +86,7 @@ export default function Chat(props: { sessionId?: string }) { ))} @@ -91,7 +106,7 @@ export default function Chat(props: { sessionId?: string }) { running={running} setRunning={setRunning} messageHistory={messageHistory} - setMessageHistory={setMessageHistory} + setMessageHistory={(history) => setMessageHistory(history)} configuration={configuration} setConfiguration={setConfiguration} /> diff --git a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx index 4a2cb6e75..aa7854cee 100644 --- a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx @@ -1,4 +1,10 @@ -import { useContext, useEffect, useLayoutEffect, useState } from "react"; +import { + useContext, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { SelectProps, SpaceBetween, @@ -14,9 +20,11 @@ import { AppContext } from "../../common/app-context"; import { ApiClient } from "../../common/api-client/api-client"; import ChatMessage from "./chat-message"; import MultiChatInputPanel, { ChatScrollState } from "./multi-chat-input-panel"; -import useWebSocket, { ReadyState } from "react-use-websocket"; +import { ReadyState } from "react-use-websocket"; import { OptionsHelper } from "../../common/helpers/options-helper"; -import { Auth } from "aws-amplify"; +import { API } from "aws-amplify"; +import { GraphQLSubscription, GraphQLResult } from "@aws-amplify/api"; +import { Model, ReceiveMessagesSubscription, Workspace } from "../../API"; import { ChatBotConfiguration, ChatBotAction, @@ -30,27 +38,25 @@ import { ChatBotHeartbeatRequest, ChatBotModelInterface, } from "./types"; -import { - ApiResult, - ModelItem, - WorkspaceItem, - ResultValue, - LoadingStatus, -} from "../../common/types"; -import { getSelectedModelMetadata, updateChatSessions } from "./utils"; +import { LoadingStatus, ModelInterface } from "../../common/types"; +import { getSelectedModelMetadata, updateMessageHistoryRef } from "./utils"; import LLMConfigDialog from "./llm-config-dialog"; import styles from "../../styles/chat.module.scss"; import { useNavigate } from "react-router-dom"; +import { receiveMessages } from "../../graphql/subscriptions"; +import { sendQuery } from "../../graphql/mutations"; +import { Utils } from "../../common/utils"; export interface ChatSession { configuration: ChatBotConfiguration; model?: SelectProps.Option; - modelMetadata?: ModelItem; + modelMetadata?: Model; workspace?: SelectProps.Option; id: string; loading: boolean; running: boolean; messageHistory: ChatBotHistoryItem[]; + subscription?: Promise; } function createNewSession(): ChatSession { @@ -86,10 +92,10 @@ const workspaceDefaultOptions: SelectProps.Option[] = [ export default function MultiChat() { const navigate = useNavigate(); const appContext = useContext(AppContext); - const [socketUrl, setSocketUrl] = useState(null); + const refChatSessions = useRef([]); const [chatSessions, setChatSessions] = useState([]); - const [models, setModels] = useState([]); - const [workspaces, setWorkspaces] = useState([]); + const [models, setModels] = useState([]); + const [workspaces, setWorkspaces] = useState([]); const [modelsStatus, setModelsStatus] = useState("loading"); const [workspacesStatus, setWorkspacesStatus] = useState("loading"); @@ -98,73 +104,59 @@ export default function MultiChat() { undefined ); const [showMetadata, setShowMetadata] = useState(false); - const { sendJsonMessage, readyState } = useWebSocket(socketUrl, { - share: true, - shouldReconnect: () => true, - onOpen: () => { - const request: ChatBotHeartbeatRequest = { - action: ChatBotAction.Heartbeat, - modelInterface: ChatBotModelInterface.Langchain, - }; - - sendJsonMessage(request); - }, - onMessage: (payload) => { - const response: ChatBotMessageResponse = JSON.parse(payload.data); - if (response.action === ChatBotAction.Heartbeat) { - return; - } - const sessionId = response.data.sessionId; - const session = chatSessions.filter((c) => c.id === sessionId)[0]; - if (session !== undefined) { - updateChatSessions(session, response); - setChatSessions([...chatSessions]); - } - }, - }); + const [readyState, setReadyState] = useState( + ReadyState.UNINSTANTIATED + ); useEffect(() => { if (!appContext) return; - - setChatSessions([createNewSession(), createNewSession()]); // reset all chats + setReadyState(ReadyState.OPEN); + addSession(); + addSession(); setEnableAddModels(true); + (async () => { const apiClient = new ApiClient(appContext); - const [session, modelsResult, workspacesResult] = await Promise.all([ - Auth.currentSession(), - apiClient.models.getModels(), - appContext?.config.rag_enabled - ? apiClient.workspaces.getWorkspaces() - : Promise.resolve>({ ok: true, data: [] }), - ]); - - const jwtToken = session.getAccessToken().getJwtToken(); - - if (jwtToken) { - setSocketUrl( - `${appContext.config.websocket_endpoint}?token=${jwtToken}` - ); + let workspaces: Workspace[] = []; + let modelsResult: GraphQLResult; + let workspacesResult: GraphQLResult; + try { + if (appContext?.config.rag_enabled) { + [modelsResult, workspacesResult] = await Promise.all([ + apiClient.models.getModels(), + apiClient.workspaces.getWorkspaces(), + ]); + workspaces = workspacesResult.data?.listWorkspaces; + setWorkspacesStatus( + workspacesResult.errors === undefined ? "finished" : "error" + ); + } else { + modelsResult = await apiClient.models.getModels(); + } + + const models = modelsResult.data + ? modelsResult.data.listModels.filter( + (m: any) => + m.inputModalities.includes(ChabotInputModality.Text) && + m.outputModalities.includes(ChabotOutputModality.Text) + ) + : []; + setModels(models); + setWorkspaces(workspaces); + setModelsStatus("finished"); + } catch (error) { + console.error(Utils.getErrorMessage(error)); + setModelsStatus("error"); } - - const models = ResultValue.ok(modelsResult) - ? modelsResult.data.filter( - (m) => - m.inputModalities.includes(ChabotInputModality.Text) && - m.outputModalities.includes(ChabotOutputModality.Text) - ) - : []; - - const workspaces = ResultValue.ok(workspacesResult) - ? workspacesResult.data - : []; - - setModels(models); - setWorkspaces(workspaces); - setModelsStatus(ResultValue.ok(modelsResult) ? "finished" : "error"); - setWorkspacesStatus( - ResultValue.ok(workspacesResult) ? "finished" : "error" - ); })(); + + return () => { + refChatSessions.current.forEach((session) => { + console.log(`Unsubscribing from ${session.id}`); + session.subscription?.then((s) => s.unsubscribe()); + }); + refChatSessions.current = []; + }; }, [appContext]); const enabled = @@ -181,7 +173,7 @@ export default function MultiChat() { chatSessions.forEach((chatSession) => { if (chatSession.running) return; if (readyState !== ReadyState.OPEN) return; - ChatScrollState.userHasScrolled = false; // check this + ChatScrollState.userHasScrolled = false; const { name, provider } = OptionsHelper.parseValue( chatSession.model?.value @@ -190,7 +182,7 @@ export default function MultiChat() { const value = message.trim(); const request: ChatBotRunRequest = { action: ChatBotAction.Run, - modelInterface: chatSession.modelMetadata!.interface, + modelInterface: chatSession.modelMetadata!.interface as ModelInterface, data: { modelName: name, provider: provider, @@ -224,18 +216,94 @@ export default function MultiChat() { ]; setChatSessions([...chatSessions]); - sendJsonMessage(request); + const result = API.graphql({ + query: sendQuery, + variables: { + data: JSON.stringify(request), + }, + }); + console.log(result); }); }; - const addSession = () => { - if (chatSessions.length >= 4) { + async function subscribe(sessionId: string) { + console.log("Subscribing to AppSync"); + const sub = await API.graphql< + GraphQLSubscription + >({ + query: receiveMessages, + variables: { + sessionId: sessionId, + }, + authMode: "AMAZON_COGNITO_USER_POOLS", + }).subscribe({ + next: ({ value }) => { + const data = value.data!.receiveMessages?.data; + if (data !== undefined && data !== null) { + const response: ChatBotMessageResponse = JSON.parse(data); + console.log(response); + if (response.action === ChatBotAction.Heartbeat) { + console.log("Heartbeat pong!"); + return; + } + + const sessionId = response.data.sessionId; + const session = refChatSessions.current.filter( + (c) => c.id === sessionId + )[0]; + if (session !== undefined) { + updateMessageHistoryRef( + session.id, + session.messageHistory, + response + ); + if ((response.action = ChatBotAction.FinalResponse)) { + session.running = false; + } + setChatSessions([...refChatSessions.current]); + } + } + }, + error: (error) => console.warn(error), + }); + return sub; + } + + function addSession() { + if (refChatSessions.current.length >= 4) { return; } + const session = createNewSession(); - setChatSessions([...chatSessions, session]); - }; + const sub = subscribe(session.id); + sub + .then(() => { + console.log(`Subscribed to session ${session.id}}`); + const request: ChatBotHeartbeatRequest = { + action: ChatBotAction.Heartbeat, + modelInterface: ChatBotModelInterface.Langchain, + data: { + sessionId: session.id, + }, + }; + const result = API.graphql({ + query: sendQuery, + variables: { + data: JSON.stringify(request), + }, + }); + console.log(result); + }) + .catch((err) => { + console.log(err); + }); + + session.subscription = sub; + refChatSessions.current.push(session); + console.log(refChatSessions); + setChatSessions([...refChatSessions.current]); + } useLayoutEffect(() => { if (ChatScrollState.skipNextHistoryUpdate) { @@ -300,12 +368,14 @@ export default function MultiChat() { ), }, - ] as TableProps.ColumnDefinition[] + ] as TableProps.ColumnDefinition[] } /> diff --git a/lib/user-interface/react-app/src/components/chatbot/types.ts b/lib/user-interface/react-app/src/components/chatbot/types.ts index 740d733cd..747144eab 100644 --- a/lib/user-interface/react-app/src/components/chatbot/types.ts +++ b/lib/user-interface/react-app/src/components/chatbot/types.ts @@ -1,9 +1,5 @@ -import { - ModelItem, - LoadingStatus, - WorkspaceItem, - ModelInterface, -} from "../../common/types"; +import { Model, Workspace } from "../../API"; +import { LoadingStatus, ModelInterface } from "../../common/types"; import { SelectProps } from "@cloudscape-design/components"; export interface ChatBotConfiguration { @@ -17,10 +13,10 @@ export interface ChatBotConfiguration { export interface ChatInputState { value: string; - workspaces?: WorkspaceItem[]; - models?: ModelItem[]; + workspaces?: Workspace[]; + models?: Model[]; selectedModel: SelectProps.Option | null; - selectedModelMetadata: ModelItem | null; + selectedModelMetadata: Model | null; selectedWorkspace: SelectProps.Option | null; modelsStatus: LoadingStatus; workspacesStatus: LoadingStatus; @@ -61,6 +57,9 @@ export interface ImageFile { export interface ChatBotHeartbeatRequest { action: ChatBotAction.Heartbeat; modelInterface: ModelInterface; + data: { + sessionId: string; + }; } export interface ChatBotRunRequest { diff --git a/lib/user-interface/react-app/src/components/chatbot/utils.ts b/lib/user-interface/react-app/src/components/chatbot/utils.ts index 0c6af2e4e..1b4b3826c 100644 --- a/lib/user-interface/react-app/src/components/chatbot/utils.ts +++ b/lib/user-interface/react-app/src/components/chatbot/utils.ts @@ -5,28 +5,20 @@ import { ChatBotHistoryItem, ChatBotMessageResponse, ChatBotMessageType, - ChatInputState, } from "./types"; import { ChatSession } from "./multi-chat"; -import { ModelItem } from "../../common/types"; import { SelectProps } from "@cloudscape-design/components"; import { OptionsHelper } from "../../common/helpers/options-helper"; +import { Model } from "../../API"; export function updateMessageHistory( sessionId: string, messageHistory: ChatBotHistoryItem[], setMessageHistory: Dispatch>, - response: ChatBotMessageResponse, - setState: Dispatch> + response: ChatBotMessageResponse ) { if (response.data?.sessionId !== sessionId) return; - if ( - response.action === ChatBotAction.FinalResponse || - response.action === ChatBotAction.Error - ) { - setState((state) => ({ ...state, running: false })); - } if ( response.action === ChatBotAction.LLMNewToken || response.action === ChatBotAction.FinalResponse || @@ -116,6 +108,96 @@ export function updateMessageHistory( ]); } } + } else { + console.error(`Unrecognized type ${response.action}`); + } +} + +export function updateMessageHistoryRef( + sessionId: string, + messageHistory: ChatBotHistoryItem[], + response: ChatBotMessageResponse +) { + if (response.data?.sessionId !== sessionId) return; + + if ( + response.action === ChatBotAction.LLMNewToken || + response.action === ChatBotAction.FinalResponse || + response.action === ChatBotAction.Error + ) { + const content = response.data?.content; + let metadata = response.data?.metadata; + const token = response.data?.token; + const hasContent = typeof content !== "undefined"; + const hasToken = typeof token !== "undefined"; + const hasMetadata = typeof metadata !== "undefined"; + if ( + messageHistory.length > 0 && + messageHistory.at(-1)?.type !== ChatBotMessageType.Human + ) { + const lastMessage = messageHistory.at(-1)!; + lastMessage.tokens = lastMessage.tokens || []; + if (hasToken) { + lastMessage.tokens.push(token); + } + + lastMessage.tokens.sort((a, b) => a.sequenceNumber - b.sequenceNumber); + console.log(lastMessage); + if (lastMessage.tokens.length > 0) { + const lastRunId = + lastMessage.tokens[lastMessage.tokens.length - 1].runId; + if (lastRunId) { + lastMessage.tokens = lastMessage.tokens.filter( + (c) => c.runId === lastRunId + ); + } + } + + if (!hasMetadata) { + metadata = lastMessage.metadata; + } + + if (hasContent) { + messageHistory[messageHistory.length - 1] = { + ...lastMessage, + type: ChatBotMessageType.AI, + content, + metadata, + tokens: lastMessage.tokens, + }; + } else { + const contentFromTokens = lastMessage.tokens + .map((c) => c.value) + .join(""); + + messageHistory[messageHistory.length - 1] = { + ...lastMessage, + type: ChatBotMessageType.AI, + content: contentFromTokens, + metadata, + tokens: lastMessage.tokens, + }; + } + } else { + if (hasContent) { + const tokens = hasToken ? [token] : []; + messageHistory.push({ + type: ChatBotMessageType.AI, + content, + metadata, + tokens, + }); + } else if (typeof token !== "undefined") { + messageHistory.push({ + type: ChatBotMessageType.AI, + content: token.value, + metadata, + tokens: [token], + }); + } + } + } else { + console.error(`Unrecognized type ${response.action}`); } } @@ -150,6 +232,9 @@ export function updateChatSessions( ChatBotMessageType.Human ) { const lastMessage = messageHistory[messageHistory.length - 1]; + if (lastMessage.metadata !== undefined) { + return; + } lastMessage.tokens = lastMessage.tokens || []; if (hasToken) { lastMessage.tokens.push(token); @@ -229,10 +314,10 @@ export async function getSignedUrl(key: string) { } export function getSelectedModelMetadata( - models: ModelItem[] | undefined, + models: Model[] | undefined, selectedModelOption: SelectProps.Option | null -): ModelItem | null { - let selectedModelMetadata: ModelItem | null = null; +): Model | null { + let selectedModelMetadata: Model | null = null; if (selectedModelOption) { const { name, provider } = OptionsHelper.parseValue( diff --git a/lib/user-interface/react-app/src/components/rag/workspace-delete-modal.tsx b/lib/user-interface/react-app/src/components/rag/workspace-delete-modal.tsx index ed2cbc328..ed681a568 100644 --- a/lib/user-interface/react-app/src/components/rag/workspace-delete-modal.tsx +++ b/lib/user-interface/react-app/src/components/rag/workspace-delete-modal.tsx @@ -5,11 +5,11 @@ import { Button, Alert, } from "@cloudscape-design/components"; -import { WorkspaceItem } from "../../common/types"; +import { Workspace } from "../../API"; export interface WorkspaceDeleteModalProps { visible: boolean; - workspace?: WorkspaceItem; + workspace?: Workspace; onDelete: () => void; onDiscard: () => void; } diff --git a/lib/user-interface/react-app/src/graphql/mutations.ts b/lib/user-interface/react-app/src/graphql/mutations.ts new file mode 100644 index 000000000..fe5f6148d --- /dev/null +++ b/lib/user-interface/react-app/src/graphql/mutations.ts @@ -0,0 +1,247 @@ +/* tslint:disable */ +/* eslint-disable */ +// this is an auto generated file. This will be overwritten + +import * as APITypes from "../API"; +type GeneratedMutation = string & { + __generatedMutationInput: InputType; + __generatedMutationOutput: OutputType; +}; + +export const sendQuery = /* GraphQL */ `mutation SendQuery($data: String) { + sendQuery(data: $data) +} +` as GeneratedMutation< + APITypes.SendQueryMutationVariables, + APITypes.SendQueryMutation +>; +export const publishResponse = /* GraphQL */ `mutation PublishResponse($sessionId: String, $userId: String, $data: String) { + publishResponse(sessionId: $sessionId, userId: $userId, data: $data) { + data + sessionId + userId + __typename + } +} +` as GeneratedMutation< + APITypes.PublishResponseMutationVariables, + APITypes.PublishResponseMutation +>; +export const createKendraWorkspace = /* GraphQL */ `mutation CreateKendraWorkspace($input: CreateWorkspaceKendraInput!) { + createKendraWorkspace(input: $input) { + id + name + formatVersion + engine + status + aossEngine + languages + hasIndex + embeddingsModelProvider + embeddingsModelName + embeddingsModelDimensions + crossEncoderModelName + crossEncoderModelProvider + metric + index + hybridSearch + chunkingStrategy + chunkSize + chunkOverlap + vectors + documents + sizeInBytes + kendraIndexId + kendraIndexExternal + kendraUseAllData + createdAt + updatedAt + __typename + } +} +` as GeneratedMutation< + APITypes.CreateKendraWorkspaceMutationVariables, + APITypes.CreateKendraWorkspaceMutation +>; +export const createOpenSearchWorkspace = /* GraphQL */ `mutation CreateOpenSearchWorkspace($input: CreateWorkspaceOpenSearchInput!) { + createOpenSearchWorkspace(input: $input) { + id + name + formatVersion + engine + status + aossEngine + languages + hasIndex + embeddingsModelProvider + embeddingsModelName + embeddingsModelDimensions + crossEncoderModelName + crossEncoderModelProvider + metric + index + hybridSearch + chunkingStrategy + chunkSize + chunkOverlap + vectors + documents + sizeInBytes + kendraIndexId + kendraIndexExternal + kendraUseAllData + createdAt + updatedAt + __typename + } +} +` as GeneratedMutation< + APITypes.CreateOpenSearchWorkspaceMutationVariables, + APITypes.CreateOpenSearchWorkspaceMutation +>; +export const createAuroraWorkspace = /* GraphQL */ `mutation CreateAuroraWorkspace($input: CreateWorkspaceAuroraInput!) { + createAuroraWorkspace(input: $input) { + id + name + formatVersion + engine + status + aossEngine + languages + hasIndex + embeddingsModelProvider + embeddingsModelName + embeddingsModelDimensions + crossEncoderModelName + crossEncoderModelProvider + metric + index + hybridSearch + chunkingStrategy + chunkSize + chunkOverlap + vectors + documents + sizeInBytes + kendraIndexId + kendraIndexExternal + kendraUseAllData + createdAt + updatedAt + __typename + } +} +` as GeneratedMutation< + APITypes.CreateAuroraWorkspaceMutationVariables, + APITypes.CreateAuroraWorkspaceMutation +>; +export const startKendraDataSync = /* GraphQL */ `mutation StartKendraDataSync($workspaceId: String!) { + startKendraDataSync(workspaceId: $workspaceId) +} +` as GeneratedMutation< + APITypes.StartKendraDataSyncMutationVariables, + APITypes.StartKendraDataSyncMutation +>; +export const deleteWorkspace = /* GraphQL */ `mutation DeleteWorkspace($workspaceId: String!) { + deleteWorkspace(workspaceId: $workspaceId) +} +` as GeneratedMutation< + APITypes.DeleteWorkspaceMutationVariables, + APITypes.DeleteWorkspaceMutation +>; +export const addTextDocument = /* GraphQL */ `mutation AddTextDocument($input: TextDocumentInput!) { + addTextDocument(input: $input) { + workspaceId + documentId + status + __typename + } +} +` as GeneratedMutation< + APITypes.AddTextDocumentMutationVariables, + APITypes.AddTextDocumentMutation +>; +export const addQnADocument = /* GraphQL */ `mutation AddQnADocument($input: QnADocumentInput!) { + addQnADocument(input: $input) { + workspaceId + documentId + status + __typename + } +} +` as GeneratedMutation< + APITypes.AddQnADocumentMutationVariables, + APITypes.AddQnADocumentMutation +>; +export const setDocumentSubscriptionStatus = /* GraphQL */ `mutation SetDocumentSubscriptionStatus( + $input: DocumentSubscriptionStatusInput! +) { + setDocumentSubscriptionStatus(input: $input) { + workspaceId + documentId + status + __typename + } +} +` as GeneratedMutation< + APITypes.SetDocumentSubscriptionStatusMutationVariables, + APITypes.SetDocumentSubscriptionStatusMutation +>; +export const addWebsite = /* GraphQL */ `mutation AddWebsite($input: WebsiteInput!) { + addWebsite(input: $input) { + workspaceId + documentId + status + __typename + } +} +` as GeneratedMutation< + APITypes.AddWebsiteMutationVariables, + APITypes.AddWebsiteMutation +>; +export const addRssFeed = /* GraphQL */ `mutation AddRssFeed($input: RssFeedInput!) { + addRssFeed(input: $input) { + workspaceId + documentId + status + __typename + } +} +` as GeneratedMutation< + APITypes.AddRssFeedMutationVariables, + APITypes.AddRssFeedMutation +>; +export const updateRssFeed = /* GraphQL */ `mutation UpdateRssFeed($input: RssFeedInput!) { + updateRssFeed(input: $input) { + workspaceId + documentId + status + __typename + } +} +` as GeneratedMutation< + APITypes.UpdateRssFeedMutationVariables, + APITypes.UpdateRssFeedMutation +>; +export const deleteUserSessions = /* GraphQL */ `mutation DeleteUserSessions { + deleteUserSessions { + id + deleted + __typename + } +} +` as GeneratedMutation< + APITypes.DeleteUserSessionsMutationVariables, + APITypes.DeleteUserSessionsMutation +>; +export const deleteSession = /* GraphQL */ `mutation DeleteSession($id: String!) { + deleteSession(id: $id) { + id + deleted + __typename + } +} +` as GeneratedMutation< + APITypes.DeleteSessionMutationVariables, + APITypes.DeleteSessionMutation +>; diff --git a/lib/user-interface/react-app/src/graphql/queries.ts b/lib/user-interface/react-app/src/graphql/queries.ts new file mode 100644 index 000000000..fcdaf9ee0 --- /dev/null +++ b/lib/user-interface/react-app/src/graphql/queries.ts @@ -0,0 +1,402 @@ +/* tslint:disable */ +/* eslint-disable */ +// this is an auto generated file. This will be overwritten + +import * as APITypes from "../API"; +type GeneratedQuery = string & { + __generatedQueryInput: InputType; + __generatedQueryOutput: OutputType; +}; + +export const none = /* GraphQL */ `query None { + none +} +` as GeneratedQuery; +export const checkHealth = /* GraphQL */ `query CheckHealth { + checkHealth +} +` as GeneratedQuery< + APITypes.CheckHealthQueryVariables, + APITypes.CheckHealthQuery +>; +export const getUploadFileURL = /* GraphQL */ `query GetUploadFileURL($input: FileUploadInput!) { + getUploadFileURL(input: $input) { + url + fields + __typename + } +} +` as GeneratedQuery< + APITypes.GetUploadFileURLQueryVariables, + APITypes.GetUploadFileURLQuery +>; +export const listModels = /* GraphQL */ `query ListModels { + listModels { + name + provider + interface + ragSupported + inputModalities + outputModalities + streaming + __typename + } +} +` as GeneratedQuery< + APITypes.ListModelsQueryVariables, + APITypes.ListModelsQuery +>; +export const listWorkspaces = /* GraphQL */ `query ListWorkspaces { + listWorkspaces { + id + name + formatVersion + engine + status + aossEngine + languages + hasIndex + embeddingsModelProvider + embeddingsModelName + embeddingsModelDimensions + crossEncoderModelName + crossEncoderModelProvider + metric + index + hybridSearch + chunkingStrategy + chunkSize + chunkOverlap + vectors + documents + sizeInBytes + kendraIndexId + kendraIndexExternal + kendraUseAllData + createdAt + updatedAt + __typename + } +} +` as GeneratedQuery< + APITypes.ListWorkspacesQueryVariables, + APITypes.ListWorkspacesQuery +>; +export const getWorkspace = /* GraphQL */ `query GetWorkspace($workspaceId: String!) { + getWorkspace(workspaceId: $workspaceId) { + id + name + formatVersion + engine + status + aossEngine + languages + hasIndex + embeddingsModelProvider + embeddingsModelName + embeddingsModelDimensions + crossEncoderModelName + crossEncoderModelProvider + metric + index + hybridSearch + chunkingStrategy + chunkSize + chunkOverlap + vectors + documents + sizeInBytes + kendraIndexId + kendraIndexExternal + kendraUseAllData + createdAt + updatedAt + __typename + } +} +` as GeneratedQuery< + APITypes.GetWorkspaceQueryVariables, + APITypes.GetWorkspaceQuery +>; +export const listRagEngines = /* GraphQL */ `query ListRagEngines { + listRagEngines { + id + name + enabled + __typename + } +} +` as GeneratedQuery< + APITypes.ListRagEnginesQueryVariables, + APITypes.ListRagEnginesQuery +>; +export const performSemanticSearch = /* GraphQL */ `query PerformSemanticSearch($input: SemanticSearchInput!) { + performSemanticSearch(input: $input) { + engine + workspaceId + queryLanguage + supportedLanguages + detectedLanguages { + code + score + __typename + } + items { + sources + chunkId + workspaceId + documentId + documentSubId + documentSubType + documentType + path + language + title + content + contentComplement + vectorSearchScore + keywordSearchScore + score + __typename + } + vectorSearchMetric + vectorSearchItems { + sources + chunkId + workspaceId + documentId + documentSubId + documentSubType + documentType + path + language + title + content + contentComplement + vectorSearchScore + keywordSearchScore + score + __typename + } + keywordSearchItems { + sources + chunkId + workspaceId + documentId + documentSubId + documentSubType + documentType + path + language + title + content + contentComplement + vectorSearchScore + keywordSearchScore + score + __typename + } + __typename + } +} +` as GeneratedQuery< + APITypes.PerformSemanticSearchQueryVariables, + APITypes.PerformSemanticSearchQuery +>; +export const listSessions = /* GraphQL */ `query ListSessions { + listSessions { + id + title + startTime + history { + type + content + metadata + __typename + } + __typename + } +} +` as GeneratedQuery< + APITypes.ListSessionsQueryVariables, + APITypes.ListSessionsQuery +>; +export const listEmbeddingModels = /* GraphQL */ `query ListEmbeddingModels { + listEmbeddingModels { + provider + name + dimensions + default + __typename + } +} +` as GeneratedQuery< + APITypes.ListEmbeddingModelsQueryVariables, + APITypes.ListEmbeddingModelsQuery +>; +export const calculateEmbeddings = /* GraphQL */ `query CalculateEmbeddings($input: CalculateEmbeddingsInput!) { + calculateEmbeddings(input: $input) { + passage + vector + __typename + } +} +` as GeneratedQuery< + APITypes.CalculateEmbeddingsQueryVariables, + APITypes.CalculateEmbeddingsQuery +>; +export const getSession = /* GraphQL */ `query GetSession($id: String!) { + getSession(id: $id) { + id + title + startTime + history { + type + content + metadata + __typename + } + __typename + } +} +` as GeneratedQuery< + APITypes.GetSessionQueryVariables, + APITypes.GetSessionQuery +>; +export const listKendraIndexes = /* GraphQL */ `query ListKendraIndexes { + listKendraIndexes { + id + name + external + __typename + } +} +` as GeneratedQuery< + APITypes.ListKendraIndexesQueryVariables, + APITypes.ListKendraIndexesQuery +>; +export const isKendraDataSynching = /* GraphQL */ `query IsKendraDataSynching($workspaceId: String!) { + isKendraDataSynching(workspaceId: $workspaceId) +} +` as GeneratedQuery< + APITypes.IsKendraDataSynchingQueryVariables, + APITypes.IsKendraDataSynchingQuery +>; +export const listDocuments = /* GraphQL */ `query ListDocuments($input: ListDocumentsInput!) { + listDocuments(input: $input) { + items { + workspaceId + id + type + subType + status + title + path + sizeInBytes + vectors + subDocuments + crawlerProperties { + followLinks + limit + __typename + } + errors + createdAt + updatedAt + rssFeedId + rssLastCheckedAt + __typename + } + lastDocumentId + __typename + } +} +` as GeneratedQuery< + APITypes.ListDocumentsQueryVariables, + APITypes.ListDocumentsQuery +>; +export const getDocument = /* GraphQL */ `query GetDocument($input: GetDocumentInput!) { + getDocument(input: $input) { + workspaceId + id + type + subType + status + title + path + sizeInBytes + vectors + subDocuments + crawlerProperties { + followLinks + limit + __typename + } + errors + createdAt + updatedAt + rssFeedId + rssLastCheckedAt + __typename + } +} +` as GeneratedQuery< + APITypes.GetDocumentQueryVariables, + APITypes.GetDocumentQuery +>; +export const getRSSPosts = /* GraphQL */ `query GetRSSPosts($input: GetRSSPostsInput!) { + getRSSPosts(input: $input) { + items { + workspaceId + id + type + subType + status + title + path + sizeInBytes + vectors + subDocuments + crawlerProperties { + followLinks + limit + __typename + } + errors + createdAt + updatedAt + rssFeedId + rssLastCheckedAt + __typename + } + lastDocumentId + __typename + } +} +` as GeneratedQuery< + APITypes.GetRSSPostsQueryVariables, + APITypes.GetRSSPostsQuery +>; +export const listCrossEncoders = /* GraphQL */ `query ListCrossEncoders { + listCrossEncoders { + provider + name + default + __typename + } +} +` as GeneratedQuery< + APITypes.ListCrossEncodersQueryVariables, + APITypes.ListCrossEncodersQuery +>; +export const rankPassages = /* GraphQL */ `query RankPassages($input: RankPassagesInput!) { + rankPassages(input: $input) { + score + passage + __typename + } +} +` as GeneratedQuery< + APITypes.RankPassagesQueryVariables, + APITypes.RankPassagesQuery +>; diff --git a/lib/user-interface/react-app/src/graphql/subscriptions.ts b/lib/user-interface/react-app/src/graphql/subscriptions.ts new file mode 100644 index 000000000..7160163af --- /dev/null +++ b/lib/user-interface/react-app/src/graphql/subscriptions.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +// this is an auto generated file. This will be overwritten + +import * as APITypes from "../API"; +type GeneratedSubscription = string & { + __generatedSubscriptionInput: InputType; + __generatedSubscriptionOutput: OutputType; +}; + +export const receiveMessages = /* GraphQL */ `subscription ReceiveMessages($sessionId: String) { + receiveMessages(sessionId: $sessionId) { + data + sessionId + userId + __typename + } +} +` as GeneratedSubscription< + APITypes.ReceiveMessagesSubscriptionVariables, + APITypes.ReceiveMessagesSubscription +>; diff --git a/lib/user-interface/react-app/src/pages/chatbot/models/column-definitions.tsx b/lib/user-interface/react-app/src/pages/chatbot/models/column-definitions.tsx index a8d5927a9..6ac9e1dc5 100644 --- a/lib/user-interface/react-app/src/pages/chatbot/models/column-definitions.tsx +++ b/lib/user-interface/react-app/src/pages/chatbot/models/column-definitions.tsx @@ -3,48 +3,47 @@ import { PropertyFilterProperty, PropertyFilterOperator, } from "@cloudscape-design/collection-hooks"; -import { ModelItem } from "../../../common/types"; +import { Model } from "../../../API"; -export const ModelsColumnDefinitions: TableProps.ColumnDefinition[] = - [ - { - id: "provider", - header: "Provider", - sortingField: "provider", - cell: (item: ModelItem) => item.provider, - }, - { - id: "name", - header: "Name", - sortingField: "name", - cell: (item: ModelItem) => item.name, - isRowHeader: true, - }, - { - id: "ragSupported", - header: "RAG Supported", - sortingField: "ragSupported", - cell: (item: ModelItem) => (item.ragSupported ? "Yes" : "No"), - }, - { - id: "inputModalities", - header: "Input modalities", - sortingField: "inputModalities", - cell: (item: ModelItem) => item.inputModalities.join(", "), - }, - { - id: "outputModalities", - header: "Output modalities", - sortingField: "outputModalities", - cell: (item: ModelItem) => item.outputModalities.join(", "), - }, - { - id: "streaming", - header: "Streaming", - sortingField: "streaming", - cell: (item: ModelItem) => (item.streaming ? "Yes" : "No"), - }, - ]; +export const ModelsColumnDefinitions: TableProps.ColumnDefinition[] = [ + { + id: "provider", + header: "Provider", + sortingField: "provider", + cell: (item: Model) => item.provider, + }, + { + id: "name", + header: "Name", + sortingField: "name", + cell: (item: Model) => item.name, + isRowHeader: true, + }, + { + id: "ragSupported", + header: "RAG Supported", + sortingField: "ragSupported", + cell: (item: Model) => (item.ragSupported ? "Yes" : "No"), + }, + { + id: "inputModalities", + header: "Input modalities", + sortingField: "inputModalities", + cell: (item: Model) => item.inputModalities.join(", "), + }, + { + id: "outputModalities", + header: "Output modalities", + sortingField: "outputModalities", + cell: (item: Model) => item.outputModalities.join(", "), + }, + { + id: "streaming", + header: "Streaming", + sortingField: "streaming", + cell: (item: Model) => (item.streaming ? "Yes" : "No"), + }, +]; export const ModelsColumnFilteringProperties: PropertyFilterProperty[] = [ { diff --git a/lib/user-interface/react-app/src/pages/chatbot/models/models.tsx b/lib/user-interface/react-app/src/pages/chatbot/models/models.tsx index 6af1dd6eb..acc42ebbe 100644 --- a/lib/user-interface/react-app/src/pages/chatbot/models/models.tsx +++ b/lib/user-interface/react-app/src/pages/chatbot/models/models.tsx @@ -13,7 +13,6 @@ import { ApiClient } from "../../../common/api-client/api-client"; import { AppContext } from "../../../common/app-context"; import { TextHelper } from "../../../common/helpers/text-helper"; import { PropertyFilterI18nStrings } from "../../../common/i18n/property-filter-i18n-strings"; -import { ModelItem, ResultValue } from "../../../common/types"; import { TableEmptyState } from "../../../components/table-empty-state"; import { TableNoMatchState } from "../../../components/table-no-match-state"; import { @@ -21,11 +20,13 @@ import { ModelsColumnFilteringProperties, } from "./column-definitions"; import { CHATBOT_NAME } from "../../../common/constants"; +import { Model } from "../../../API"; +import { Utils } from "../../../common/utils"; export default function Models() { const onFollow = useOnFollow(); const appContext = useContext(AppContext); - const [models, setModels] = useState([]); + const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const { items, @@ -60,11 +61,13 @@ export default function Models() { if (!appContext) return; const apiClient = new ApiClient(appContext); - const result = await apiClient.models.getModels(); - if (ResultValue.ok(result)) { - setModels(result.data); - } + try { + const result = await apiClient.models.getModels(); + setModels(result.data!.listModels); + } catch (error) { + console.error(Utils.getErrorMessage(error)); + } setLoading(false); }, [appContext]); diff --git a/lib/user-interface/react-app/src/pages/rag/add-data/add-data.tsx b/lib/user-interface/react-app/src/pages/rag/add-data/add-data.tsx index f8f91a4e4..b6f821be1 100644 --- a/lib/user-interface/react-app/src/pages/rag/add-data/add-data.tsx +++ b/lib/user-interface/react-app/src/pages/rag/add-data/add-data.tsx @@ -10,11 +10,7 @@ import { Tabs, } from "@cloudscape-design/components"; import { useContext, useEffect, useState } from "react"; -import { - LoadingStatus, - ResultValue, - WorkspaceItem, -} from "../../../common/types"; +import { LoadingStatus } from "../../../common/types"; import { OptionsHelper } from "../../../common/helpers/options-helper"; import BaseAppLayout from "../../../components/base-app-layout"; import useOnFollow from "../../../common/hooks/use-on-follow"; @@ -30,6 +26,7 @@ import CrawlWebsite from "./crawl-website"; import DataFileUpload from "./data-file-upload"; import { CHATBOT_NAME } from "../../../common/constants"; import AddRssSubscription from "./add-rss-subscription"; +import { Workspace } from "../../../API"; export default function AddData() { const onFollow = useOnFollow(); @@ -37,7 +34,7 @@ export default function AddData() { const [searchParams, setSearchParams] = useSearchParams(); const [submitting, setSubmitting] = useState(false); const [activeTab, setActiveTab] = useState(searchParams.get("tab") || "file"); - const [workspaces, setWorkspaces] = useState([]); + const [workspaces, setWorkspaces] = useState([]); const [workspacesLoadingStatus, setWorkspacesLoadingStatus] = useState("loading"); const { data, onChange, errors, validate } = useForm({ @@ -68,12 +65,12 @@ export default function AddData() { (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.getWorkspaces(); + try { + const result = await apiClient.workspaces.getWorkspaces(); - if (ResultValue.ok(result)) { const workspaceId = searchParams.get("workspaceId"); if (workspaceId) { - const workspace = result.data.find( + const workspace = result.data?.listWorkspaces.find( (workspace) => workspace.id === workspaceId ); @@ -84,9 +81,9 @@ export default function AddData() { } } - setWorkspaces(result.data); + setWorkspaces(result.data?.listWorkspaces!); setWorkspacesLoadingStatus("finished"); - } else { + } catch (error) { setWorkspacesLoadingStatus("error"); } })(); diff --git a/lib/user-interface/react-app/src/pages/rag/add-data/add-qna.tsx b/lib/user-interface/react-app/src/pages/rag/add-data/add-qna.tsx index cf00d612c..4b6202d1a 100644 --- a/lib/user-interface/react-app/src/pages/rag/add-data/add-qna.tsx +++ b/lib/user-interface/react-app/src/pages/rag/add-data/add-qna.tsx @@ -13,14 +13,14 @@ import { useForm } from "../../../common/hooks/use-form"; import { useContext, useState } from "react"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { useNavigate } from "react-router-dom"; import { Utils } from "../../../common/utils"; +import { Workspace } from "../../../API"; export interface AddQnAProps { data: AddDataData; validate: () => boolean; - selectedWorkspace?: WorkspaceItem; + selectedWorkspace?: Workspace; submitting: boolean; setSubmitting: (submitting: boolean) => void; } @@ -70,13 +70,13 @@ export default function AddQnA(props: AddQnAProps) { setGlobalError(undefined); const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.addQnADocument( - props.data.workspace.value, - data.question, - data.answer - ); + try { + await apiClient.documents.addQnADocument( + props.data.workspace.value, + data.question, + data.answer + ); - if (ResultValue.ok(result)) { setFlashbarItem({ type: "success", content: "Q&A added successfully", @@ -89,8 +89,9 @@ export default function AddQnA(props: AddQnAProps) { }); onChange({ question: "", answer: "" }, true); - } else { - setGlobalError(Utils.getErrorMessage(result)); + } catch (error: any) { + console.error(Utils.getErrorMessage(error)); + setGlobalError(Utils.getErrorMessage(error)); } props.setSubmitting(false); diff --git a/lib/user-interface/react-app/src/pages/rag/add-data/add-rss-subscription.tsx b/lib/user-interface/react-app/src/pages/rag/add-data/add-rss-subscription.tsx index 3f1dcf7d8..ebf3c7bf6 100644 --- a/lib/user-interface/react-app/src/pages/rag/add-data/add-rss-subscription.tsx +++ b/lib/user-interface/react-app/src/pages/rag/add-data/add-rss-subscription.tsx @@ -15,13 +15,13 @@ import { useContext, useState } from "react"; import { AppContext } from "../../../common/app-context"; import { useNavigate } from "react-router-dom"; import { Utils } from "../../../common/utils"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { ApiClient } from "../../../common/api-client/api-client"; +import { Workspace } from "../../../API"; export interface AddRssSubscriptionProps { data: AddDataData; validate: () => boolean; - selectedWorkspace?: WorkspaceItem; + selectedWorkspace?: Workspace; submitting: boolean; setSubmitting: (submitting: boolean) => void; } @@ -77,15 +77,15 @@ export default function AddRssSubscription(props: AddRssSubscriptionProps) { setGlobalError(undefined); const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.addRssFeedSubscription( - props.data.workspace.value, - data.rssFeedUrl, - data.rssFeedTitle, - data.linkLimit, - data.followLinks - ); + try { + await apiClient.documents.addRssFeedSubscription( + props.data.workspace.value, + data.rssFeedUrl, + data.rssFeedTitle, + data.linkLimit, + data.followLinks + ); - if (ResultValue.ok(result)) { setFlashbarItem({ type: "success", content: "RSS Feed subscribed successfully", @@ -101,8 +101,9 @@ export default function AddRssSubscription(props: AddRssSubscriptionProps) { onChange({ rssFeedUrl: "" }, true); onChange({ rssFeedTitle: "" }, true); - } else { - setGlobalError(Utils.getErrorMessage(result)); + } catch (error: any) { + console.error(Utils.getErrorMessage(error)); + setGlobalError(Utils.getErrorMessage(error)); } props.setSubmitting(false); diff --git a/lib/user-interface/react-app/src/pages/rag/add-data/add-text.tsx b/lib/user-interface/react-app/src/pages/rag/add-data/add-text.tsx index ccb6f20a4..45e7f9752 100644 --- a/lib/user-interface/react-app/src/pages/rag/add-data/add-text.tsx +++ b/lib/user-interface/react-app/src/pages/rag/add-data/add-text.tsx @@ -14,14 +14,14 @@ import { useForm } from "../../../common/hooks/use-form"; import { useContext, useState } from "react"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { Utils } from "../../../common/utils"; import { useNavigate } from "react-router-dom"; +import { Workspace } from "../../../API"; export interface AddTextProps { data: AddDataData; validate: () => boolean; - selectedWorkspace?: WorkspaceItem; + selectedWorkspace?: Workspace; submitting: boolean; setSubmitting: (submitting: boolean) => void; } @@ -71,13 +71,13 @@ export default function AddText(props: AddTextProps) { setGlobalError(undefined); const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.addTextDocument( - props.data.workspace.value, - data.title, - data.content - ); + try { + await apiClient.documents.addTextDocument( + props.data.workspace.value, + data.title, + data.content + ); - if (ResultValue.ok(result)) { setFlashbarItem({ type: "success", content: "Text added successfully", @@ -90,8 +90,9 @@ export default function AddText(props: AddTextProps) { }); onChange({ title: "", content: "" }, true); - } else { - setGlobalError(Utils.getErrorMessage(result)); + } catch (error: any) { + console.log(Utils.getErrorMessage(error)); + setGlobalError(Utils.getErrorMessage(error)); } props.setSubmitting(false); diff --git a/lib/user-interface/react-app/src/pages/rag/add-data/crawl-website.tsx b/lib/user-interface/react-app/src/pages/rag/add-data/crawl-website.tsx index ad8ae871b..3547be90b 100644 --- a/lib/user-interface/react-app/src/pages/rag/add-data/crawl-website.tsx +++ b/lib/user-interface/react-app/src/pages/rag/add-data/crawl-website.tsx @@ -16,13 +16,13 @@ import { useContext, useState } from "react"; import { AppContext } from "../../../common/app-context"; import { useNavigate } from "react-router-dom"; import { Utils } from "../../../common/utils"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { ApiClient } from "../../../common/api-client/api-client"; +import { Workspace } from "../../../API"; export interface CrawlWebsiteProps { data: AddDataData; validate: () => boolean; - selectedWorkspace?: WorkspaceItem; + selectedWorkspace?: Workspace; submitting: boolean; setSubmitting: (submitting: boolean) => void; } @@ -91,15 +91,15 @@ export default function CrawlWebsite(props: CrawlWebsiteProps) { const apiClient = new ApiClient(appContext); const isSitemap = data.urlType === "sitemap"; - const result = await apiClient.documents.addWebsiteDocument( - props.data.workspace.value, - isSitemap, - isSitemap ? data.sitemapUrl : data.websiteUrl, - data.followLinks, - data.limit - ); + try { + await apiClient.documents.addWebsiteDocument( + props.data.workspace.value, + isSitemap, + isSitemap ? data.sitemapUrl : data.websiteUrl, + data.followLinks, + data.limit + ); - if (ResultValue.ok(result)) { setFlashbarItem({ type: "success", content: "Website added successfully", @@ -114,8 +114,9 @@ export default function CrawlWebsite(props: CrawlWebsiteProps) { }); onChange({ websiteUrl: "", sitemapUrl: "" }, true); - } else { - setGlobalError(Utils.getErrorMessage(result)); + } catch (error: any) { + setGlobalError(Utils.getErrorMessage(error)); + console.error(Utils.getErrorMessage(error)); } props.setSubmitting(false); diff --git a/lib/user-interface/react-app/src/pages/rag/add-data/data-file-upload.tsx b/lib/user-interface/react-app/src/pages/rag/add-data/data-file-upload.tsx index 50523d66a..0c2ff698e 100644 --- a/lib/user-interface/react-app/src/pages/rag/add-data/data-file-upload.tsx +++ b/lib/user-interface/react-app/src/pages/rag/add-data/data-file-upload.tsx @@ -14,15 +14,15 @@ import { useContext, useState } from "react"; import { AddDataData } from "./types"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { Utils } from "../../../common/utils"; import { FileUploader } from "../../../common/file-uploader"; import { useNavigate } from "react-router-dom"; +import { Workspace } from "../../../API"; export interface DataFileUploadProps { data: AddDataData; validate: () => boolean; - selectedWorkspace?: WorkspaceItem; + selectedWorkspace?: Workspace; } const fileExtensions = new Set([ @@ -111,19 +111,23 @@ export default function DataFileUpload(props: DataFileUploadProps) { setCurrentFileName(file.name); let fileUploaded = 0; - const result = await apiClient.documents.presignedFileUploadPost( - props.data.workspace?.value, - file.name - ); + try { + const result = await apiClient.documents.presignedFileUploadPost( + props.data.workspace?.value, + file.name + ); - if (ResultValue.ok(result)) { try { - await uploader.upload(file, result.data, (uploaded: number) => { - fileUploaded = uploaded; - const totalUploaded = fileUploaded + accumulator; - const percent = Math.round((totalUploaded / totalSize) * 100); - setUploadProgress(percent); - }); + await uploader.upload( + file, + result.data!.getUploadFileURL!, + (uploaded: number) => { + fileUploaded = uploaded; + const totalUploaded = fileUploaded + accumulator; + const percent = Math.round((totalUploaded / totalSize) * 100); + setUploadProgress(percent); + } + ); accumulator += file.size; setUploadingIndex(Math.min(filesToUpload.length, i + 2)); @@ -133,8 +137,9 @@ export default function DataFileUpload(props: DataFileUploadProps) { hasError = true; break; } - } else { - setGlobalError(Utils.getErrorMessage(result)); + } catch (error: any) { + setGlobalError(Utils.getErrorMessage(error)); + console.error(Utils.getErrorMessage(error)); setUploadingStatus("error"); hasError = true; break; diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-aurora.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-aurora.tsx index ed6678b23..325b0cd27 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-aurora.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-aurora.tsx @@ -1,6 +1,6 @@ import { SpaceBetween, Button, Form } from "@cloudscape-design/components"; import { useContext, useState } from "react"; -import { AuroraWorkspaceCreateInput, ResultValue } from "../../../common/types"; +import { AuroraWorkspaceCreateInput } from "../../../common/types"; import { useForm } from "../../../common/hooks/use-form"; import { ApiClient } from "../../../common/api-client/api-client"; import { AppContext } from "../../../common/app-context"; @@ -133,28 +133,31 @@ export default function CreateWorkspaceAurora() { ); const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.createAuroraWorkspace({ - name: data.name.trim(), - embeddingsModelProvider: embeddingsModel.provider, - embeddingsModelName: embeddingsModel.name, - crossEncoderModelProvider: crossEncoderModel.provider, - crossEncoderModelName: crossEncoderModel.name, - languages: data.languages.map((x) => x.value ?? ""), - metric: data.metric, - index: data.index, - hybridSearch: data.hybridSearch, - chunking_strategy: "recursive", - chunkSize: data.chunkSize, - chunkOverlap: data.chunkOverlap, - }); - - if (ResultValue.ok(result)) { - navigate("/rag/workspaces"); + try { + const result = await apiClient.workspaces.createAuroraWorkspace({ + name: data.name.trim(), + embeddingsModelProvider: embeddingsModel.provider, + embeddingsModelName: embeddingsModel.name, + crossEncoderModelProvider: crossEncoderModel.provider, + crossEncoderModelName: crossEncoderModel.name, + languages: data.languages.map((x) => x.value ?? ""), + metric: data.metric, + index: data.index, + hybridSearch: data.hybridSearch, + chunkingStrategy: "recursive", + chunkSize: data.chunkSize, + chunkOverlap: data.chunkOverlap, + }); + + navigate(`/rag/workspaces/${result.data?.createAuroraWorkspace.id}`); return; + } catch (e: any) { + setSubmitting(false); + console.error( + `Invocation error: ${e.errors.map((x: any) => x.message).join("")}` + ); + setGlobalError("Something went wrong"); } - - setSubmitting(false); - setGlobalError("Something went wrong"); }; if (Utils.isDevelopment()) { diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-kendra.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-kendra.tsx index 37defce5f..194fe92b8 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-kendra.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-kendra.tsx @@ -3,7 +3,7 @@ import { useContext, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Utils } from "../../../common/utils"; import { useForm } from "../../../common/hooks/use-form"; -import { KendraWorkspaceCreateInput, ResultValue } from "../../../common/types"; +import { KendraWorkspaceCreateInput } from "../../../common/types"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; import RouterButton from "../../../components/wrappers/router-button"; @@ -58,19 +58,19 @@ export default function CreateWorkspaceKendra() { setSubmitting(true); const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.createKendraWorkspace({ - name: data.name.trim(), - kendraIndexId: data.kendraIndex?.value ?? "", - useAllData: data.useAllData, - }); + try { + await apiClient.workspaces.createKendraWorkspace({ + name: data.name.trim(), + kendraIndexId: data.kendraIndex?.value ?? "", + useAllData: data.useAllData, + }); - if (ResultValue.ok(result)) { navigate("/rag/workspaces"); return; + } catch (e) { + setSubmitting(false); + setGlobalError("Something went wrong"); } - - setSubmitting(false); - setGlobalError("Something went wrong"); }; if (Utils.isDevelopment()) { diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-opensearch.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-opensearch.tsx index 33821d1e0..a297536d7 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-opensearch.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace-opensearch.tsx @@ -3,10 +3,7 @@ import { useContext, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Utils } from "../../../common/utils"; import { useForm } from "../../../common/hooks/use-form"; -import { - OpenSearchWorkspaceCreateInput, - ResultValue, -} from "../../../common/types"; +import { OpenSearchWorkspaceCreateInput } from "../../../common/types"; import { EmbeddingsModelHelper } from "../../../common/helpers/embeddings-model-helper"; import { AppContext } from "../../../common/app-context"; import { OptionsHelper } from "../../../common/helpers/options-helper"; @@ -108,26 +105,26 @@ export default function CreateWorkspaceOpenSearch() { ); const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.createOpenSearchWorkspace({ - name: data.name.trim(), - embeddingsModelProvider: embeddingsModel.provider, - embeddingsModelName: embeddingsModel.name, - crossEncoderModelProvider: crossEncoderModel.provider, - crossEncoderModelName: crossEncoderModel.name, - languages: data.languages.map((x) => x.value ?? ""), - hybridSearch: data.hybridSearch, - chunking_strategy: "recursive", - chunkSize: data.chunkSize, - chunkOverlap: data.chunkOverlap, - }); - - if (ResultValue.ok(result)) { + try { + await apiClient.workspaces.createOpenSearchWorkspace({ + name: data.name.trim(), + embeddingsModelProvider: embeddingsModel.provider, + embeddingsModelName: embeddingsModel.name, + crossEncoderModelProvider: crossEncoderModel.provider, + crossEncoderModelName: crossEncoderModel.name, + languages: data.languages.map((x) => x.value ?? ""), + hybridSearch: data.hybridSearch, + chunkingStrategy: "recursive", + chunkSize: data.chunkSize, + chunkOverlap: data.chunkOverlap, + }); + navigate("/rag/workspaces"); return; + } catch (e) { + setSubmitting(false); + setGlobalError("Something went wrong"); } - - setSubmitting(false); - setGlobalError("Something went wrong"); }; if (Utils.isDevelopment()) { diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace.tsx index b389f78f5..90888c3f1 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/create-workspace.tsx @@ -8,7 +8,6 @@ import { import { CreateWorkspaceHeader } from "./create-workspace-header"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; -import { ResultValue } from "../../../common/types"; import BaseAppLayout from "../../../components/base-app-layout"; import useOnFollow from "../../../common/hooks/use-on-follow"; import CreateWorkspaceOpenSearch from "./create-workspace-opensearch"; @@ -16,6 +15,7 @@ import CreateWorkspaceAurora from "./create-workspace-aurora"; import CreateWorkspaceKendra from "./create-workspace-kendra"; import SelectEnginePanel from "./select-engine-panel"; import { CHATBOT_NAME } from "../../../common/constants"; +import { Utils } from "../../../common/utils"; export default function CreateWorkspace() { const onFollow = useOnFollow(); @@ -29,16 +29,16 @@ export default function CreateWorkspace() { (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.ragEngines.getRagEngines(); + try { + const result = await apiClient.ragEngines.getRagEngines(); - const engineMap = new Map(); + const engineMap = new Map(); - if (ResultValue.ok(result)) { - result.data.forEach((engine) => { + result.data!.listRagEngines.forEach((engine) => { engineMap.set(engine.id, engine.enabled); }); - if (result.data.length > 0) { + if (result.data!.listRagEngines.length > 0) { if (engineMap.get("aurora") === true) { setEngine("aurora"); } else if (engineMap.get("opensearch") === true) { @@ -49,6 +49,8 @@ export default function CreateWorkspace() { } setEngines(engineMap); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } setLoading(false); diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/cross-encoder-selector-field.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/cross-encoder-selector-field.tsx index 59325f0ee..ea65c2a97 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/cross-encoder-selector-field.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/cross-encoder-selector-field.tsx @@ -1,13 +1,11 @@ import { useContext, useEffect, useState } from "react"; import { ApiClient } from "../../../common/api-client/api-client"; -import { - CrossEncoderModelItem, - LoadingStatus, - ResultValue, -} from "../../../common/types"; +import { LoadingStatus } from "../../../common/types"; import { AppContext } from "../../../common/app-context"; import { OptionsHelper } from "../../../common/helpers/options-helper"; import { Select, SelectProps } from "@cloudscape-design/components"; +import { CrossEncoderData } from "../../../API"; +import { Utils } from "../../../common/utils"; interface CrossEncoderSelectorProps { submitting: boolean; @@ -21,7 +19,7 @@ export function CrossEncoderSelectorField(props: CrossEncoderSelectorProps) { const [crossEncoderModelsStatus, setCrossEncoderModelsStatus] = useState("loading"); const [crossEncoderModels, setCrossEncoderModels] = useState< - CrossEncoderModelItem[] + CrossEncoderData[] >([]); useEffect(() => { @@ -29,12 +27,14 @@ export function CrossEncoderSelectorField(props: CrossEncoderSelectorProps) { (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.crossEncoders.getModels(); + try { + const result = await apiClient.crossEncoders.getModels(); - if (ResultValue.ok(result)) { - setCrossEncoderModels(result.data); + setCrossEncoderModels(result.data?.listCrossEncoders!); setCrossEncoderModelsStatus("finished"); - } else { + } catch (error) { + console.error(Utils.getErrorMessage(error)); + setCrossEncoderModels([]); setCrossEncoderModelsStatus("error"); } })(); diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/embeddings-selector-field.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/embeddings-selector-field.tsx index 4e94ac715..8053d3457 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/embeddings-selector-field.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/embeddings-selector-field.tsx @@ -1,13 +1,11 @@ import { FormField, Select, SelectProps } from "@cloudscape-design/components"; -import { - EmbeddingsModelItem, - LoadingStatus, - ResultValue, -} from "../../../common/types"; +import { LoadingStatus } from "../../../common/types"; import { EmbeddingsModelHelper } from "../../../common/helpers/embeddings-model-helper"; import { useContext, useEffect, useState } from "react"; import { ApiClient } from "../../../common/api-client/api-client"; import { AppContext } from "../../../common/app-context"; +import { EmbeddingModel } from "../../../API"; +import { Utils } from "../../../common/utils"; interface EmbeddingsSelectionProps { submitting: boolean; @@ -20,21 +18,22 @@ export default function EmbeddingSelector(props: EmbeddingsSelectionProps) { const appContext = useContext(AppContext); const [embeddingsModelsStatus, setEmbeddingsModelsStatus] = useState("loading"); - const [embeddingsModels, setEmbeddingsModels] = useState< - EmbeddingsModelItem[] - >([]); + const [embeddingsModels, setEmbeddingsModels] = useState( + [] + ); useEffect(() => { if (!appContext?.config) return; (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.embeddings.getModels(); + try { + const result = await apiClient.embeddings.getModels(); - if (ResultValue.ok(result)) { - setEmbeddingsModels(result.data); + setEmbeddingsModels(result.data!.listEmbeddingModels); setEmbeddingsModelsStatus("finished"); - } else { + } catch (error) { + console.error(Utils.getErrorMessage(error)); setEmbeddingsModelsStatus("error"); } })(); diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/kendra-form.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/kendra-form.tsx index 11a0e8c7e..9aed83a49 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/kendra-form.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/kendra-form.tsx @@ -1,9 +1,7 @@ import { useContext, useEffect, useState } from "react"; import { - KendraIndexItem, KendraWorkspaceCreateInput, LoadingStatus, - ResultValue, } from "../../../common/types"; import { Container, @@ -17,6 +15,7 @@ import { } from "@cloudscape-design/components"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; +import { KendraIndex } from "../../../API"; export interface KendraFormProps { data: KendraWorkspaceCreateInput; @@ -29,36 +28,44 @@ export default function KendraForm(props: KendraFormProps) { const appContext = useContext(AppContext); const [kendraIndexStatus, setKendraIndexStatus] = useState("loading"); - const [kendraIndexes, setKendraIndexes] = useState([]); + const [kendraIndexes, setKendraIndexes] = useState< + KendraIndex[] | null | undefined + >([]); useEffect(() => { if (!appContext) return; (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.kendra.getKendraIndexes(); + try { + const result = await apiClient.kendra.getKendraIndexes(); - if (ResultValue.ok(result)) { - const data = result.data?.sort((a, b) => a.name.localeCompare(b.name)); + const data = result.data?.listKendraIndexes.sort((a, b) => + a.name.localeCompare(b.name) + ); setKendraIndexes(data); setKendraIndexStatus("finished"); - } else { + } catch (error) { setKendraIndexStatus("error"); + console.error(error); } })(); }, [appContext]); - const kendraIndexOptions: SelectProps.Option[] = kendraIndexes.map((item) => { - return { - label: item.name, - value: item.id, - description: item.id, - }; - }); + const kendraIndexOptions: SelectProps.Option[] = kendraIndexes + ? kendraIndexes.map((item) => { + return { + label: item.name, + value: item.id, + description: item.id, + }; + }) + : []; - const externalSelected = - kendraIndexes.find((c) => c.id === props.data.kendraIndex?.value) - ?.external === true; + const externalSelected = kendraIndexes + ? kendraIndexes.find((c) => c.id === props.data.kendraIndex?.value) + ?.external === true + : false; return ( ("loading"); const [crossEncoderModels, setCrossEncoderModels] = useState< - CrossEncoderModelItem[] + CrossEncoderData[] >([]); const [ranking, setRanking] = useState< | { @@ -103,12 +99,14 @@ export default function CrossEncoders() { (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.crossEncoders.getModels(); + try { + const result = await apiClient.crossEncoders.getModels(); - if (ResultValue.ok(result)) { - setCrossEncoderModels(result.data); + console.log(result?.data?.listCrossEncoders); + setCrossEncoderModels(result?.data?.listCrossEncoders!); setCrossEncoderModelsStatus("finished"); - } else { + } catch (error) { + console.error(Utils.getErrorMessage(error)); setCrossEncoderModelsStatus("error"); } })(); @@ -159,18 +157,19 @@ export default function CrossEncoders() { data.passages.map((p) => p.trim()) ); - if (ResultValue.ok(result)) { - const passages = data.passages - .map((passage, index) => ({ + console.log(result); + if (result.errors === undefined) { + const passages = result + .data!.rankPassages!.map((rank, index) => ({ index, - passage, - score: result.data[index], + passage: rank.passage, + score: rank.score, })) .sort((a, b) => b.score - a.score); setRanking(passages); - } else if (result.message) { - setGlobalError(Utils.getErrorMessage(result)); + } else { + setGlobalError(result.errors.map((x) => x.message).join(",")); } setSubmitting(false); diff --git a/lib/user-interface/react-app/src/pages/rag/dashboard/column-definitions.tsx b/lib/user-interface/react-app/src/pages/rag/dashboard/column-definitions.tsx index e3831be27..c62464989 100644 --- a/lib/user-interface/react-app/src/pages/rag/dashboard/column-definitions.tsx +++ b/lib/user-interface/react-app/src/pages/rag/dashboard/column-definitions.tsx @@ -3,18 +3,18 @@ import { PropertyFilterProperty, PropertyFilterOperator, } from "@cloudscape-design/collection-hooks"; -import { WorkspaceItem } from "../../../common/types"; import { Labels } from "../../../common/constants"; import { DateTime } from "luxon"; import RouterLink from "../../../components/wrappers/router-link"; +import { Workspace } from "../../../API"; -export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition[] = +export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition[] = [ { id: "name", header: "Name", sortingField: "name", - cell: (item: WorkspaceItem) => ( + cell: (item: Workspace) => ( {item.name} ), isRowHeader: true, @@ -23,15 +23,15 @@ export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition Labels.engineMap[item.engine], + cell: (item: Workspace) => Labels.engineMap[item.engine], }, { id: "starus", header: "Status", sortingField: "status", cell: (item) => ( - - {Labels.statusMap[item.status]} + + {Labels.statusMap[item.status!]} ), minWidth: 120, @@ -40,13 +40,13 @@ export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition item.documents, + cell: (item: Workspace) => item.documents, }, { id: "timestamp", header: "Creation Date", sortingField: "timestamp", - cell: (item: WorkspaceItem) => + cell: (item: Workspace) => DateTime.fromISO(new Date(item.createdAt).toISOString()).toLocaleString( DateTime.DATETIME_SHORT ), diff --git a/lib/user-interface/react-app/src/pages/rag/dashboard/dashboard.tsx b/lib/user-interface/react-app/src/pages/rag/dashboard/dashboard.tsx index 70184c940..d93a1f395 100644 --- a/lib/user-interface/react-app/src/pages/rag/dashboard/dashboard.tsx +++ b/lib/user-interface/react-app/src/pages/rag/dashboard/dashboard.tsx @@ -1,7 +1,6 @@ import { ContentLayout, SpaceBetween } from "@cloudscape-design/components"; import { BreadcrumbGroup } from "@cloudscape-design/components"; import { useContext, useEffect, useState } from "react"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { ApiClient } from "../../../common/api-client/api-client"; import { AppContext } from "../../../common/app-context"; import DashboardHeader from "./dashboard-header"; @@ -10,12 +9,13 @@ import useOnFollow from "../../../common/hooks/use-on-follow"; import BaseAppLayout from "../../../components/base-app-layout"; import GeneralConfig, { WorkspacesStatistics } from "./general-config"; import { CHATBOT_NAME } from "../../../common/constants"; +import { Workspace } from "../../../API"; export default function Dashboard() { const onFollow = useOnFollow(); const appContext = useContext(AppContext); const [loading, setLoading] = useState(true); - const [workspaces, setWorkspaces] = useState([]); + const [workspaces, setWorkspaces] = useState([]); const [statistics, setStatistics] = useState( null ); @@ -26,20 +26,23 @@ export default function Dashboard() { console.log("WorkspacesTable: useEffect"); const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.getWorkspaces(); - if (ResultValue.ok(result)) { - const data = result.data; + try { + const result = await apiClient.workspaces.getWorkspaces(); + + const data = result.data?.listWorkspaces!; setWorkspaces(data); console.log(data); setStatistics({ count: data.length, - documents: data.reduce((a, b) => a + b.documents, 0), - vectors: data.reduce((a, b) => a + b.vectors, 0), - sizeInBytes: data.reduce((a, b) => a + b.sizeInBytes, 0), + documents: data.reduce((a, b) => a + b.documents!, 0), + vectors: data.reduce((a, b) => a + b.vectors!, 0), + sizeInBytes: data.reduce((a, b) => a + b.sizeInBytes!, 0), }); - } - setLoading(false); + setLoading(false); + } catch (e) { + console.log(e); + } })(); }, [appContext]); diff --git a/lib/user-interface/react-app/src/pages/rag/dashboard/workspaces-table.tsx b/lib/user-interface/react-app/src/pages/rag/dashboard/workspaces-table.tsx index 0fac7c2a7..f86b8f0fd 100644 --- a/lib/user-interface/react-app/src/pages/rag/dashboard/workspaces-table.tsx +++ b/lib/user-interface/react-app/src/pages/rag/dashboard/workspaces-table.tsx @@ -6,19 +6,19 @@ import { TableProps, } from "@cloudscape-design/components"; import { useState } from "react"; -import { WorkspaceItem } from "../../../common/types"; import { TextHelper } from "../../../common/helpers/text-helper"; import { WorkspacesColumnDefinitions } from "./column-definitions"; import RouterLink from "../../../components/wrappers/router-link"; import RouterButton from "../../../components/wrappers/router-button"; +import { Workspace } from "../../../API"; export interface WorkspacesTableProps { loading: boolean; - workspaces: WorkspaceItem[]; + workspaces: Workspace[]; } export default function WorkspacesTable(props: WorkspacesTableProps) { - const [selectedItems, setSelectedItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); const isOnlyOneSelected = selectedItems.length === 1; return ( @@ -46,7 +46,7 @@ export default function WorkspacesTable(props: WorkspacesTableProps) { items={props.workspaces.slice(0, 5)} selectedItems={selectedItems} onSelectionChange={(event: { - detail: TableProps.SelectionChangeDetail; + detail: TableProps.SelectionChangeDetail; }) => setSelectedItems(event.detail.selectedItems)} header={

("loading"); const [embeddingsModelsResults, setEmbeddingsModelsResults] = useState< - EmbeddingsModelItem[] + EmbeddingModel[] >([]); - const [embeddings, setEmbeddings] = useState(null); + const [embeddings, setEmbeddings] = useState(null); const [metricsMatrices, setMetricsMatrices] = useState< { embeddingsVector: number[][]; @@ -104,12 +101,13 @@ export default function Embeddings() { (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.embeddings.getModels(); + try { + const result = await apiClient.embeddings.getModels(); - if (ResultValue.ok(result)) { - setEmbeddingsModelsResults(result.data); + setEmbeddingsModelsResults(result.data!.listEmbeddingModels!); setEmbeddingsModelsStatus("finished"); - } else { + } catch (error) { + console.error(Utils.getErrorMessage(error)); setEmbeddingsModelsStatus("error"); } })(); @@ -158,21 +156,27 @@ export default function Embeddings() { }); const matricesResults = []; - await Promise.all(results); - for (let i = 0; i < results.length; i++) { - console.log(`getting results for ${i}`); - const result = await results[i]; - if (ResultValue.ok(result)) { - const matrices = MetricsHelper.matrices(result.data); + try { + await Promise.all(results); + for (let i = 0; i < results.length; i++) { + console.log(`getting results for ${i}`); + const result = await results[i]; + + const matrices = MetricsHelper.matrices( + result.data!.calculateEmbeddings.map((x) => x!.vector) + ); console.log(` results ${i} OK`); matricesResults.push({ embeddingModel: embeddingModels[i], - embeddingsVector: result.data, + embeddingsVector: result.data!.calculateEmbeddings.map( + (x) => x!.vector + ), ...matrices, }); - } else if (result.message) { - setGlobalError(Utils.getErrorMessage(result)); } + } catch (error) { + console.error(Utils.getErrorMessage(error)); + setGlobalError("Error while calculating embeddings"); } console.log(`setting matrices - ${matricesResults.length}`); setMetricsMatrices(matricesResults); diff --git a/lib/user-interface/react-app/src/pages/rag/engines/engines.tsx b/lib/user-interface/react-app/src/pages/rag/engines/engines.tsx index 6f46c8dd3..d1f0b1bc6 100644 --- a/lib/user-interface/react-app/src/pages/rag/engines/engines.tsx +++ b/lib/user-interface/react-app/src/pages/rag/engines/engines.tsx @@ -5,16 +5,16 @@ import { Header, } from "@cloudscape-design/components"; import { EnginesPageHeader } from "./engines-page-header"; -import { EngineItem, ResultValue } from "../../../common/types"; import { ApiClient } from "../../../common/api-client/api-client"; import { AppContext } from "../../../common/app-context"; import { useContext, useEffect, useState } from "react"; import useOnFollow from "../../../common/hooks/use-on-follow"; import BaseAppLayout from "../../../components/base-app-layout"; import { CHATBOT_NAME } from "../../../common/constants"; +import { RagEngine } from "../../../API"; const CARD_DEFINITIONS = { - header: (item: EngineItem) => ( + header: (item: RagEngine) => (
{item.name}
@@ -23,12 +23,12 @@ const CARD_DEFINITIONS = { { id: "id", header: "id", - content: (item: EngineItem) => item.id, + content: (item: RagEngine) => item.id, }, { id: "state", header: "State", - content: (item: EngineItem) => ( + content: (item: RagEngine) => ( {item.enabled ? "Enabled" : "Disabled"} @@ -40,7 +40,7 @@ const CARD_DEFINITIONS = { export default function Engines() { const onFollow = useOnFollow(); const appContext = useContext(AppContext); - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { @@ -48,10 +48,12 @@ export default function Engines() { (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.ragEngines.getRagEngines(); + try { + const result = await apiClient.ragEngines.getRagEngines(); - if (ResultValue.ok(result)) { - setData(result.data); + setData(result.data?.listRagEngines!); + } catch (error) { + console.error(error); } setLoading(false); diff --git a/lib/user-interface/react-app/src/pages/rag/semantic-search/result-items.tsx b/lib/user-interface/react-app/src/pages/rag/semantic-search/result-items.tsx index 0f6720d66..193b607de 100644 --- a/lib/user-interface/react-app/src/pages/rag/semantic-search/result-items.tsx +++ b/lib/user-interface/react-app/src/pages/rag/semantic-search/result-items.tsx @@ -5,15 +5,12 @@ import { Header, SpaceBetween, } from "@cloudscape-design/components"; -import { - SemanticSearchResult, - SemanticSearchResultItem, -} from "../../../common/types"; import { Labels } from "../../../common/constants"; import React from "react"; +import { SemanticSearchItem, SemanticSearchResult } from "../../../API"; export interface ResultItemsProps { - items: SemanticSearchResultItem[]; + items: SemanticSearchItem[]; result: SemanticSearchResult; } @@ -51,7 +48,7 @@ export default function ResultItems(props: ResultItemsProps) { } function ItemDetails(props: { - item: SemanticSearchResultItem; + item: SemanticSearchItem; result: SemanticSearchResult; }) { const { item, result } = props; @@ -69,7 +66,9 @@ function ItemDetails(props: {
Sources
- {item.sources.map((c) => Labels.sourceTypeMap[c]).join(", ")} + {item + .sources!.map((c: any) => Labels.sourceTypeMap[c]) + .join(", ")}
@@ -101,7 +100,7 @@ function ItemDetails(props: {
Sources
- {item.sources.map((c) => Labels.sourceTypeMap[c]).join(", ")} + {item.sources!.map((c: any) => Labels.sourceTypeMap[c]).join(", ")}
diff --git a/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search-details.tsx b/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search-details.tsx index 575e62b4d..e7d33ec24 100644 --- a/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search-details.tsx +++ b/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search-details.tsx @@ -1,4 +1,3 @@ -import { SemanticSearchResult } from "../../../common/types"; import { ExpandableSection, ColumnLayout, @@ -7,6 +6,7 @@ import { } from "@cloudscape-design/components"; import { Labels } from "../../../common/constants"; import RouterLink from "../../../components/wrappers/router-link"; +import { SemanticSearchResult } from "../../../API"; export interface SemanticSearchDetailsProps { searchResults: SemanticSearchResult | null; diff --git a/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search.tsx b/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search.tsx index d0540a29e..62d85bf86 100644 --- a/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search.tsx +++ b/lib/user-interface/react-app/src/pages/rag/semantic-search/semantic-search.tsx @@ -19,12 +19,7 @@ import BaseAppLayout from "../../../components/base-app-layout"; import { useContext, useEffect, useState } from "react"; import { useForm } from "../../../common/hooks/use-form"; import { AppContext } from "../../../common/app-context"; -import { - LoadingStatus, - ResultValue, - SemanticSearchResult, - WorkspaceItem, -} from "../../../common/types"; +import { LoadingStatus } from "../../../common/types"; import { OptionsHelper } from "../../../common/helpers/options-helper"; import { ApiClient } from "../../../common/api-client/api-client"; import { useSearchParams } from "react-router-dom"; @@ -32,6 +27,7 @@ import { Utils } from "../../../common/utils"; import SemanticSearchDetails from "./semantic-search-details"; import ResultItems from "./result-items"; import { CHATBOT_NAME } from "../../../common/constants"; +import { SemanticSearchResult, Workspace } from "../../../API"; interface SemanticSearchData { workspace: SelectProps.Option | null; @@ -42,15 +38,15 @@ export default function SemanticSearch() { const onFollow = useOnFollow(); const appContext = useContext(AppContext); const [searchParams, setSearchParams] = useSearchParams(); - const [searchResult, setSearchResult] = useState( - null - ); + const [searchResult, setSearchResult] = useState< + SemanticSearchResult | null | undefined + >(null); const [submitting, setSubmitting] = useState(false); const [detailsExpanded, setDetailsExpanded] = useState(false); const [globalError, setGlobalError] = useState(undefined); const [workspacesLoadingStatus, setWorkspacesLoadingStatus] = useState("loading"); - const [workspaces, setWorkspaces] = useState([]); + const [workspaces, setWorkspaces] = useState([]); const { data, onChange, errors, validate } = useForm({ initialValue: () => { return { @@ -83,15 +79,15 @@ export default function SemanticSearch() { setSearchResult(null); const apiClient = new ApiClient(appContext); - const result = await apiClient.semanticSearch.query( - data.workspace?.value, - data.query - ); - - if (ResultValue.ok(result)) { - setSearchResult(result.data); - } else { - setGlobalError(Utils.getErrorMessage(result)); + try { + const result = await apiClient.semanticSearch.query( + data.workspace?.value, + data.query + ); + setSearchResult(result.data?.performSemanticSearch); + } catch (error: any) { + console.error(error); + setGlobalError(error.errors.map((x: any) => x.message).join(",")); } setSubmitting(false); @@ -102,12 +98,12 @@ export default function SemanticSearch() { (async () => { const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.getWorkspaces(); + try { + const result = await apiClient.workspaces.getWorkspaces(); - if (ResultValue.ok(result)) { const workspaceId = searchParams.get("workspaceId"); if (workspaceId) { - const workspace = result.data.find( + const workspace = result.data?.listWorkspaces.find( (workspace) => workspace.id === workspaceId ); @@ -118,11 +114,12 @@ export default function SemanticSearch() { } } - setWorkspaces(result.data); - setWorkspacesLoadingStatus("finished"); - } else { - setGlobalError(Utils.getErrorMessage(result)); + setWorkspaces(result.data?.listWorkspaces!); + } catch (error: any) { + console.error(error); + setGlobalError(error.errors?.map((x: any) => x.error).join(",")); } + setWorkspacesLoadingStatus("finished"); })(); }, [appContext, onChange, searchParams]); @@ -137,29 +134,31 @@ export default function SemanticSearch() { tabs.push({ label: "Results", id: "results", - content: , + content: ( + + ), }); - if (searchResult.vectorSearchItems?.length > 0) { + if (searchResult!.vectorSearchItems!.length > 0) { tabs.push({ label: "Vector Search", id: "vector-search", content: ( ), }); } - if (searchResult.keywordSearchItems?.length > 0) { + if (searchResult.keywordSearchItems!.length > 0) { tabs.push({ label: "Keyword Search", id: "keyword-search", content: ( ), @@ -264,7 +263,7 @@ export default function SemanticSearch() { - {searchResult && ( + {searchResult && searchResult.items && ( <> {searchResult.items.length === 0 && ( diff --git a/lib/user-interface/react-app/src/pages/rag/workspace/aurora-workspace-settings.tsx b/lib/user-interface/react-app/src/pages/rag/workspace/aurora-workspace-settings.tsx index 88447937a..60b21212d 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspace/aurora-workspace-settings.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspace/aurora-workspace-settings.tsx @@ -7,10 +7,10 @@ import { Header, } from "@cloudscape-design/components"; import { Labels } from "../../../common/constants"; -import { WorkspaceItem } from "../../../common/types"; +import { Workspace } from "../../../API"; export interface AuroraWorkspaceSettingsProps { - workspace: WorkspaceItem; + workspace: Workspace; } export default function AuroraWorkspaceSettings( @@ -36,7 +36,7 @@ export default function AuroraWorkspaceSettings( Languages
{(props.workspace.languages ?? []) - .map((c) => Labels.languageMap.get(c)) + .map((c) => Labels.languageMap.get(c!)) .join(", ")}
@@ -44,9 +44,9 @@ export default function AuroraWorkspaceSettings( Status
- {Labels.statusMap[props.workspace.status]} + {Labels.statusMap[props.workspace.status!]}
@@ -78,7 +78,7 @@ export default function AuroraWorkspaceSettings( Metric (scoring function)
{ - Labels.distainceFunctionScoreMapAurora[ + Labels.distanceFunctionScoreMapAurora[ props.workspace.metric ?? "" ] } diff --git a/lib/user-interface/react-app/src/pages/rag/workspace/columns.tsx b/lib/user-interface/react-app/src/pages/rag/workspace/columns.tsx index d136f1d46..d105d4d0e 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspace/columns.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspace/columns.tsx @@ -1,29 +1,30 @@ import { Link, StatusIndicator } from "@cloudscape-design/components"; -import { DocumentItem, RagDocumentType } from "../../../common/types"; +import { RagDocumentType } from "../../../common/types"; import { Labels } from "../../../common/constants"; import { DateTime } from "luxon"; import { Utils } from "../../../common/utils"; +import { Document } from "../../../API"; const FILES_COLUMN_DEFINITIONS = [ { id: "name", header: "Name", - cell: (item: DocumentItem) => item.path, + cell: (item: Document) => item.path, isRowHeader: true, }, { id: "status", header: "Status", - cell: (item: DocumentItem) => ( - - {Labels.statusMap[item.status]} + cell: (item: Document) => ( + + {Labels.statusMap[item.status!]} ), }, { id: "createdAt", header: "Upload date", - cell: (item: DocumentItem) => + cell: (item: Document) => DateTime.fromISO(new Date(item.createdAt).toISOString()).toLocaleString( DateTime.DATETIME_SHORT ), @@ -31,7 +32,7 @@ const FILES_COLUMN_DEFINITIONS = [ { id: "size", header: "Size", - cell: (item: DocumentItem) => Utils.bytesToSize(item.sizeInBytes), + cell: (item: Document) => Utils.bytesToSize(item.sizeInBytes!), }, ]; @@ -39,24 +40,22 @@ const TEXTS_COLUMN_DEFINITIONS = [ { id: "title", header: "Title", - cell: (item: DocumentItem) => ( - <>{Utils.textEllipsis(item.title ?? "", 100)} - ), + cell: (item: Document) => <>{Utils.textEllipsis(item.title ?? "", 100)}, isRowHeader: true, }, { id: "status", header: "Status", - cell: (item: DocumentItem) => ( - - {Labels.statusMap[item.status]} + cell: (item: Document) => ( + + {Labels.statusMap[item.status!]} ), }, { id: "createdAt", header: "Upload date", - cell: (item: DocumentItem) => + cell: (item: Document) => DateTime.fromISO(new Date(item.createdAt).toISOString()).toLocaleString( DateTime.DATETIME_SHORT ), @@ -67,7 +66,7 @@ const RSS_COLUMN_DEFINITIONS = [ { id: "title", header: "RSS Feed Title", - cell: (item: DocumentItem) => ( + cell: (item: Document) => ( {Utils.textEllipsis(item.title ?? "", 100)} @@ -77,17 +76,15 @@ const RSS_COLUMN_DEFINITIONS = [ { id: "path", header: "RSS Feed URL", - cell: (item: DocumentItem) => ( - <>{Utils.textEllipsis(item.path ?? "", 100)} - ), + cell: (item: Document) => <>{Utils.textEllipsis(item.path ?? "", 100)}, isRowHeader: true, }, { id: "status", header: "RSS Subscription Status", - cell: (item: DocumentItem) => ( - - {Labels.statusMap[item.status]} + cell: (item: Document) => ( + + {Labels.statusMap[item.status!]} ), }, @@ -97,17 +94,15 @@ const QNA_COLUMN_DEFINITIONS = [ { id: "title", header: "Question", - cell: (item: DocumentItem) => ( - <>{Utils.textEllipsis(item.title ?? "", 100)} - ), + cell: (item: Document) => <>{Utils.textEllipsis(item.title ?? "", 100)}, isRowHeader: true, }, { id: "status", header: "Status", - cell: (item: DocumentItem) => ( - - {Labels.statusMap[item.status]} + cell: (item: Document) => ( + + {Labels.statusMap[item.status!]} ), isRowHeader: true, @@ -115,7 +110,7 @@ const QNA_COLUMN_DEFINITIONS = [ { id: "createdAt", header: "Upload date", - cell: (item: DocumentItem) => + cell: (item: Document) => DateTime.fromISO(new Date(item.createdAt).toISOString()).toLocaleString( DateTime.DATETIME_SHORT ), @@ -127,23 +122,25 @@ const WEBSITES_COLUMN_DEFINITIONS = [ { id: "name", header: "Name", - cell: (item: DocumentItem) => - item.path.length > 100 ? item.path.substring(0, 100) + "..." : item.path, + cell: (item: Document) => + item.path!.length > 100 + ? item.path!.substring(0, 100) + "..." + : item.path, isRowHeader: true, }, { id: "status", header: "Status", - cell: (item: DocumentItem) => ( - - {Labels.statusMap[item.status]} + cell: (item: Document) => ( + + {Labels.statusMap[item.status!]} ), }, { id: "subType", header: "Type", - cell: (item: DocumentItem) => ( + cell: (item: Document) => ( <>{item.subType == "sitemap" ? "sitemap" : "website"} ), isRowHeader: true, @@ -151,13 +148,13 @@ const WEBSITES_COLUMN_DEFINITIONS = [ { id: "subDocuments", header: "Pages", - cell: (item: DocumentItem) => item.subDocuments, + cell: (item: Document) => item.subDocuments, isRowHeader: true, }, { id: "createdAt", header: "Upload date", - cell: (item: DocumentItem) => + cell: (item: Document) => DateTime.fromISO(new Date(item.createdAt).toISOString()).toLocaleString( DateTime.DATETIME_SHORT ), diff --git a/lib/user-interface/react-app/src/pages/rag/workspace/documents-tab.tsx b/lib/user-interface/react-app/src/pages/rag/workspace/documents-tab.tsx index fc6647eeb..eae09c9cc 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspace/documents-tab.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspace/documents-tab.tsx @@ -6,16 +6,14 @@ import { Pagination, } from "@cloudscape-design/components"; import { useCallback, useContext, useEffect, useState } from "react"; -import { - DocumentResult, - RagDocumentType, - ResultValue, -} from "../../../common/types"; +import { RagDocumentType } from "../../../common/types"; import RouterButton from "../../../components/wrappers/router-button"; import { TableEmptyState } from "../../../components/table-empty-state"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; import { getColumnDefinition } from "./columns"; +import { Utils } from "../../../common/utils"; +import { DocumentsResult } from "../../../API"; export interface DocumentsTabProps { workspaceId?: string; @@ -26,7 +24,7 @@ export default function DocumentsTab(props: DocumentsTabProps) { const appContext = useContext(AppContext); const [loading, setLoading] = useState(true); const [currentPageIndex, setCurrentPageIndex] = useState(1); - const [pages, setPages] = useState([]); + const [pages, setPages] = useState<(DocumentsResult | undefined)[]>([]); const getDocuments = useCallback( async (params: { lastDocumentId?: string; pageIndex?: number }) => { @@ -36,30 +34,33 @@ export default function DocumentsTab(props: DocumentsTabProps) { setLoading(true); const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.getDocuments( - props.workspaceId, - props.documentType, - params?.lastDocumentId - ); + try { + const result = await apiClient.documents.getDocuments( + props.workspaceId, + props.documentType, + params?.lastDocumentId + ); - if (ResultValue.ok(result)) { setPages((current) => { const foundIndex = current.findIndex( - (c) => c.lastDocumentId === result.data.lastDocumentId + (c) => + c!.lastDocumentId === result.data!.listDocuments.lastDocumentId ); if (foundIndex !== -1) { - current[foundIndex] = result.data; + current[foundIndex] = result.data?.listDocuments; return [...current]; } else if (typeof params.pageIndex !== "undefined") { - current[params.pageIndex - 1] = result.data; + current[params.pageIndex - 1] = result.data?.listDocuments; return [...current]; - } else if (result.data.items.length === 0) { + } else if (result.data?.listDocuments.items.length === 0) { return current; } else { - return [...current, result.data]; + return [...current, result.data?.listDocuments]; } }); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } setLoading(false); @@ -93,7 +94,7 @@ export default function DocumentsTab(props: DocumentsTabProps) { if (currentPageIndex <= 1) { await getDocuments({ pageIndex: currentPageIndex }); } else { - const lastDocumentId = pages[currentPageIndex - 2]?.lastDocumentId; + const lastDocumentId = pages[currentPageIndex - 2]?.lastDocumentId!; await getDocuments({ lastDocumentId }); } }; @@ -109,7 +110,7 @@ export default function DocumentsTab(props: DocumentsTabProps) { loading={loading} loadingText={`Loading ${typeStr}s`} columnDefinitions={columnDefinitions} - items={pages[Math.min(pages.length - 1, currentPageIndex - 1)]?.items} + items={pages[Math.min(pages.length - 1, currentPageIndex - 1)]?.items!} header={
{ - const result = await apiClient.kendra.kendraIsSyncing(props.workspace.id); + try { + const result = await apiClient.kendra.kendraIsSyncing( + props.workspace.id + ); - if (ResultValue.ok(result)) { - setKendraIndexSyncing(result.data === true); + setKendraIndexSyncing(result.data?.isKendraDataSynching === true); + } catch (error) { + console.error(error); } }; @@ -54,14 +57,13 @@ export default function KendraWorkspaceSettings( setGlobalError(""); const apiClient = new ApiClient(appContext); - const result = await apiClient.kendra.startKendraDataSync( - props.workspace.id - ); + try { + await apiClient.kendra.startKendraDataSync(props.workspace.id); - if (ResultValue.ok(result)) { setKendraIndexSyncing(true); - } else { - setGlobalError(Utils.getErrorMessage(result)); + } catch (error: any) { + console.error(error); + setGlobalError(error.errors.map((e: any) => e.message).join(",")); } setSendingRequest(false); @@ -107,9 +109,9 @@ export default function KendraWorkspaceSettings( Status
- {Labels.statusMap[props.workspace.status]} + {Labels.statusMap[props.workspace.status!]}
@@ -125,18 +127,14 @@ export default function KendraWorkspaceSettings(
External
- {props.workspace.kendraIndexExternal === true - ? "Yes" - : "No"} + {props.workspace.kendraIndexExternal ? "Yes" : "No"}
)} {typeof props.workspace.kendraUseAllData !== "undefined" && (
Use All Data -
- {props.workspace.kendraUseAllData === true ? "Yes" : "No"} -
+
{props.workspace.kendraUseAllData ? "Yes" : "No"}
)} diff --git a/lib/user-interface/react-app/src/pages/rag/workspace/open-search-workspace-settings.tsx b/lib/user-interface/react-app/src/pages/rag/workspace/open-search-workspace-settings.tsx index 1c0af8bb8..ac0102b2c 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspace/open-search-workspace-settings.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspace/open-search-workspace-settings.tsx @@ -7,10 +7,10 @@ import { Header, } from "@cloudscape-design/components"; import { Labels } from "../../../common/constants"; -import { WorkspaceItem } from "../../../common/types"; +import { Workspace } from "../../../API"; export interface OpenSearchWorkspaceSettingsProps { - workspace: WorkspaceItem; + workspace: Workspace; } export default function OpenSearchWorkspaceSettings( @@ -38,7 +38,7 @@ export default function OpenSearchWorkspaceSettings( Languages
{(props.workspace.languages ?? []) - .map((c) => Labels.languageMap.get(c)) + .map((c) => Labels.languageMap.get(c!)) .join(", ")}
@@ -46,9 +46,9 @@ export default function OpenSearchWorkspaceSettings( Status
- {Labels.statusMap[props.workspace.status]} + {Labels.statusMap[props.workspace.status!]}
@@ -80,7 +80,7 @@ export default function OpenSearchWorkspaceSettings( Metric (scoring function)
{ - Labels.distainceFunctionScoreMapOpenSearch[ + Labels.distanceFunctionScoreMapOpenSearch[ props.workspace.metric ?? "" ] } diff --git a/lib/user-interface/react-app/src/pages/rag/workspace/rss-feed.tsx b/lib/user-interface/react-app/src/pages/rag/workspace/rss-feed.tsx index 22b36a40f..1504f2be6 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspace/rss-feed.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspace/rss-feed.tsx @@ -22,13 +22,7 @@ import useOnFollow from "../../../common/hooks/use-on-follow"; import BaseAppLayout from "../../../components/base-app-layout"; import { useNavigate, useParams } from "react-router-dom"; import { useCallback, useContext, useEffect, useState } from "react"; -import { - DocumentItem, - DocumentResult, - DocumentSubscriptionStatus, - ResultValue, - WorkspaceItem, -} from "../../../common/types"; +import { DocumentSubscriptionStatus } from "../../../common/types"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; import { CHATBOT_NAME, Labels } from "../../../common/constants"; @@ -36,6 +30,7 @@ import { TableEmptyState } from "../../../components/table-empty-state"; import { DateTime } from "luxon"; import { Utils } from "../../../common/utils"; import { useForm } from "../../../common/hooks/use-form"; +import { Workspace, Document, DocumentsResult } from "../../../API"; export default function RssFeed() { const appContext = useContext(AppContext); @@ -43,16 +38,16 @@ export default function RssFeed() { const onFollow = useOnFollow(); const { workspaceId, feedId } = useParams(); const [loading, setLoading] = useState(true); - const [rssSubscription, setRssSubscription] = useState( - null - ); + const [rssSubscription, setRssSubscription] = useState(null); const [rssSubscriptionStatus, setRssSubscriptionStatus] = useState(DocumentSubscriptionStatus.DEFAULT); const [rssCrawlerFollowLinks, setRssCrawlerFollowLinks] = useState(false); const [rssCrawlerLimit, setRssCrawlerLimit] = useState(0); const [currentPageIndex, setCurrentPageIndex] = useState(1); - const [pages, setPages] = useState([]); - const [workspace, setWorkspace] = useState(null); + const [pages, setPages] = useState<(DocumentsResult | undefined | null)[]>( + [] + ); + const [workspace, setWorkspace] = useState(null); const [isEditingCrawlerSettings, setIsEditingCrawlerSettings] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -62,15 +57,17 @@ export default function RssFeed() { if (!appContext || !workspaceId) return; const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.getWorkspace(workspaceId); + try { + const result = await apiClient.workspaces.getWorkspace(workspaceId); - if (ResultValue.ok(result)) { - if (!result.data) { + if (!result.data?.getWorkspace) { navigate("/rag/workspaces"); return; } - setWorkspace(result.data); + setWorkspace(result.data?.getWorkspace); + } catch (e) { + console.error(e); } }, [appContext, navigate, workspaceId]); @@ -79,32 +76,35 @@ export default function RssFeed() { if (!appContext || !workspaceId || !feedId) return; setPostsLoading(true); const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.getRssSubscriptionPosts( - workspaceId, - feedId, - params.lastDocumentId - ); - if (ResultValue.ok(result)) { + try { + const result = await apiClient.documents.getRssSubscriptionPosts( + workspaceId, + feedId, + params.lastDocumentId + ); + setPages((current) => { const foundIndex = current.findIndex( - (c) => c.lastDocumentId === result.data.lastDocumentId + (c) => + c!.lastDocumentId === result.data?.getRSSPosts?.lastDocumentId ); setPostsLoading(false); if (foundIndex !== -1) { - current[foundIndex] = result.data; + current[foundIndex] = result.data?.getRSSPosts; return [...current]; } else if (typeof params.pageIndex !== "undefined") { - current[params.pageIndex - 1] = result.data; + current[params.pageIndex - 1] = result.data?.getRSSPosts; return [...current]; - } else if (result.data.items.length === 0) { + } else if (result.data?.getRSSPosts!.items.length === 0) { return current; } else { - return [...current, result.data]; + return [...current, result.data?.getRSSPosts]; } }); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } - setLoading(false); }, [appContext, workspaceId, feedId] @@ -113,29 +113,23 @@ export default function RssFeed() { const getRssSubscriptionDetails = useCallback(async () => { if (!appContext || !workspaceId || !feedId) return; const apiClient = new ApiClient(appContext); - const rssSubscriptionResult = await apiClient.documents.getDocumentDetails( - workspaceId, - feedId - ); - if ( - ResultValue.ok(rssSubscriptionResult) && - rssSubscriptionResult.data.items - ) { - setRssSubscription(rssSubscriptionResult.data.items[0]); - setRssSubscriptionStatus( - rssSubscriptionResult.data.items[0].status == "enabled" - ? DocumentSubscriptionStatus.ENABLED - : DocumentSubscriptionStatus.DISABLED - ); - setRssCrawlerFollowLinks( - rssSubscriptionResult.data.items[0].crawlerProperties?.followLinks ?? - true - ); - setRssCrawlerLimit( - rssSubscriptionResult.data.items[0].crawlerProperties?.limit ?? 0 - ); + try { + const rssSubscriptionResult = + await apiClient.documents.getDocumentDetails(workspaceId, feedId); + if (rssSubscriptionResult.data?.getDocument) { + const doc = rssSubscriptionResult.data.getDocument!; + setRssSubscription(doc); + setRssSubscriptionStatus( + doc.status == "enabled" + ? DocumentSubscriptionStatus.ENABLED + : DocumentSubscriptionStatus.DISABLED + ); + setRssCrawlerFollowLinks(doc.crawlerProperties?.followLinks ?? true); + setRssCrawlerLimit(doc.crawlerProperties?.limit ?? 0); + } + } catch (error) { + console.error(Utils.getErrorMessage(error)); } - setLoading(false); }, [appContext, workspaceId, feedId]); @@ -144,30 +138,35 @@ export default function RssFeed() { if (!appContext || !workspaceId || !feedId) return; if (toState.toLowerCase() == "disable") { const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.disableRssSubscription( - workspaceId, - feedId - ); - setIsEditingCrawlerSettings(false); - if (ResultValue.ok(result)) { + try { + const result = await apiClient.documents.disableRssSubscription( + workspaceId, + feedId + ); + setIsEditingCrawlerSettings(false); + setRssSubscriptionStatus( - result.data.status == "enabled" + result.data?.setDocumentSubscriptionStatus!.status! == "enabled" ? DocumentSubscriptionStatus.ENABLED : DocumentSubscriptionStatus.DISABLED ); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } } else if (toState.toLowerCase() == "enable") { const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.enableRssSubscription( - workspaceId, - feedId - ); - if (ResultValue.ok(result)) { + try { + const result = await apiClient.documents.enableRssSubscription( + workspaceId, + feedId + ); setRssSubscriptionStatus( - result.data.status == "enabled" + result.data?.setDocumentSubscriptionStatus!.status! == "enabled" ? DocumentSubscriptionStatus.ENABLED : DocumentSubscriptionStatus.DISABLED ); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } } }, @@ -217,7 +216,7 @@ export default function RssFeed() { if (currentPageIndex <= 1) { await getRssSubscriptionPosts({ pageIndex: currentPageIndex }); } else { - const lastDocumentId = pages[currentPageIndex - 2]?.lastDocumentId; + const lastDocumentId = pages[currentPageIndex - 2]?.lastDocumentId!; await getRssSubscriptionPosts({ lastDocumentId }); } }; @@ -448,7 +447,7 @@ export default function RssFeed() { { id: "title", header: "Title", - cell: (item: DocumentItem) => ( + cell: (item: Document) => ( <>{Utils.textEllipsis(item.title ?? "", 100)} ), isRowHeader: true, @@ -456,7 +455,7 @@ export default function RssFeed() { { id: "url", header: "URL", - cell: (item: DocumentItem) => ( + cell: (item: Document) => ( ), isRowHeader: true, @@ -464,16 +463,16 @@ export default function RssFeed() { { id: "status", header: "Status", - cell: (item: DocumentItem) => ( - - {Labels.statusMap[item.status]} + cell: (item: Document) => ( + + {Labels.statusMap[item.status!]} ), }, { id: "createdAt", header: "RSS Post Detected", - cell: (item: DocumentItem) => + cell: (item: Document) => item.createdAt ? DateTime.fromISO( new Date(item.createdAt).toISOString() @@ -482,7 +481,7 @@ export default function RssFeed() { }, ]} items={ - pages[Math.min(pages.length - 1, currentPageIndex - 1)]?.items + pages[Math.min(pages.length - 1, currentPageIndex - 1)]?.items! } empty={ - @@ -627,14 +626,17 @@ export function RssFeedCrawlerForm(props: RssFeedEditorProps) { if (!validationResult) return; props.setSubmitting(true); const apiClient = new ApiClient(appContext); - const result = await apiClient.documents.updateRssSubscriptionCrawler( - props.workspaceId, - props.documentId, - data.followLinks, - data.limit - ); - if (ResultValue.ok(result)) { + try { + await apiClient.documents.updateRssSubscriptionCrawler( + props.workspaceId, + props.documentId, + data.followLinks, + data.limit + ); + props.setSubmitting(false); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } }; return ( diff --git a/lib/user-interface/react-app/src/pages/rag/workspace/workspace.tsx b/lib/user-interface/react-app/src/pages/rag/workspace/workspace.tsx index 845e29272..6a981c183 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspace/workspace.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspace/workspace.tsx @@ -11,7 +11,6 @@ import useOnFollow from "../../../common/hooks/use-on-follow"; import BaseAppLayout from "../../../components/base-app-layout"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useCallback, useContext, useEffect, useState } from "react"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { AppContext } from "../../../common/app-context"; import { ApiClient } from "../../../common/api-client/api-client"; import { Utils } from "../../../common/utils"; @@ -22,8 +21,9 @@ import DocumentsTab from "./documents-tab"; import OpenSearchWorkspaceSettings from "./open-search-workspace-settings"; import KendraWorkspaceSettings from "./kendra-workspace-settings"; import { CHATBOT_NAME } from "../../../common/constants"; +import { Workspace } from "../../../API"; -export default function Workspace() { +export default function WorkspacePane() { const appContext = useContext(AppContext); const navigate = useNavigate(); const onFollow = useOnFollow(); @@ -31,23 +31,25 @@ export default function Workspace() { const [searchParams, setSearchParams] = useSearchParams(); const [activeTab, setActiveTab] = useState(searchParams.get("tab") || "file"); const [loading, setLoading] = useState(true); - const [workspace, setWorkspace] = useState(null); + const [workspace, setWorkspace] = useState( + null + ); const getWorkspace = useCallback(async () => { if (!appContext || !workspaceId) return; const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.getWorkspace(workspaceId); - - if (ResultValue.ok(result)) { - if (!result.data) { + try { + const result = await apiClient.workspaces.getWorkspace(workspaceId); + if (!result.data?.getWorkspace) { navigate("/rag/workspaces"); return; } - - setWorkspace(result.data); - setLoading(false); + setWorkspace(result.data!.getWorkspace); + } catch (error) { + console.error(error); } + setLoading(false); }, [appContext, navigate, workspaceId]); useEffect(() => { diff --git a/lib/user-interface/react-app/src/pages/rag/workspaces/column-definitions.tsx b/lib/user-interface/react-app/src/pages/rag/workspaces/column-definitions.tsx index b29746186..031f535f0 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspaces/column-definitions.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspaces/column-definitions.tsx @@ -4,17 +4,17 @@ import { PropertyFilterOperator, } from "@cloudscape-design/collection-hooks"; import RouterLink from "../../../components/wrappers/router-link"; -import { WorkspaceItem } from "../../../common/types"; import { Labels } from "../../../common/constants"; import { DateTime } from "luxon"; +import { Workspace } from "../../../API"; -export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition[] = +export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition[] = [ { id: "name", header: "Name", sortingField: "name", - cell: (item: WorkspaceItem) => ( + cell: (item: Workspace) => ( {item.name} ), isRowHeader: true, @@ -23,15 +23,15 @@ export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition Labels.engineMap[item.engine], + cell: (item: Workspace) => Labels.engineMap[item.engine], }, { - id: "starus", + id: "status", header: "Status", sortingField: "status", cell: (item) => ( - - {Labels.statusMap[item.status]} + + {Labels.statusMap[item.status!]} ), minWidth: 120, @@ -40,13 +40,13 @@ export const WorkspacesColumnDefinitions: TableProps.ColumnDefinition item.documents, + cell: (item: Workspace) => item.documents, }, { id: "timestamp", header: "Creation Date", sortingField: "timestamp", - cell: (item: WorkspaceItem) => + cell: (item: Workspace) => DateTime.fromISO(new Date(item.createdAt).toISOString()).toLocaleString( DateTime.DATETIME_SHORT ), diff --git a/lib/user-interface/react-app/src/pages/rag/workspaces/workspaces-page-header.tsx b/lib/user-interface/react-app/src/pages/rag/workspaces/workspaces-page-header.tsx index 996ab3cdc..8cf0c5239 100644 --- a/lib/user-interface/react-app/src/pages/rag/workspaces/workspaces-page-header.tsx +++ b/lib/user-interface/react-app/src/pages/rag/workspaces/workspaces-page-header.tsx @@ -5,18 +5,19 @@ import { SpaceBetween, } from "@cloudscape-design/components"; import RouterButton from "../../../components/wrappers/router-button"; -import { ResultValue, WorkspaceItem } from "../../../common/types"; import { useNavigate } from "react-router-dom"; import { useContext, useState } from "react"; import WorkspaceDeleteModal from "../../../components/rag/workspace-delete-modal"; import { ApiClient } from "../../../common/api-client/api-client"; import { AppContext } from "../../../common/app-context"; +import { Workspace } from "../../../API"; +import { Utils } from "../../../common/utils"; interface WorkspacesPageHeaderProps extends HeaderProps { title?: string; createButtonText?: string; getWorkspaces: () => Promise; - selectedWorkspaces: readonly WorkspaceItem[]; + selectedWorkspaces: readonly Workspace[]; } export function WorkspacesPageHeader({ @@ -33,7 +34,11 @@ export function WorkspacesPageHeader({ props.selectedWorkspaces[0].status == "error"); const onRefreshClick = async () => { - await props.getWorkspaces(); + try { + await props.getWorkspaces(); + } catch (error) { + console.error(Utils.getErrorMessage(error)); + } }; const onViewDetailsClick = () => { @@ -46,20 +51,22 @@ export function WorkspacesPageHeader({ setShowDeleteModal(true); }; - const onDeleteWorksapce = async () => { + const onDeleteWorkspace = async () => { if (!appContext) return; if (!isOnlyOneSelected) return; setShowDeleteModal(false); const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.deleteWorkspace( - props.selectedWorkspaces[0].id - ); + try { + await apiClient.workspaces.deleteWorkspace( + props.selectedWorkspaces[0].id + ); - if (ResultValue.ok(result)) { setTimeout(async () => { await props.getWorkspaces(); - }, 2500); + }, 1500); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } }; @@ -68,7 +75,7 @@ export function WorkspacesPageHeader({ setShowDeleteModal(false)} - onDelete={onDeleteWorksapce} + onDelete={onDeleteWorkspace} workspace={props.selectedWorkspaces[0]} />
([]); + const [workspaces, setWorkspaces] = useState([]); const [loading, setLoading] = useState(true); const { items, @@ -60,9 +61,12 @@ export default function WorkspacesTable() { if (!appContext) return; const apiClient = new ApiClient(appContext); - const result = await apiClient.workspaces.getWorkspaces(); - if (ResultValue.ok(result)) { - setWorkspaces(result.data); + try { + const result = await apiClient.workspaces.getWorkspaces(); + + setWorkspaces(result.data!.listWorkspaces); + } catch (error) { + console.error(Utils.getErrorMessage(error)); } setLoading(false); diff --git a/package-lock.json b/package-lock.json index 6c82322ba..7c543c270 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@typescript-eslint/parser": "^6.0.0", "aws-cdk": "2.114.1", "eslint": "^8.45.0", + "graphql": "^16.8.1", "prettier": "^3.0.3", "ts-node": "^10.9.1", "typescript": "~5.1.3", @@ -3397,6 +3398,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/package.json b/package.json index 9eb4a36e3..a12039739 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@typescript-eslint/parser": "^6.0.0", "aws-cdk": "2.114.1", "eslint": "^8.45.0", + "graphql": "^16.8.1", "prettier": "^3.0.3", "ts-node": "^10.9.1", "typescript": "~5.1.3", diff --git a/tsconfig.json b/tsconfig.json index bf3847b27..529d3d124 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,9 @@ "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"] }, - "exclude": ["node_modules", "cdk.out", "lib/user-interface/react-app"] + "exclude": [ + "node_modules", + "cdk.out", + "lib/user-interface/react-app", + ] }