From 1364065f9c30c8bd8b7a07b4418afbea5251049a Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 8 Nov 2023 17:56:58 +0800 Subject: [PATCH 01/31] feat: add workflows Signed-off-by: SuZhou-Joe --- .github/workflows/backport.yml | 43 ++++++++ .github/workflows/changelog_verifier.yml | 19 ++++ .github/workflows/delete_backport_branch.yml | 15 +++ .github/workflows/links_checker.yml | 31 ++++++ .github/workflows/unit_test_workflow.yml | 104 +++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 .github/workflows/backport.yml create mode 100644 .github/workflows/changelog_verifier.yml create mode 100644 .github/workflows/delete_backport_branch.yml create mode 100644 .github/workflows/links_checker.yml create mode 100644 .github/workflows/unit_test_workflow.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 00000000..c8dfef41 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,43 @@ +name: Backport +on: + pull_request_target: + types: + - closed + - labeled + +jobs: + backport: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + name: Backport + # Only react to merged PRs for security reasons. + # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. + if: > + github.event.pull_request.merged + && ( + github.event.action == 'closed' + || ( + github.event.action == 'labeled' + && contains(github.event.label.name, 'backport') + ) + ) + steps: + - name: GitHub App token + id: github_app_token + uses: tibdex/github-app-token@v1.5.0 + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + # opensearch-trigger-bot installation ID + installation_id: 22958780 + + - name: Backport + uses: VachaShah/backport@v2.2.0 + with: + github_token: ${{ steps.github_app_token.outputs.token }} + head_template: backport/backport-<%= number %>-to-<%= base %> + files_to_skip: "CHANGELOG.md" + labels_template: "<%= JSON.stringify([...labels, 'autocut']) %>" + failure_labels: "failed backport" diff --git a/.github/workflows/changelog_verifier.yml b/.github/workflows/changelog_verifier.yml new file mode 100644 index 00000000..0890ea8b --- /dev/null +++ b/.github/workflows/changelog_verifier.yml @@ -0,0 +1,19 @@ +name: "Changelog Verifier" +on: + pull_request: + branches: [ '**', '!feature/**' ] + types: [opened, edited, review_requested, synchronize, reopened, ready_for_review, labeled, unlabeled] + +jobs: + # Enforces the update of a changelog file on every pull request + verify-changelog: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.event.pull_request.head.sha }} + + - uses: dangoslen/changelog-enforcer@v3 + with: + skipLabels: "autocut, Skip-Changelog" diff --git a/.github/workflows/delete_backport_branch.yml b/.github/workflows/delete_backport_branch.yml new file mode 100644 index 00000000..387a124b --- /dev/null +++ b/.github/workflows/delete_backport_branch.yml @@ -0,0 +1,15 @@ +name: Delete merged branch of the backport PRs +on: + pull_request: + types: + - closed + +jobs: + delete-branch: + runs-on: ubuntu-latest + if: startsWith(github.event.pull_request.head.ref,'backport/') + steps: + - name: Delete merged branch + uses: SvanBoxel/delete-merged-branch@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/links_checker.yml b/.github/workflows/links_checker.yml new file mode 100644 index 00000000..c02921d9 --- /dev/null +++ b/.github/workflows/links_checker.yml @@ -0,0 +1,31 @@ +# Copyright OpenSearch Contributors +# SPDX-License-Identifier: Apache-2.0 + +name: Link Checker + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + linkchecker: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Load Excludes + run: | + LYCHEE_EXCLUDE=$(sed -e :a -e 'N;s/\n/ --exclude /;ta' .lycheeexclude) + echo "LYCHEE_EXCLUDE=$LYCHEE_EXCLUDE" >> $GITHUB_ENV + - name: Lychee Link Checker + id: lychee + uses: lycheeverse/lychee-action@v1.0.9 + with: + args: --accept=200,403,429 --exclude ${{ env.LYCHEE_EXCLUDE }} --exclude-mail "**/*.html" "**/*.md" "**/*.txt" "**/*.json" "**/*.js" "**/*.ts" "**/*.tsx" + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Fail if there were link errors + run: exit ${{ steps.lychee.outputs.exit_code }} \ No newline at end of file diff --git a/.github/workflows/unit_test_workflow.yml b/.github/workflows/unit_test_workflow.yml new file mode 100644 index 00000000..4ff6319f --- /dev/null +++ b/.github/workflows/unit_test_workflow.yml @@ -0,0 +1,104 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Build and test + +# trigger on every commit push and PR for all branches except pushes for backport branches +on: + push: + branches: ['*'] + paths-ignore: + - '**/*.md' + - 'docs/**' + pull_request: + branches: ['*'] + paths-ignore: + - '**/*.md' + - 'docs/**' +env: + OPENSEARCH_DASHBOARDS_VERSION: '2.x' + NODE_OPTIONS: "--max-old-space-size=6144 --dns-result-order=ipv4first" + +jobs: + Get-CI-Image-Tag: + uses: opensearch-project/opensearch-build/.github/workflows/get-ci-image-tag.yml@main + with: + product: opensearch-dashboards + + tests-linux: + needs: Get-CI-Image-Tag + name: Run unit tests + runs-on: ubuntu-latest + container: + # using the same image which is used by opensearch-build team to build the OpenSearch Distribution + # this image tag is subject to change as more dependencies and updates will arrive over time + image: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-version-linux }} + # need to switch to root so that github actions can install runner binary on container without permission issues. + options: --user root + + steps: + # Enable longer filenames for windows + - name: Checkout OpenSearch-Dashboards + uses: actions/checkout@v2 + with: + repository: opensearch-project/OpenSearch-Dashboards + ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} + path: OpenSearch-Dashboards + - name: Checkout dashboards-assistant plugin + uses: actions/checkout@v2 + with: + path: OpenSearch-Dashboards/plugins/dashboards-assistant + - name: Bootstrap / Run tests + run: | + chown -R 1000:1000 `pwd` + cd ./OpenSearch-Dashboards/ + su `id -un 1000` -c "source $NVM_DIR/nvm.sh && nvm use && node -v && yarn -v && + cd ./plugins/dashboards-assistant && + whoami && yarn osd bootstrap && yarn run test:jest --coverage" + + - name: Uploads coverage + uses: codecov/codecov-action@v1 + + tests-windows-macos: + name: Run unit tests + strategy: + matrix: + os: [macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + # Enable longer filenames for windows + - name: Enable longer filenames + if: ${{ matrix.os == 'windows-latest' }} + run: git config --system core.longpaths true + - name: Checkout OpenSearch-Dashboards + uses: actions/checkout@v2 + with: + repository: opensearch-project/OpenSearch-Dashboards + ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} + path: OpenSearch-Dashboards + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version-file: './OpenSearch-Dashboards/.nvmrc' + registry-url: 'https://registry.npmjs.org' + - name: Install Yarn + # Need to use bash to avoid having a windows/linux specific step + shell: bash + run: | + YARN_VERSION=$(node -p "require('./OpenSearch-Dashboards/package.json').engines.yarn") + echo "Installing yarn@$YARN_VERSION" + npm i -g yarn@$YARN_VERSION + - run: node -v + - run: yarn -v + - name: Checkout dashboards-assistant plugin + uses: actions/checkout@v2 + with: + path: OpenSearch-Dashboards/plugins/dashboards-assistant + - name: Bootstrap plugin/dashboards-assistant + run: | + cd OpenSearch-Dashboards/plugins/dashboards-assistant + yarn osd bootstrap + - name: Run tests + run: | + cd OpenSearch-Dashboards/plugins/dashboards-assistant + yarn run test:jest --coverage From d616e0971b4663cfd4f905d79096c014fbc5a46a Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 8 Nov 2023 17:58:52 +0800 Subject: [PATCH 02/31] feat: remove useless workflow Signed-off-by: SuZhou-Joe --- .github/workflows/delete_backport_branch.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .github/workflows/delete_backport_branch.yml diff --git a/.github/workflows/delete_backport_branch.yml b/.github/workflows/delete_backport_branch.yml deleted file mode 100644 index 387a124b..00000000 --- a/.github/workflows/delete_backport_branch.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Delete merged branch of the backport PRs -on: - pull_request: - types: - - closed - -jobs: - delete-branch: - runs-on: ubuntu-latest - if: startsWith(github.event.pull_request.head.ref,'backport/') - steps: - - name: Delete merged branch - uses: SvanBoxel/delete-merged-branch@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 16ae642f35b2b1235a525a4174529b7d89de5fc1 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 9 Nov 2023 13:16:08 +0800 Subject: [PATCH 03/31] feat: fix unit test flow Signed-off-by: SuZhou-Joe --- .github/workflows/unit_test_workflow.yml | 14 +++++--------- package.json | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/unit_test_workflow.yml b/.github/workflows/unit_test_workflow.yml index 4ff6319f..752b7d1a 100644 --- a/.github/workflows/unit_test_workflow.yml +++ b/.github/workflows/unit_test_workflow.yml @@ -5,16 +5,12 @@ name: Build and test # trigger on every commit push and PR for all branches except pushes for backport branches on: - push: - branches: ['*'] - paths-ignore: - - '**/*.md' - - 'docs/**' pull_request: - branches: ['*'] - paths-ignore: - - '**/*.md' - - 'docs/**' + branches: + - "*" + push: + branches: + - "*" env: OPENSEARCH_DASHBOARDS_VERSION: '2.x' NODE_OPTIONS: "--max-old-space-size=6144 --dns-result-order=ipv4first" diff --git a/package.json b/package.json index 4681ec31..1aaf6fab 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "osd": "node ../../scripts/osd", "build": "yarn plugin-helpers build", - "test": "../../node_modules/.bin/jest --config ./test/jest.config.js", + "test:jest": "../../node_modules/.bin/jest --config ./test/jest.config.js", "plugin-helpers": "node ../../scripts/plugin_helpers", "prepare": "husky install", "lint:es": "node ../../scripts/eslint", From 6d26618f0af56ab66ff0a437fae66c8ed6eaaf4f Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 9 Nov 2023 17:49:16 +0800 Subject: [PATCH 04/31] feat: make workflow run Signed-off-by: SuZhou-Joe --- .github/workflows/unit_test_workflow.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit_test_workflow.yml b/.github/workflows/unit_test_workflow.yml index 752b7d1a..8c301874 100644 --- a/.github/workflows/unit_test_workflow.yml +++ b/.github/workflows/unit_test_workflow.yml @@ -6,11 +6,9 @@ name: Build and test # trigger on every commit push and PR for all branches except pushes for backport branches on: pull_request: - branches: - - "*" + branches: ["**"] push: - branches: - - "*" + branches: ["**"] env: OPENSEARCH_DASHBOARDS_VERSION: '2.x' NODE_OPTIONS: "--max-old-space-size=6144 --dns-result-order=ipv4first" From f12a316593d095344acd244b35ba70baf3da02a5 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 9 Nov 2023 17:51:34 +0800 Subject: [PATCH 05/31] feat: make workflows run Signed-off-by: SuZhou-Joe --- .github/workflows/changelog_verifier.yml | 2 +- .github/workflows/links_checker.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/changelog_verifier.yml b/.github/workflows/changelog_verifier.yml index 0890ea8b..2cf959c0 100644 --- a/.github/workflows/changelog_verifier.yml +++ b/.github/workflows/changelog_verifier.yml @@ -1,7 +1,7 @@ name: "Changelog Verifier" on: pull_request: - branches: [ '**', '!feature/**' ] + branches: [ '**' ] types: [opened, edited, review_requested, synchronize, reopened, ready_for_review, labeled, unlabeled] jobs: diff --git a/.github/workflows/links_checker.yml b/.github/workflows/links_checker.yml index c02921d9..5a5cf463 100644 --- a/.github/workflows/links_checker.yml +++ b/.github/workflows/links_checker.yml @@ -5,9 +5,9 @@ name: Link Checker on: push: - branches: [ main ] + branches: [ "**" ] pull_request: - branches: [ main ] + branches: [ "**" ] jobs: linkchecker: From af894362fc5e8a089a28b42eef0273a6360361c0 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 9 Nov 2023 17:54:52 +0800 Subject: [PATCH 06/31] feat: make workflows run Signed-off-by: SuZhou-Joe --- .github/workflows/links_checker.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/links_checker.yml b/.github/workflows/links_checker.yml index 5a5cf463..06cfbf33 100644 --- a/.github/workflows/links_checker.yml +++ b/.github/workflows/links_checker.yml @@ -16,9 +16,15 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Checkout OpenSearch-Dashboards + uses: actions/checkout@v2 + with: + repository: opensearch-project/OpenSearch-Dashboards + ref: main + path: OpenSearch-Dashboards - name: Load Excludes run: | - LYCHEE_EXCLUDE=$(sed -e :a -e 'N;s/\n/ --exclude /;ta' .lycheeexclude) + LYCHEE_EXCLUDE=$(sed -e :a -e 'N;s/\n/ --exclude /;ta' OpenSearch-Dashboards/.lycheeexclude) echo "LYCHEE_EXCLUDE=$LYCHEE_EXCLUDE" >> $GITHUB_ENV - name: Lychee Link Checker id: lychee From 1821b6cb666e632749fdd527954a8a204832981e Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 10 Nov 2023 16:13:09 +0800 Subject: [PATCH 07/31] feat: make windows flow pass Signed-off-by: SuZhou-Joe --- .babelrc | 18 ------------------ babel.config.js | 26 ++++++++++++++++++++++++++ test/jest.config.js | 6 +----- 3 files changed, 27 insertions(+), 23 deletions(-) delete mode 100644 .babelrc create mode 100644 babel.config.js diff --git a/.babelrc b/.babelrc deleted file mode 100644 index e21b3f2f..00000000 --- a/.babelrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets": { "node": "10" } - } - ], - "@babel/preset-react", - "@babel/preset-typescript" - ], - "plugins": [ - "@babel/plugin-transform-modules-commonjs", - ["@babel/plugin-transform-runtime", { "regenerator": true }], - "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-object-rest-spread" - ] -} \ No newline at end of file diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..a0f8a3ad --- /dev/null +++ b/babel.config.js @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// babelrc doesn't respect NODE_PATH anymore but using require does. +// Alternative to install them locally in node_modules +module.exports = function (api) { + // ensure env is test so that this config won't impact build or dev server + if (api.env('test')) { + return { + presets: [ + require('@babel/preset-env'), + require('@babel/preset-react'), + require('@babel/preset-typescript'), + ], + plugins: [ + [require('@babel/plugin-transform-runtime'), { regenerator: true }], + require('@babel/plugin-proposal-class-properties'), + require('@babel/plugin-proposal-object-rest-spread'), + [require('@babel/plugin-transform-modules-commonjs'), { allowTopLevelThis: true }], + ], + }; + } + return {}; +}; diff --git a/test/jest.config.js b/test/jest.config.js index 50752886..b2e97fb7 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -22,11 +22,7 @@ module.exports = { '/public/requests/', '/__utils__/', ], - transform: { - '^.+\\.tsx?$': ['ts-jest', { diagnostics: false }], - 'node_modules/(langchain|langsmith)/.+\\.js$': ['ts-jest', { diagnostics: false }], - }, - transformIgnorePatterns: ['/node_modules/(?!langchain|langsmith)'], + transformIgnorePatterns: ['node_modules/(?!langchain|langsmith)'], moduleNameMapper: { '\\.(css|less|sass|scss)$': '/test/__mocks__/styleMock.js', '\\.(gif|ttf|eot|svg|png)$': '/test/__mocks__/fileMock.js', From 50bfccd4568ac4812a202e11eccf453e0686bd81 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 10 Nov 2023 16:27:51 +0800 Subject: [PATCH 08/31] feat: change .babelrc to babel.config.js according to https://github.com/jestjs/jest/issues/6229\#issuecomment-403539460 Signed-off-by: SuZhou-Joe --- test/jest.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/jest.config.js b/test/jest.config.js index b2e97fb7..3a5ab955 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -22,6 +22,7 @@ module.exports = { '/public/requests/', '/__utils__/', ], + // https://github.com/jestjs/jest/issues/6229#issuecomment-403539460 transformIgnorePatterns: ['node_modules/(?!langchain|langsmith)'], moduleNameMapper: { '\\.(css|less|sass|scss)$': '/test/__mocks__/styleMock.js', From 267131c63b43cdd09d6f4eea95da3dd3fe39d55b Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 1 Nov 2023 17:39:50 +0800 Subject: [PATCH 09/31] feat: use agent framework API to generate answer Signed-off-by: SuZhou-Joe --- server/services/chat/olly_chat_service.ts | 29 ++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index 6c2f09c2..b77d50fc 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -5,6 +5,7 @@ import { Run } from 'langchain/callbacks'; import { v4 as uuid } from 'uuid'; +import { ApiResponse } from '@opensearch-project/opensearch'; import { OpenSearchDashboardsRequest, RequestHandlerContext } from '../../../../../src/core/server'; import { IMessage, IInput } from '../../../common/types/chat_saved_object_attributes'; import { convertToTraces } from '../../../common/utils/llm_chat/traces'; @@ -51,16 +52,22 @@ export class OllyChatService implements ChatService { callbacks ); const memory = memoryInit(payload.messages); - const chatAgent = chatAgentInit( - model, - pluginTools.flatMap((tool) => tool.toolsList), - callbacks, - memory - ); - const agentResponse = await chatAgent.run( - payload.input.content, - payload.sessionId ? OllyChatService.abortControllers.get(payload.sessionId) : undefined - ); + + const agentFrameworkResponse = (await opensearchClient.transport.request({ + method: 'POST', + path: '/_plugins/_ml/agents/usjqiYsBC_Oyjc6-Rhpq/_execute', + body: { + parameters: { + question: payload.input.content, + }, + }, + })) as ApiResponse<{ + inference_results: Array<{ output: Array<{ name: string; result: string }> }>; + }>; + const agentFrameworkAnswer = + agentFrameworkResponse.body.inference_results[0].output[0].result; + await memory.chatHistory.addUserMessage(payload.input.content); + await memory.chatHistory.addAIChatMessage(agentFrameworkAnswer); const suggestions = await requestSuggestionsChain( model, @@ -71,7 +78,7 @@ export class OllyChatService implements ChatService { return buildOutputs( payload.input.content, - agentResponse, + agentFrameworkAnswer, traceId, suggestions, convertToTraces(runs) From 04ef3326e6820974c3a30ef9d1e56dafd75191a8 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 8 Nov 2023 17:37:00 +0800 Subject: [PATCH 10/31] feat: comply with multi type of agent Signed-off-by: SuZhou-Joe --- server/services/chat/olly_chat_service.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index b77d50fc..0f970c81 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -53,19 +53,25 @@ export class OllyChatService implements ChatService { ); const memory = memoryInit(payload.messages); + /** + * Wait for an API to fetch root agent id. + */ const agentFrameworkResponse = (await opensearchClient.transport.request({ method: 'POST', - path: '/_plugins/_ml/agents/usjqiYsBC_Oyjc6-Rhpq/_execute', + path: '/_plugins/_ml/agents/_UoprosBZFp32K9Rsfqe/_execute', body: { parameters: { question: payload.input.content, }, }, })) as ApiResponse<{ - inference_results: Array<{ output: Array<{ name: string; result: string }> }>; + inference_results: Array<{ + output: Array<{ name: string; result?: string; dataAsMap?: { response: string } }>; + }>; }>; + const outputBody = agentFrameworkResponse.body.inference_results?.[0]?.output?.[0]; const agentFrameworkAnswer = - agentFrameworkResponse.body.inference_results[0].output[0].result; + agentFrameworkResponse.body.inference_results[0].output[0].result || ""; await memory.chatHistory.addUserMessage(payload.input.content); await memory.chatHistory.addAIChatMessage(agentFrameworkAnswer); From 7cfd027085989e0cf1690415267ae5f36ae5f652 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 10:58:00 +0800 Subject: [PATCH 11/31] feat: integrate with memory APIs Signed-off-by: SuZhou-Joe --- server/routes/chat_routes.ts | 36 ++--- server/services/chat/chat_service.ts | 5 +- server/services/chat/olly_chat_service.ts | 97 ++++++------ .../agent_framework_storage_service.ts | 148 ++++++++++++++++++ server/services/storage/storage_service.ts | 2 + 5 files changed, 213 insertions(+), 75 deletions(-) create mode 100644 server/services/storage/agent_framework_storage_service.ts diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index 7e63804b..e857b645 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -15,6 +15,7 @@ import { ASSISTANT_API } from '../../common/constants/llm'; import { OllyChatService } from '../services/chat/olly_chat_service'; import { SavedObjectsStorageService } from '../services/storage/saved_objects_storage_service'; import { IMessage, IInput } from '../../common/types/chat_saved_object_attributes'; +import { AgentFrameworkStorageService } from '../services/storage/agent_framework_storage_service'; const llmRequestRoute = { path: ASSISTANT_API.SEND_MESSAGE, @@ -104,7 +105,7 @@ const updateSessionRoute = { export function registerChatRoutes(router: IRouter) { const createStorageService = (context: RequestHandlerContext) => - new SavedObjectsStorageService(context.core.savedObjects.client); + new AgentFrameworkStorageService(context.core.opensearch.client.asCurrentUser); const createChatService = () => new OllyChatService(); router.post( @@ -114,34 +115,25 @@ export function registerChatRoutes(router: IRouter) { request, response ): Promise> => { - const { sessionId, input, messages = [] } = request.body; + const { messages = [], input, sessionId: sessionIdInRequestBody } = request.body; const storageService = createStorageService(context); const chatService = createChatService(); - // get history from the chat object for existing chats - if (sessionId && messages.length === 0) { - try { - const session = await storageService.getSession(sessionId); - messages.push(...session.messages); - } catch (error) { - return response.custom({ statusCode: error.statusCode || 500, body: error.message }); - } - } - try { const outputs = await chatService.requestLLM( - { messages, input, sessionId }, + { messages, input, sessionId: sessionIdInRequestBody }, context, request ); - const title = input.content.substring(0, 50); - const saveMessagesResponse = await storageService.saveMessages( - title, - sessionId, - [...messages, input, ...outputs].filter((message) => message.content !== 'AbortError') - ); + const sessionId = outputs.memoryId; + const finalMessage = await storageService.getSession(sessionId); + return response.ok({ - body: { ...saveMessagesResponse, title }, + body: { + messages: finalMessage.messages, + sessionId: outputs.memoryId, + title: finalMessage.title + }, }); } catch (error) { context.assistant_plugin.logger.warn(error); @@ -278,13 +270,13 @@ export function registerChatRoutes(router: IRouter) { const outputs = await chatService.requestLLM( { messages, input, sessionId }, context, - request + request as any ); const title = input.content.substring(0, 50); const saveMessagesResponse = await storageService.saveMessages( title, sessionId, - [...messages, input, ...outputs].filter((message) => message.content !== 'AbortError') + [...messages, input, ...outputs.messages].filter((message) => message.content !== 'AbortError') ); return response.ok({ body: { ...saveMessagesResponse, title }, diff --git a/server/services/chat/chat_service.ts b/server/services/chat/chat_service.ts index 92d2ec89..e1e81b9a 100644 --- a/server/services/chat/chat_service.ts +++ b/server/services/chat/chat_service.ts @@ -13,7 +13,10 @@ export interface ChatService { payload: { messages: IMessage[]; input: IInput; sessionId?: string }, context: RequestHandlerContext, request: OpenSearchDashboardsRequest - ): Promise; + ): Promise<{ + messages: IMessage[]; + memoryId: string; + }>; generatePPL( context: RequestHandlerContext, request: OpenSearchDashboardsRequest diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index 0f970c81..d7dd4abe 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -9,30 +9,30 @@ import { ApiResponse } from '@opensearch-project/opensearch'; import { OpenSearchDashboardsRequest, RequestHandlerContext } from '../../../../../src/core/server'; import { IMessage, IInput } from '../../../common/types/chat_saved_object_attributes'; import { convertToTraces } from '../../../common/utils/llm_chat/traces'; -import { chatAgentInit } from '../../olly/agents/agent_helpers'; import { OpenSearchTracer } from '../../olly/callbacks/opensearch_tracer'; -import { requestSuggestionsChain } from '../../olly/chains/suggestions_generator'; -import { memoryInit } from '../../olly/memory/chat_agent_memory'; import { LLMModelFactory } from '../../olly/models/llm_model_factory'; -import { initTools } from '../../olly/tools/tools_helper'; import { PPLTools } from '../../olly/tools/tool_sets/ppl'; import { buildOutputs } from '../../olly/utils/output_builders/build_outputs'; import { AbortAgentExecutionSchema, LLMRequestSchema } from '../../routes/chat_routes'; import { PPLGenerationRequestSchema } from '../../routes/langchain_routes'; import { ChatService } from './chat_service'; +const MEMORY_ID_FIELD = 'memory_id'; +const RESPONSE_FIELD = 'response'; + export class OllyChatService implements ChatService { static abortControllers: Map = new Map(); public async requestLLM( payload: { messages: IMessage[]; input: IInput; sessionId?: string }, context: RequestHandlerContext, - request: OpenSearchDashboardsRequest - ): Promise { - const traceId = uuid(); - const observabilityClient = context.assistant_plugin.observabilityClient.asScoped(request); + request: OpenSearchDashboardsRequest + ): Promise<{ + messages: IMessage[]; + memoryId: string; + }> { + const { input, sessionId } = payload; const opensearchClient = context.core.opensearch.client.asCurrentUser; - const savedObjectsClient = context.core.savedObjects.client; if (payload.sessionId) { OllyChatService.abortControllers.set(payload.sessionId, new AbortController()); @@ -40,65 +40,58 @@ export class OllyChatService implements ChatService { try { const runs: Run[] = []; - const callbacks = [new OpenSearchTracer(opensearchClient, traceId, runs)]; - const model = LLMModelFactory.createModel({ client: opensearchClient }); - const embeddings = LLMModelFactory.createEmbeddings({ client: opensearchClient }); - const pluginTools = initTools( - model, - embeddings, - opensearchClient, - observabilityClient, - savedObjectsClient, - callbacks - ); - const memory = memoryInit(payload.messages); /** * Wait for an API to fetch root agent id. */ + const parametersPayload: { + question: string; + verbose?: boolean; + memory_id?: string; + } = { + question: input.content, + verbose: true, + }; + if (sessionId) { + parametersPayload.memory_id = sessionId; + } const agentFrameworkResponse = (await opensearchClient.transport.request({ method: 'POST', - path: '/_plugins/_ml/agents/_UoprosBZFp32K9Rsfqe/_execute', + path: '/_plugins/_ml/agents/-jld3IsBXlmiPBu-5dDC/_execute', body: { - parameters: { - question: payload.input.content, - }, + parameters: parametersPayload, }, })) as ApiResponse<{ inference_results: Array<{ - output: Array<{ name: string; result?: string; dataAsMap?: { response: string } }>; + output: Array<{ name: string; result?: string }>; }>; }>; - const outputBody = agentFrameworkResponse.body.inference_results?.[0]?.output?.[0]; - const agentFrameworkAnswer = - agentFrameworkResponse.body.inference_results[0].output[0].result || ""; - await memory.chatHistory.addUserMessage(payload.input.content); - await memory.chatHistory.addAIChatMessage(agentFrameworkAnswer); + const outputBody = + agentFrameworkResponse.body.inference_results?.[0]?.output || + agentFrameworkResponse.body.inference_results?.[0]?.output; + const memoryIdItem = outputBody?.find((item) => item.name === MEMORY_ID_FIELD); + const reversedOutputBody = [...outputBody].reverse(); + const finalAnswerItem = reversedOutputBody.find((item) => item.name === RESPONSE_FIELD); - const suggestions = await requestSuggestionsChain( - model, - pluginTools.flatMap((tool) => tool.toolsList), - memory, - callbacks - ); + const agentFrameworkAnswer = finalAnswerItem?.result || ''; - return buildOutputs( - payload.input.content, - agentFrameworkAnswer, - traceId, - suggestions, - convertToTraces(runs) - ); + return { + messages: buildOutputs(input.content, agentFrameworkAnswer, '', {}, convertToTraces(runs)), + memoryId: memoryIdItem?.result || '', + }; } catch (error) { context.assistant_plugin.logger.error(error); - return [ - { - type: 'output', - traceId, - contentType: 'error', - content: error.message, - }, - ]; + return { + messages: [ + { + type: 'output', + traceId: '', + contentType: 'error', + content: error.message, + }, + ], + memoryId: '', + }; } finally { if (payload.sessionId) { OllyChatService.abortControllers.delete(payload.sessionId); diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts new file mode 100644 index 00000000..7731301e --- /dev/null +++ b/server/services/storage/agent_framework_storage_service.ts @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ApiResponse } from '@opensearch-project/opensearch/.'; +import { OpenSearchClient } from '../../../../../src/core/server'; +import { LLM_INDEX } from '../../../common/constants/llm'; +import { + IInput, + IMessage, + IOutput, + ISession, + ISessionFindResponse, +} from '../../../common/types/chat_saved_object_attributes'; +import { GetSessionsSchema } from '../../routes/chat_routes'; +import { StorageService } from './storage_service'; + +export class AgentFrameworkStorageService implements StorageService { + constructor(private readonly client: OpenSearchClient) {} + async getSession(sessionId: string): Promise { + const session = (await this.client.transport.request({ + method: 'GET', + path: `/_plugins/_ml/memory/conversation/${sessionId}`, + })) as ApiResponse<{ + interactions: Array<{ + input: string; + response: string; + parent_interaction_id: string; + interaction_id: string; + }>; + }>; + return { + title: 'test', + version: 1, + createdTimeMs: Date.now(), + updatedTimeMs: Date.now(), + messages: session.body.interactions + .filter((item) => !item.parent_interaction_id) + .reduce((total, current) => { + const inputItem: IInput = { + type: 'input', + contentType: 'text', + content: current.input, + }; + const outputItems: IOutput[] = [ + { + type: 'output', + contentType: 'markdown', + content: current.response, + traceId: current.interaction_id, + }, + ]; + return [...total, inputItem, ...outputItems]; + }, [] as IMessage[]), + }; + } + + async getSessions(query: GetSessionsSchema): Promise { + await this.createIndex(); + const sessions = await this.client.search({ + index: LLM_INDEX.SESSIONS, + body: { + from: (query.page - 1) * query.perPage, + size: query.perPage, + ...(query.sortField && + query.sortOrder && { sort: [{ [query.sortField]: query.sortOrder }] }), + }, + }); + + return { + objects: sessions.body.hits.hits + .filter( + (hit): hit is RequiredKey => + hit._source !== null && hit._source !== undefined + ) + .map((session) => ({ ...session._source, id: session._id })), + total: + typeof sessions.body.hits.total === 'number' + ? sessions.body.hits.total + : sessions.body.hits.total.value, + }; + } + + async saveMessages( + title: string, + sessionId: string | undefined, + messages: IMessage[] + ): Promise<{ sessionId: string; messages: IMessage[] }> { + await this.createIndex(); + const timestamp = new Date().getTime(); + if (!sessionId) { + const createResponse = await this.client.index({ + index: LLM_INDEX.SESSIONS, + body: { + title, + version: 1, + createdTimeMs: timestamp, + updatedTimeMs: timestamp, + messages, + }, + }); + return { sessionId: createResponse.body._id, messages }; + } + const updateResponse = await this.client.update>({ + index: LLM_INDEX.SESSIONS, + id: sessionId, + body: { + doc: { + messages, + updatedTimeMs: timestamp, + }, + }, + }); + return { sessionId, messages }; + } + + private async createIndex() { + const existsResponse = await this.client.indices.exists({ index: LLM_INDEX.SESSIONS }); + if (!existsResponse.body) { + return this.client.indices.create({ + index: LLM_INDEX.SESSIONS, + body: { + settings: { + index: { + number_of_shards: '1', + auto_expand_replicas: '0-2', + mapping: { ignore_malformed: true }, + }, + }, + mappings: { + properties: { + title: { type: 'keyword' }, + createdTimeMs: { type: 'date' }, + updatedTimeMs: { type: 'date' }, + }, + }, + }, + }); + } + } + deleteSession(sessionId: string): Promise<{}> { + throw new Error('Method not implemented.'); + } + updateSession(sessionId: string, title: string): Promise<{}> { + throw new Error('Method not implemented.'); + } +} diff --git a/server/services/storage/storage_service.ts b/server/services/storage/storage_service.ts index 8d676c5e..0fe27df6 100644 --- a/server/services/storage/storage_service.ts +++ b/server/services/storage/storage_service.ts @@ -18,4 +18,6 @@ export interface StorageService { sessionId: string | undefined, messages: IMessage[] ): Promise<{ sessionId: string; messages: IMessage[] }>; + deleteSession(sessionId: string): Promise<{}>; + updateSession(sessionId: string, title: string): Promise<{}>; } From 20f2092f771daa9545af95c6149cc8ead23bbe0f Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 11:15:15 +0800 Subject: [PATCH 12/31] feat: use the agent id from request body Signed-off-by: SuZhou-Joe --- public/chat_header_button.tsx | 4 ++++ public/contexts/chat_context.tsx | 1 + public/hooks/use_chat_actions.tsx | 1 + server/routes/chat_routes.ts | 1 + server/services/chat/olly_chat_service.ts | 4 ++-- 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/public/chat_header_button.tsx b/public/chat_header_button.tsx index 74d48cc3..c90a14c3 100644 --- a/public/chat_header_button.tsx +++ b/public/chat_header_button.tsx @@ -41,6 +41,9 @@ export const HeaderChatButton: React.FC = (props) => { const [inputFocus, setInputFocus] = useState(false); const flyoutFullScreen = chatSize === 'fullscreen'; const inputRef = useRef(null); + const [rootAgentId, setRootAgentId] = useState( + new URL(window.location.href).searchParams.get('agent_id') || '' + ); if (!flyoutLoaded && flyoutVisible) flyoutLoaded = true; @@ -76,6 +79,7 @@ export const HeaderChatButton: React.FC = (props) => { setTitle, traceId, setTraceId, + rootAgentId, }), [ appId, diff --git a/public/contexts/chat_context.tsx b/public/contexts/chat_context.tsx index c0807be3..24d3a0f7 100644 --- a/public/contexts/chat_context.tsx +++ b/public/contexts/chat_context.tsx @@ -25,6 +25,7 @@ export interface IChatContext { setTitle: React.Dispatch>; traceId?: string; setTraceId: React.Dispatch>; + rootAgentId?: string; } export const ChatContext = React.createContext(null); diff --git a/public/hooks/use_chat_actions.tsx b/public/hooks/use_chat_actions.tsx index 864bf757..6561a89d 100644 --- a/public/hooks/use_chat_actions.tsx +++ b/public/hooks/use_chat_actions.tsx @@ -36,6 +36,7 @@ export const useChatActions = (): AssistantActions => { // do not send abort signal to http client to allow LLM call run in background body: JSON.stringify({ sessionId: chatContext.sessionId, + rootAgentId: chatContext.rootAgentId, ...(!chatContext.sessionId && { messages: chatState.messages }), // include all previous messages for new chats input, }), diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index e857b645..01329e1f 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -23,6 +23,7 @@ const llmRequestRoute = { body: schema.object({ sessionId: schema.maybe(schema.string()), messages: schema.maybe(schema.arrayOf(schema.any())), + rootAgentId: schema.string(), input: schema.object({ type: schema.literal('input'), context: schema.object({ diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index d7dd4abe..68a92ed4 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -31,7 +31,7 @@ export class OllyChatService implements ChatService { messages: IMessage[]; memoryId: string; }> { - const { input, sessionId } = payload; + const { input, sessionId, rootAgentId } = request.body; const opensearchClient = context.core.opensearch.client.asCurrentUser; if (payload.sessionId) { @@ -57,7 +57,7 @@ export class OllyChatService implements ChatService { } const agentFrameworkResponse = (await opensearchClient.transport.request({ method: 'POST', - path: '/_plugins/_ml/agents/-jld3IsBXlmiPBu-5dDC/_execute', + path: `/_plugins/_ml/agents/${rootAgentId}/_execute`, body: { parameters: parametersPayload, }, From 60b3a7cc6bd17521adb315e6cec4e22ad1c93b5e Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 14:50:32 +0800 Subject: [PATCH 13/31] feat: remove useless code Signed-off-by: SuZhou-Joe --- .../agent_framework_storage_service.ts | 76 +------------------ 1 file changed, 2 insertions(+), 74 deletions(-) diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts index 7731301e..64f2bd79 100644 --- a/server/services/storage/agent_framework_storage_service.ts +++ b/server/services/storage/agent_framework_storage_service.ts @@ -57,29 +57,7 @@ export class AgentFrameworkStorageService implements StorageService { } async getSessions(query: GetSessionsSchema): Promise { - await this.createIndex(); - const sessions = await this.client.search({ - index: LLM_INDEX.SESSIONS, - body: { - from: (query.page - 1) * query.perPage, - size: query.perPage, - ...(query.sortField && - query.sortOrder && { sort: [{ [query.sortField]: query.sortOrder }] }), - }, - }); - - return { - objects: sessions.body.hits.hits - .filter( - (hit): hit is RequiredKey => - hit._source !== null && hit._source !== undefined - ) - .map((session) => ({ ...session._source, id: session._id })), - total: - typeof sessions.body.hits.total === 'number' - ? sessions.body.hits.total - : sessions.body.hits.total.value, - }; + throw new Error('Method not implemented.'); } async saveMessages( @@ -87,57 +65,7 @@ export class AgentFrameworkStorageService implements StorageService { sessionId: string | undefined, messages: IMessage[] ): Promise<{ sessionId: string; messages: IMessage[] }> { - await this.createIndex(); - const timestamp = new Date().getTime(); - if (!sessionId) { - const createResponse = await this.client.index({ - index: LLM_INDEX.SESSIONS, - body: { - title, - version: 1, - createdTimeMs: timestamp, - updatedTimeMs: timestamp, - messages, - }, - }); - return { sessionId: createResponse.body._id, messages }; - } - const updateResponse = await this.client.update>({ - index: LLM_INDEX.SESSIONS, - id: sessionId, - body: { - doc: { - messages, - updatedTimeMs: timestamp, - }, - }, - }); - return { sessionId, messages }; - } - - private async createIndex() { - const existsResponse = await this.client.indices.exists({ index: LLM_INDEX.SESSIONS }); - if (!existsResponse.body) { - return this.client.indices.create({ - index: LLM_INDEX.SESSIONS, - body: { - settings: { - index: { - number_of_shards: '1', - auto_expand_replicas: '0-2', - mapping: { ignore_malformed: true }, - }, - }, - mappings: { - properties: { - title: { type: 'keyword' }, - createdTimeMs: { type: 'date' }, - updatedTimeMs: { type: 'date' }, - }, - }, - }, - }); - } + throw new Error('Method not implemented.'); } deleteSession(sessionId: string): Promise<{}> { throw new Error('Method not implemented.'); From 7d4153be1f0d67e9b98bfa31180093b087629ec0 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 15:09:23 +0800 Subject: [PATCH 14/31] feat: update babel.config.ts Signed-off-by: SuZhou-Joe --- babel.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/babel.config.js b/babel.config.js index a0f8a3ad..3805f7cf 100644 --- a/babel.config.js +++ b/babel.config.js @@ -16,8 +16,8 @@ module.exports = function (api) { ], plugins: [ [require('@babel/plugin-transform-runtime'), { regenerator: true }], - require('@babel/plugin-proposal-class-properties'), - require('@babel/plugin-proposal-object-rest-spread'), + require('@babel/plugin-transform-class-properties'), + require('@babel/plugin-transform-object-rest-spread'), [require('@babel/plugin-transform-modules-commonjs'), { allowTopLevelThis: true }], ], }; From 8daadfc8215890b8c26d0dc35c27d78a4109678e Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 21 Nov 2023 16:05:44 +0800 Subject: [PATCH 15/31] feat: modify API path according to doc Signed-off-by: SuZhou-Joe --- server/services/storage/agent_framework_storage_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts index 64f2bd79..05165acc 100644 --- a/server/services/storage/agent_framework_storage_service.ts +++ b/server/services/storage/agent_framework_storage_service.ts @@ -21,7 +21,7 @@ export class AgentFrameworkStorageService implements StorageService { async getSession(sessionId: string): Promise { const session = (await this.client.transport.request({ method: 'GET', - path: `/_plugins/_ml/memory/conversation/${sessionId}`, + path: `/_plugins/_ml/memory/conversation/${sessionId}/_list`, })) as ApiResponse<{ interactions: Array<{ input: string; From b20f382535aead50e95775ec911c7ab2ae3209de Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 14:15:50 +0800 Subject: [PATCH 16/31] feat: add mechannism to register messageParser Signed-off-by: SuZhou-Joe --- server/plugin.ts | 26 +++++++++++-- server/routes/chat_routes.ts | 3 +- server/routes/index.ts | 5 ++- .../storage/saved_objects_storage_service.ts | 6 ++- server/types.ts | 33 +++++++++++++++++ server/utils/message_parser_helper.test.ts | 24 ++++++++++++ server/utils/message_parser_helper.ts | 15 ++++++++ server/utils/message_parser_runner.test.ts | 37 +++++++++++++++++++ server/utils/message_parser_runner.ts | 19 ++++++++++ 9 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 server/utils/message_parser_helper.test.ts create mode 100644 server/utils/message_parser_helper.ts create mode 100644 server/utils/message_parser_runner.test.ts create mode 100644 server/utils/message_parser_runner.ts diff --git a/server/plugin.ts b/server/plugin.ts index b0d095cc..4f951f43 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -21,11 +21,12 @@ import { AssistantServerConfig } from './config/schema'; import './fetch-polyfill'; import { setupRoutes } from './routes/index'; import { chatSavedObject } from './saved_objects/chat_saved_object'; -import { AssistantPluginSetup, AssistantPluginStart } from './types'; +import { AssistantPluginSetup, AssistantPluginStart, MessageParser } from './types'; import { chatConfigSavedObject } from './saved_objects/chat_config_saved_object'; export class AssistantPlugin implements Plugin { private readonly logger: Logger; + private messageParsers: MessageParser[] = []; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); @@ -54,7 +55,9 @@ export class AssistantPlugin implements Plugin { + const findItem = this.messageParsers.find((item) => item.id === messageParser.id); + if (findItem) { + throw new Error(`There is already a messageParser whose id is ${messageParser.id}`); + } + + this.messageParsers.push(messageParser); + }, + removeMessageParser: (parserId: MessageParser['id']) => { + const findIndex = this.messageParsers.findIndex((item) => item.id === parserId); + if (findIndex < 0) { + this.logger.error(`There is not a messageParser whose id is ${parserId}`); + } + + this.messageParsers.splice(findIndex, 1); + }, + }; } public start(core: CoreStart) { diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index 01329e1f..18bb4680 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -16,6 +16,7 @@ import { OllyChatService } from '../services/chat/olly_chat_service'; import { SavedObjectsStorageService } from '../services/storage/saved_objects_storage_service'; import { IMessage, IInput } from '../../common/types/chat_saved_object_attributes'; import { AgentFrameworkStorageService } from '../services/storage/agent_framework_storage_service'; +import { RoutesOptions } from '../types'; const llmRequestRoute = { path: ASSISTANT_API.SEND_MESSAGE, @@ -104,7 +105,7 @@ const updateSessionRoute = { }, }; -export function registerChatRoutes(router: IRouter) { +export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) { const createStorageService = (context: RequestHandlerContext) => new AgentFrameworkStorageService(context.core.opensearch.client.asCurrentUser); const createChatService = () => new OllyChatService(); diff --git a/server/routes/index.ts b/server/routes/index.ts index ae33e1c3..0b2d7eee 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { RoutesOptions } from '../types'; import { IRouter } from '../../../../src/core/server'; import { registerChatRoutes } from './chat_routes'; import { registerLangchainRoutes } from './langchain_routes'; -export function setupRoutes(router: IRouter) { - registerChatRoutes(router); +export function setupRoutes(router: IRouter, options: RoutesOptions) { + registerChatRoutes(router, options); registerLangchainRoutes(router); } diff --git a/server/services/storage/saved_objects_storage_service.ts b/server/services/storage/saved_objects_storage_service.ts index 78fcffb4..f85bba48 100644 --- a/server/services/storage/saved_objects_storage_service.ts +++ b/server/services/storage/saved_objects_storage_service.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { MessageParser } from '../../types'; import { SavedObjectsClientContract } from '../../../../../src/core/server'; import { CHAT_SAVED_OBJECT, @@ -15,7 +16,10 @@ import { GetSessionsSchema } from '../../routes/chat_routes'; import { StorageService } from './storage_service'; export class SavedObjectsStorageService implements StorageService { - constructor(private readonly client: SavedObjectsClientContract) {} + constructor( + private readonly client: SavedObjectsClientContract, + private readonly messageParsers: MessageParser[] + ) {} private convertUpdatedTimeField(updatedAt: string | undefined) { return updatedAt ? new Date(updatedAt).getTime() : undefined; diff --git a/server/types.ts b/server/types.ts index bb72cc4b..3c1ffca5 100644 --- a/server/types.ts +++ b/server/types.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { IMessage } from '../common/types/chat_saved_object_attributes'; import { ILegacyClusterClient, Logger } from '../../../src/core/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -10,6 +11,38 @@ export interface AssistantPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AssistantPluginStart {} +export interface IMessageParserHelper { + addMessage: (message: IMessage) => boolean; +} + +export interface Interaction { + input: string; + response: string; +} + +export interface MessageParser { + /** + * The id of the parser, should be unique among the parsers. + */ + id: string; + /** + * Order declare the order message parser will be execute. + * parser with order 2 will be execute before parser with order 1. + */ + order?: number; + /** + * parserProvider is the callback that will be triggered in each message + */ + parserProvider: ( + interaction: Interaction, + messageParserHelper: IMessageParserHelper + ) => Promise; +} + +export interface RoutesOptions { + messageParsers: MessageParser[]; +} + declare module '../../../src/core/server' { interface RequestHandlerContext { assistant_plugin: { diff --git a/server/utils/message_parser_helper.test.ts b/server/utils/message_parser_helper.test.ts new file mode 100644 index 00000000..7799892a --- /dev/null +++ b/server/utils/message_parser_helper.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MessageParserHelper } from './message_parser_helper'; + +describe('MessageParserHelper', () => { + it('return with correct message', async () => { + const messageParserHelper = new MessageParserHelper(); + messageParserHelper.addMessage({ + type: 'output', + contentType: 'markdown', + content: 'output', + }); + expect(messageParserHelper.messages).toEqual([ + { + type: 'output', + contentType: 'markdown', + content: 'output', + }, + ]); + }); +}); diff --git a/server/utils/message_parser_helper.ts b/server/utils/message_parser_helper.ts new file mode 100644 index 00000000..36ff0963 --- /dev/null +++ b/server/utils/message_parser_helper.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IMessage } from '../../common/types/chat_saved_object_attributes'; +import { IMessageParserHelper } from '../types'; + +export class MessageParserHelper implements IMessageParserHelper { + public messages: IMessage[] = []; + addMessage(message: IMessage) { + this.messages.push(message); + return true; + } +} diff --git a/server/utils/message_parser_runner.test.ts b/server/utils/message_parser_runner.test.ts new file mode 100644 index 00000000..03ce3a1b --- /dev/null +++ b/server/utils/message_parser_runner.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MessageParserRunner } from './message_parser_runner'; + +describe('MessageParserRunner', () => { + it('run with correct result', async () => { + const messageParserRunner = new MessageParserRunner([ + { + id: 'test', + parserProvider(interaction, messageParserHelper) { + messageParserHelper.addMessage({ + type: 'output', + contentType: 'markdown', + content: interaction.response, + }); + return Promise.resolve(''); + }, + }, + ]); + + expect( + await messageParserRunner.run({ + response: 'output', + input: 'input', + }) + ).toEqual([ + { + type: 'output', + contentType: 'markdown', + content: 'output', + }, + ]); + }); +}); diff --git a/server/utils/message_parser_runner.ts b/server/utils/message_parser_runner.ts new file mode 100644 index 00000000..3545d02d --- /dev/null +++ b/server/utils/message_parser_runner.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IMessage } from '../../common/types/chat_saved_object_attributes'; +import { Interaction, MessageParser } from '../types'; +import { MessageParserHelper } from './message_parser_helper'; + +export class MessageParserRunner { + constructor(private readonly messageParsers: MessageParser[]) {} + async run(interaction: Interaction): Promise { + const messageParserHelper = new MessageParserHelper(); + for (const messageParser of this.messageParsers) { + await messageParser.parserProvider(interaction, messageParserHelper); + } + return messageParserHelper.messages; + } +} From 2ca5cedc1d314264b893c89ce48127cf14871a57 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 15:41:10 +0800 Subject: [PATCH 17/31] feat: sort when run Signed-off-by: SuZhou-Joe --- server/types.ts | 6 ++- server/utils/message_parser_runner.test.ts | 63 ++++++++++++++++++++++ server/utils/message_parser_runner.ts | 8 ++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/server/types.ts b/server/types.ts index 3c1ffca5..f6200284 100644 --- a/server/types.ts +++ b/server/types.ts @@ -26,8 +26,10 @@ export interface MessageParser { */ id: string; /** - * Order declare the order message parser will be execute. - * parser with order 2 will be execute before parser with order 1. + * Order field declares the order message parser will be execute. + * parser with order 2 will be executed after parser with order 1. + * If not specified, the default order will be 999. + * @default 999 */ order?: number; /** diff --git a/server/utils/message_parser_runner.test.ts b/server/utils/message_parser_runner.test.ts index 03ce3a1b..6c0bde65 100644 --- a/server/utils/message_parser_runner.test.ts +++ b/server/utils/message_parser_runner.test.ts @@ -34,4 +34,67 @@ describe('MessageParserRunner', () => { }, ]); }); + + it('run with correct result when different order is present', async () => { + const messageParserRunner = new MessageParserRunner([ + { + id: 'testA', + order: 2, + parserProvider(interaction, messageParserHelper) { + messageParserHelper.addMessage({ + type: 'output', + contentType: 'markdown', + content: 'A', + }); + return Promise.resolve(''); + }, + }, + { + id: 'testNoOrder', + parserProvider(interaction, messageParserHelper) { + messageParserHelper.addMessage({ + type: 'output', + contentType: 'markdown', + content: 'NoOrder', + }); + return Promise.resolve(''); + }, + }, + { + id: 'testB', + order: 1, + parserProvider(interaction, messageParserHelper) { + messageParserHelper.addMessage({ + type: 'output', + contentType: 'markdown', + content: 'B', + }); + return Promise.resolve(''); + }, + }, + ]); + + expect( + await messageParserRunner.run({ + response: 'output', + input: 'input', + }) + ).toEqual([ + { + type: 'output', + contentType: 'markdown', + content: 'B', + }, + { + type: 'output', + contentType: 'markdown', + content: 'A', + }, + { + type: 'output', + contentType: 'markdown', + content: 'NoOrder', + }, + ]); + }); }); diff --git a/server/utils/message_parser_runner.ts b/server/utils/message_parser_runner.ts index 3545d02d..b091c6de 100644 --- a/server/utils/message_parser_runner.ts +++ b/server/utils/message_parser_runner.ts @@ -11,7 +11,13 @@ export class MessageParserRunner { constructor(private readonly messageParsers: MessageParser[]) {} async run(interaction: Interaction): Promise { const messageParserHelper = new MessageParserHelper(); - for (const messageParser of this.messageParsers) { + const sortedParsers = [...this.messageParsers]; + sortedParsers.sort((parserA, parserB) => { + const { order: orderA = 999 } = parserA; + const { order: orderB = 999 } = parserB; + return orderA - orderB; + }); + for (const messageParser of sortedParsers) { await messageParser.parserProvider(interaction, messageParserHelper); } return messageParserHelper.messages; From d0ddf09be6a2076e409fa63ee51fea06431a6d4c Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 15:42:08 +0800 Subject: [PATCH 18/31] feat: sort when run Signed-off-by: SuZhou-Joe --- server/utils/message_parser_runner.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/server/utils/message_parser_runner.test.ts b/server/utils/message_parser_runner.test.ts index 6c0bde65..a86353e0 100644 --- a/server/utils/message_parser_runner.test.ts +++ b/server/utils/message_parser_runner.test.ts @@ -49,6 +49,18 @@ describe('MessageParserRunner', () => { return Promise.resolve(''); }, }, + { + id: 'testOrder1000', + order: 1000, + parserProvider(interaction, messageParserHelper) { + messageParserHelper.addMessage({ + type: 'output', + contentType: 'markdown', + content: 'order1000', + }); + return Promise.resolve(''); + }, + }, { id: 'testNoOrder', parserProvider(interaction, messageParserHelper) { @@ -95,6 +107,11 @@ describe('MessageParserRunner', () => { contentType: 'markdown', content: 'NoOrder', }, + { + type: 'output', + contentType: 'markdown', + content: 'order1000', + }, ]); }); }); From f46739b8fe9c677010a9d5272b3cda0ce28fd9db Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 17:30:19 +0800 Subject: [PATCH 19/31] feat: add doc change Signed-off-by: SuZhou-Joe --- CHANGELOG.md | 7 +++++++ server/README.md | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 server/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3577f8af --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# CHANGELOG + +Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) + +### 📈 Features/Enhancements + +- Add support for registerMessageParser ([#5](https://github.com/opensearch-project/dashboards-assistant/pull/5)) \ No newline at end of file diff --git a/server/README.md b/server/README.md new file mode 100644 index 00000000..619bb070 --- /dev/null +++ b/server/README.md @@ -0,0 +1,22 @@ +# `registerMessageParser` — Register your customized parser logic into Chatbot. + +**Interaction** refers to a question-answer pair in Chatbot application. In most cases, an interaction consists of two messages: an `Input` message and an `Output` message. However, as the Chatbot evolves to become more powerful, it may display new messages such as visualizations, data explorers, or data grids. Therefore, it is crucial to implement a mechanism that allows other plugins to register their customized parser logic based on each interaction body. + + +## API + +### registerMessageParser + +``` +dashboardAssistant.registerMessageParser({ + id: "foo_parser", + parserProvider: async (interaction, messageParserHelper) => { + if (interaction.additional_info?.visualizationId) { + messageParserHelper.addMessage({ + contentType: "visualization", + content: interaction.additional_info.visualizationId + }) + } + } +}) +``` From 8d54ff02386c7e02b89b1310967a812cefe28c36 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 20 Nov 2023 17:32:24 +0800 Subject: [PATCH 20/31] feat: add diagram Signed-off-by: SuZhou-Joe --- server/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/README.md b/server/README.md index 619bb070..4008477a 100644 --- a/server/README.md +++ b/server/README.md @@ -2,6 +2,8 @@ **Interaction** refers to a question-answer pair in Chatbot application. In most cases, an interaction consists of two messages: an `Input` message and an `Output` message. However, as the Chatbot evolves to become more powerful, it may display new messages such as visualizations, data explorers, or data grids. Therefore, it is crucial to implement a mechanism that allows other plugins to register their customized parser logic based on each interaction body. +![message parser](https://github.com/opensearch-project/dashboards-assistant/assets/13493605/b4ec1ff8-5339-4119-ad20-b2c31057bb0b) + ## API From b191faa90902564e5a4d0582c5a90d8bedb5989d Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 22 Nov 2023 13:26:02 +0800 Subject: [PATCH 21/31] feat: optimize code Signed-off-by: SuZhou-Joe --- server/README.md | 25 ++++--- .../storage/saved_objects_storage_service.ts | 6 +- server/types.ts | 5 +- server/utils/message_parser_helper.test.ts | 24 ------ server/utils/message_parser_helper.ts | 15 ---- server/utils/message_parser_runner.test.ts | 75 ++++++++++--------- server/utils/message_parser_runner.ts | 13 +++- 7 files changed, 65 insertions(+), 98 deletions(-) delete mode 100644 server/utils/message_parser_helper.test.ts delete mode 100644 server/utils/message_parser_helper.ts diff --git a/server/README.md b/server/README.md index 4008477a..b7905c80 100644 --- a/server/README.md +++ b/server/README.md @@ -4,21 +4,24 @@ ![message parser](https://github.com/opensearch-project/dashboards-assistant/assets/13493605/b4ec1ff8-5339-4119-ad20-b2c31057bb0b) - ## API ### registerMessageParser -``` +```typescript dashboardAssistant.registerMessageParser({ - id: "foo_parser", - parserProvider: async (interaction, messageParserHelper) => { - if (interaction.additional_info?.visualizationId) { - messageParserHelper.addMessage({ - contentType: "visualization", - content: interaction.additional_info.visualizationId - }) - } + id: 'foo_parser', + parserProvider: async (interaction) => { + if (interaction.input) { + return [ + { + type: 'input', + contentType: 'text', + content: interaction.input, + }, + ]; } -}) + return []; + }, +}); ``` diff --git a/server/services/storage/saved_objects_storage_service.ts b/server/services/storage/saved_objects_storage_service.ts index f85bba48..78fcffb4 100644 --- a/server/services/storage/saved_objects_storage_service.ts +++ b/server/services/storage/saved_objects_storage_service.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MessageParser } from '../../types'; import { SavedObjectsClientContract } from '../../../../../src/core/server'; import { CHAT_SAVED_OBJECT, @@ -16,10 +15,7 @@ import { GetSessionsSchema } from '../../routes/chat_routes'; import { StorageService } from './storage_service'; export class SavedObjectsStorageService implements StorageService { - constructor( - private readonly client: SavedObjectsClientContract, - private readonly messageParsers: MessageParser[] - ) {} + constructor(private readonly client: SavedObjectsClientContract) {} private convertUpdatedTimeField(updatedAt: string | undefined) { return updatedAt ? new Date(updatedAt).getTime() : undefined; diff --git a/server/types.ts b/server/types.ts index f6200284..dbcda88d 100644 --- a/server/types.ts +++ b/server/types.ts @@ -35,10 +35,7 @@ export interface MessageParser { /** * parserProvider is the callback that will be triggered in each message */ - parserProvider: ( - interaction: Interaction, - messageParserHelper: IMessageParserHelper - ) => Promise; + parserProvider: (interaction: Interaction) => Promise; } export interface RoutesOptions { diff --git a/server/utils/message_parser_helper.test.ts b/server/utils/message_parser_helper.test.ts deleted file mode 100644 index 7799892a..00000000 --- a/server/utils/message_parser_helper.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { MessageParserHelper } from './message_parser_helper'; - -describe('MessageParserHelper', () => { - it('return with correct message', async () => { - const messageParserHelper = new MessageParserHelper(); - messageParserHelper.addMessage({ - type: 'output', - contentType: 'markdown', - content: 'output', - }); - expect(messageParserHelper.messages).toEqual([ - { - type: 'output', - contentType: 'markdown', - content: 'output', - }, - ]); - }); -}); diff --git a/server/utils/message_parser_helper.ts b/server/utils/message_parser_helper.ts deleted file mode 100644 index 36ff0963..00000000 --- a/server/utils/message_parser_helper.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { IMessage } from '../../common/types/chat_saved_object_attributes'; -import { IMessageParserHelper } from '../types'; - -export class MessageParserHelper implements IMessageParserHelper { - public messages: IMessage[] = []; - addMessage(message: IMessage) { - this.messages.push(message); - return true; - } -} diff --git a/server/utils/message_parser_runner.test.ts b/server/utils/message_parser_runner.test.ts index a86353e0..97238ecf 100644 --- a/server/utils/message_parser_runner.test.ts +++ b/server/utils/message_parser_runner.test.ts @@ -10,13 +10,14 @@ describe('MessageParserRunner', () => { const messageParserRunner = new MessageParserRunner([ { id: 'test', - parserProvider(interaction, messageParserHelper) { - messageParserHelper.addMessage({ - type: 'output', - contentType: 'markdown', - content: interaction.response, - }); - return Promise.resolve(''); + parserProvider(interaction) { + return Promise.resolve([ + { + type: 'output', + contentType: 'markdown', + content: interaction.response, + }, + ]); }, }, ]); @@ -40,48 +41,52 @@ describe('MessageParserRunner', () => { { id: 'testA', order: 2, - parserProvider(interaction, messageParserHelper) { - messageParserHelper.addMessage({ - type: 'output', - contentType: 'markdown', - content: 'A', - }); - return Promise.resolve(''); + parserProvider() { + return Promise.resolve([ + { + type: 'output', + contentType: 'markdown', + content: 'A', + }, + ]); }, }, { id: 'testOrder1000', order: 1000, - parserProvider(interaction, messageParserHelper) { - messageParserHelper.addMessage({ - type: 'output', - contentType: 'markdown', - content: 'order1000', - }); - return Promise.resolve(''); + parserProvider() { + return Promise.resolve([ + { + type: 'output', + contentType: 'markdown', + content: 'order1000', + }, + ]); }, }, { id: 'testNoOrder', - parserProvider(interaction, messageParserHelper) { - messageParserHelper.addMessage({ - type: 'output', - contentType: 'markdown', - content: 'NoOrder', - }); - return Promise.resolve(''); + parserProvider(interaction) { + return Promise.resolve([ + { + type: 'output', + contentType: 'markdown', + content: 'NoOrder', + }, + ]); }, }, { id: 'testB', order: 1, - parserProvider(interaction, messageParserHelper) { - messageParserHelper.addMessage({ - type: 'output', - contentType: 'markdown', - content: 'B', - }); - return Promise.resolve(''); + parserProvider() { + return Promise.resolve([ + { + type: 'output', + contentType: 'markdown', + content: 'B', + }, + ]); }, }, ]); diff --git a/server/utils/message_parser_runner.ts b/server/utils/message_parser_runner.ts index b091c6de..97972d17 100644 --- a/server/utils/message_parser_runner.ts +++ b/server/utils/message_parser_runner.ts @@ -5,21 +5,26 @@ import { IMessage } from '../../common/types/chat_saved_object_attributes'; import { Interaction, MessageParser } from '../types'; -import { MessageParserHelper } from './message_parser_helper'; export class MessageParserRunner { constructor(private readonly messageParsers: MessageParser[]) {} async run(interaction: Interaction): Promise { - const messageParserHelper = new MessageParserHelper(); const sortedParsers = [...this.messageParsers]; sortedParsers.sort((parserA, parserB) => { const { order: orderA = 999 } = parserA; const { order: orderB = 999 } = parserB; return orderA - orderB; }); + let results: IMessage[] = []; for (const messageParser of sortedParsers) { - await messageParser.parserProvider(interaction, messageParserHelper); + let tempResult: IMessage[] = []; + try { + tempResult = await messageParser.parserProvider(interaction); + } catch (e) { + tempResult = []; + } + results = [...results, ...tempResult]; } - return messageParserHelper.messages; + return results; } } From 4bcca2a4798e213120ea83fc6ea953d5571b19b7 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 22 Nov 2023 13:32:05 +0800 Subject: [PATCH 22/31] feat: add error handling Signed-off-by: SuZhou-Joe --- server/utils/message_parser_runner.test.ts | 28 ++++++++++++++++++++++ server/utils/message_parser_runner.ts | 6 +++++ 2 files changed, 34 insertions(+) diff --git a/server/utils/message_parser_runner.test.ts b/server/utils/message_parser_runner.test.ts index 97238ecf..e931f47a 100644 --- a/server/utils/message_parser_runner.test.ts +++ b/server/utils/message_parser_runner.test.ts @@ -119,4 +119,32 @@ describe('MessageParserRunner', () => { }, ]); }); + + it('Do not append messages that are throwed with error or not an array', async () => { + const messageParserRunner = new MessageParserRunner([ + { + id: 'test_with_error', + parserProvider() { + throw new Error('error'); + }, + }, + { + id: 'test_with_incorrect_format_of_return', + parserProvider() { + return Promise.resolve({ + type: 'output', + contentType: 'markdown', + content: 'order1000', + }); + }, + }, + ]); + + expect( + await messageParserRunner.run({ + response: 'output', + input: 'input', + }) + ).toEqual([]); + }); }); diff --git a/server/utils/message_parser_runner.ts b/server/utils/message_parser_runner.ts index 97972d17..2f5d7d59 100644 --- a/server/utils/message_parser_runner.ts +++ b/server/utils/message_parser_runner.ts @@ -20,6 +20,12 @@ export class MessageParserRunner { let tempResult: IMessage[] = []; try { tempResult = await messageParser.parserProvider(interaction); + /** + * Make sure the tempResult is an array. + */ + if (!Array.isArray(tempResult)) { + tempResult = []; + } } catch (e) { tempResult = []; } From b7035bda4433ef793b5a447edeb8e86e61a2a469 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 22 Nov 2023 15:34:26 +0800 Subject: [PATCH 23/31] feat: optimize Signed-off-by: SuZhou-Joe --- public/tabs/chat/chat_page.tsx | 2 +- server/plugin.ts | 4 +--- server/routes/chat_routes.ts | 11 +++++++---- server/routes/index.ts | 4 ++-- server/types.ts | 8 ++++---- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/public/tabs/chat/chat_page.tsx b/public/tabs/chat/chat_page.tsx index 6ca8893f..f8101d2b 100644 --- a/public/tabs/chat/chat_page.tsx +++ b/public/tabs/chat/chat_page.tsx @@ -6,11 +6,11 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; import cs from 'classnames'; +import { useObservable } from 'react-use'; import { useChatContext } from '../../contexts/chat_context'; import { useChatState } from '../../hooks/use_chat_state'; import { ChatPageContent } from './chat_page_content'; import { ChatInputControls } from './controls/chat_input_controls'; -import { useObservable } from 'react-use'; import { useCore } from '../../contexts/core_context'; interface ChatPageProps { diff --git a/server/plugin.ts b/server/plugin.ts index 4f951f43..47906ee0 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -55,9 +55,7 @@ export class AssistantPlugin implements Plugin new AgentFrameworkStorageService(context.core.opensearch.client.asCurrentUser); const createChatService = () => new OllyChatService(); @@ -134,7 +134,7 @@ export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) body: { messages: finalMessage.messages, sessionId: outputs.memoryId, - title: finalMessage.title + title: finalMessage.title, }, }); } catch (error) { @@ -272,13 +272,16 @@ export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) const outputs = await chatService.requestLLM( { messages, input, sessionId }, context, - request as any + // @ts-ignore + request ); const title = input.content.substring(0, 50); const saveMessagesResponse = await storageService.saveMessages( title, sessionId, - [...messages, input, ...outputs.messages].filter((message) => message.content !== 'AbortError') + [...messages, input, ...outputs.messages].filter( + (message) => message.content !== 'AbortError' + ) ); return response.ok({ body: { ...saveMessagesResponse, title }, diff --git a/server/routes/index.ts b/server/routes/index.ts index 0b2d7eee..52426cf2 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -8,7 +8,7 @@ import { IRouter } from '../../../../src/core/server'; import { registerChatRoutes } from './chat_routes'; import { registerLangchainRoutes } from './langchain_routes'; -export function setupRoutes(router: IRouter, options: RoutesOptions) { - registerChatRoutes(router, options); +export function setupRoutes(router: IRouter) { + registerChatRoutes(router); registerLangchainRoutes(router); } diff --git a/server/types.ts b/server/types.ts index dbcda88d..625f51ba 100644 --- a/server/types.ts +++ b/server/types.ts @@ -11,13 +11,13 @@ export interface AssistantPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AssistantPluginStart {} -export interface IMessageParserHelper { - addMessage: (message: IMessage) => boolean; -} - export interface Interaction { input: string; response: string; + conversation_id: string; + interaction_id: string; + create_time: string; + additional_info: Record; } export interface MessageParser { From 15dcaf111969c90c19389c904093deba3f94861b Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 22 Nov 2023 16:44:22 +0800 Subject: [PATCH 24/31] feat: change implementation of basic_input_output to built-in parser (#10) * feat: change implementation of basic_input_output to built-in parser Signed-off-by: SuZhou-Joe * feat: update CHANGELOG Signed-off-by: SuZhou-Joe * feat: enable build input-output message Signed-off-by: SuZhou-Joe * feat: sort interactions Signed-off-by: SuZhou-Joe * feat: remove useless code Signed-off-by: SuZhou-Joe * feat: use parent_interaction_id as traceId Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- CHANGELOG.md | 3 +- server/parsers/basic_input_output_parser.ts | 28 ++++++++++ server/plugin.ts | 25 +++++---- server/routes/chat_routes.ts | 24 ++++----- server/routes/index.ts | 4 +- server/services/chat/olly_chat_service.ts | 29 ++++------ .../agent_framework_storage_service.ts | 54 ++++++++++--------- server/types.ts | 1 + 8 files changed, 99 insertions(+), 69 deletions(-) create mode 100644 server/parsers/basic_input_output_parser.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3577f8af..78c3fdb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,5 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 📈 Features/Enhancements -- Add support for registerMessageParser ([#5](https://github.com/opensearch-project/dashboards-assistant/pull/5)) \ No newline at end of file +- Add support for registerMessageParser ([#5](https://github.com/opensearch-project/dashboards-assistant/pull/5)) +- Change implementation of basic_input_output to built-in parser ([#10](https://github.com/opensearch-project/dashboards-assistant/pull/10)) \ No newline at end of file diff --git a/server/parsers/basic_input_output_parser.ts b/server/parsers/basic_input_output_parser.ts new file mode 100644 index 00000000..7880e4e4 --- /dev/null +++ b/server/parsers/basic_input_output_parser.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IInput, IOutput } from '../../common/types/chat_saved_object_attributes'; +import { Interaction } from '../types'; + +export const BasicInputOutputParser = { + order: 0, + id: 'output_message', + async parserProvider(interaction: Interaction) { + const inputItem: IInput = { + type: 'input', + contentType: 'text', + content: interaction.input, + }; + const outputItems: IOutput[] = [ + { + type: 'output', + contentType: 'markdown', + content: interaction.response, + traceId: interaction.parent_interaction_id, + }, + ]; + return [inputItem, ...outputItems]; + }, +}; diff --git a/server/plugin.ts b/server/plugin.ts index 47906ee0..745e3b0c 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -23,6 +23,7 @@ import { setupRoutes } from './routes/index'; import { chatSavedObject } from './saved_objects/chat_saved_object'; import { AssistantPluginSetup, AssistantPluginStart, MessageParser } from './types'; import { chatConfigSavedObject } from './saved_objects/chat_config_saved_object'; +import { BasicInputOutputParser } from './parsers/basic_input_output_parser'; export class AssistantPlugin implements Plugin { private readonly logger: Logger; @@ -55,7 +56,9 @@ export class AssistantPlugin implements Plugin { - const findItem = this.messageParsers.find((item) => item.id === messageParser.id); - if (findItem) { - throw new Error(`There is already a messageParser whose id is ${messageParser.id}`); - } + const registerMessageParser = (messageParser: MessageParser) => { + const findItem = this.messageParsers.find((item) => item.id === messageParser.id); + if (findItem) { + throw new Error(`There is already a messageParser whose id is ${messageParser.id}`); + } - this.messageParsers.push(messageParser); - }, + this.messageParsers.push(messageParser); + }; + + registerMessageParser(BasicInputOutputParser); + + return { + registerMessageParser, removeMessageParser: (parserId: MessageParser['id']) => { const findIndex = this.messageParsers.findIndex((item) => item.id === parserId); if (findIndex < 0) { diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index de4a4dbf..946df488 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -13,7 +13,6 @@ import { } from '../../../../src/core/server'; import { ASSISTANT_API } from '../../common/constants/llm'; import { OllyChatService } from '../services/chat/olly_chat_service'; -import { SavedObjectsStorageService } from '../services/storage/saved_objects_storage_service'; import { IMessage, IInput } from '../../common/types/chat_saved_object_attributes'; import { AgentFrameworkStorageService } from '../services/storage/agent_framework_storage_service'; import { RoutesOptions } from '../types'; @@ -63,6 +62,7 @@ const regenerateRoute = { validate: { body: schema.object({ sessionId: schema.string(), + rootAgentId: schema.string(), }), }, }; @@ -105,9 +105,12 @@ const updateSessionRoute = { }, }; -export function registerChatRoutes(router: IRouter) { +export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) { const createStorageService = (context: RequestHandlerContext) => - new AgentFrameworkStorageService(context.core.opensearch.client.asCurrentUser); + new AgentFrameworkStorageService( + context.core.opensearch.client.asCurrentUser, + routeOptions.messageParsers + ); const createChatService = () => new OllyChatService(); router.post( @@ -117,15 +120,14 @@ export function registerChatRoutes(router: IRouter) { request, response ): Promise> => { - const { messages = [], input, sessionId: sessionIdInRequestBody } = request.body; + const { messages = [], input, sessionId: sessionIdInRequestBody, rootAgentId } = request.body; const storageService = createStorageService(context); const chatService = createChatService(); try { const outputs = await chatService.requestLLM( - { messages, input, sessionId: sessionIdInRequestBody }, - context, - request + { messages, input, sessionId: sessionIdInRequestBody, rootAgentId }, + context ); const sessionId = outputs.memoryId; const finalMessage = await storageService.getSession(sessionId); @@ -250,7 +252,7 @@ export function registerChatRoutes(router: IRouter) { request, response ): Promise> => { - const { sessionId } = request.body; + const { sessionId, rootAgentId } = request.body; const storageService = createStorageService(context); let messages: IMessage[] = []; const chatService = createChatService(); @@ -270,10 +272,8 @@ export function registerChatRoutes(router: IRouter) { try { const outputs = await chatService.requestLLM( - { messages, input, sessionId }, - context, - // @ts-ignore - request + { messages, input, sessionId, rootAgentId }, + context ); const title = input.content.substring(0, 50); const saveMessagesResponse = await storageService.saveMessages( diff --git a/server/routes/index.ts b/server/routes/index.ts index 52426cf2..093bb313 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -8,7 +8,7 @@ import { IRouter } from '../../../../src/core/server'; import { registerChatRoutes } from './chat_routes'; import { registerLangchainRoutes } from './langchain_routes'; -export function setupRoutes(router: IRouter) { - registerChatRoutes(router); +export function setupRoutes(router: IRouter, routeOptions: RoutesOptions) { + registerChatRoutes(router, routeOptions); registerLangchainRoutes(router); } diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index 68a92ed4..8dadc991 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -3,35 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Run } from 'langchain/callbacks'; import { v4 as uuid } from 'uuid'; import { ApiResponse } from '@opensearch-project/opensearch'; import { OpenSearchDashboardsRequest, RequestHandlerContext } from '../../../../../src/core/server'; import { IMessage, IInput } from '../../../common/types/chat_saved_object_attributes'; -import { convertToTraces } from '../../../common/utils/llm_chat/traces'; import { OpenSearchTracer } from '../../olly/callbacks/opensearch_tracer'; import { LLMModelFactory } from '../../olly/models/llm_model_factory'; import { PPLTools } from '../../olly/tools/tool_sets/ppl'; -import { buildOutputs } from '../../olly/utils/output_builders/build_outputs'; -import { AbortAgentExecutionSchema, LLMRequestSchema } from '../../routes/chat_routes'; import { PPLGenerationRequestSchema } from '../../routes/langchain_routes'; import { ChatService } from './chat_service'; +import { LLMRequestSchema } from '../../routes/chat_routes'; const MEMORY_ID_FIELD = 'memory_id'; -const RESPONSE_FIELD = 'response'; export class OllyChatService implements ChatService { static abortControllers: Map = new Map(); public async requestLLM( - payload: { messages: IMessage[]; input: IInput; sessionId?: string }, - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest + payload: { messages: IMessage[]; input: IInput; sessionId?: string; rootAgentId: string }, + context: RequestHandlerContext ): Promise<{ messages: IMessage[]; memoryId: string; }> { - const { input, sessionId, rootAgentId } = request.body; + const { input, sessionId, rootAgentId } = payload; const opensearchClient = context.core.opensearch.client.asCurrentUser; if (payload.sessionId) { @@ -39,8 +34,6 @@ export class OllyChatService implements ChatService { } try { - const runs: Run[] = []; - /** * Wait for an API to fetch root agent id. */ @@ -66,17 +59,15 @@ export class OllyChatService implements ChatService { output: Array<{ name: string; result?: string }>; }>; }>; - const outputBody = - agentFrameworkResponse.body.inference_results?.[0]?.output || - agentFrameworkResponse.body.inference_results?.[0]?.output; + const outputBody = agentFrameworkResponse.body.inference_results?.[0]?.output; const memoryIdItem = outputBody?.find((item) => item.name === MEMORY_ID_FIELD); - const reversedOutputBody = [...outputBody].reverse(); - const finalAnswerItem = reversedOutputBody.find((item) => item.name === RESPONSE_FIELD); - - const agentFrameworkAnswer = finalAnswerItem?.result || ''; return { - messages: buildOutputs(input.content, agentFrameworkAnswer, '', {}, convertToTraces(runs)), + /** + * Interactions will be stored in Agent framework, + * thus we do not need to return the latest message back. + */ + messages: [], memoryId: memoryIdItem?.result || '', }; } catch (error) { diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts index 05165acc..3448fd67 100644 --- a/server/services/storage/agent_framework_storage_service.ts +++ b/server/services/storage/agent_framework_storage_service.ts @@ -5,7 +5,6 @@ import { ApiResponse } from '@opensearch-project/opensearch/.'; import { OpenSearchClient } from '../../../../../src/core/server'; -import { LLM_INDEX } from '../../../common/constants/llm'; import { IInput, IMessage, @@ -15,44 +14,47 @@ import { } from '../../../common/types/chat_saved_object_attributes'; import { GetSessionsSchema } from '../../routes/chat_routes'; import { StorageService } from './storage_service'; +import { Interaction, MessageParser } from '../../types'; +import { MessageParserRunner } from '../../utils/message_parser_runner'; export class AgentFrameworkStorageService implements StorageService { - constructor(private readonly client: OpenSearchClient) {} + constructor( + private readonly client: OpenSearchClient, + private readonly messageParsers: MessageParser[] = [] + ) {} async getSession(sessionId: string): Promise { const session = (await this.client.transport.request({ method: 'GET', path: `/_plugins/_ml/memory/conversation/${sessionId}/_list`, })) as ApiResponse<{ - interactions: Array<{ - input: string; - response: string; - parent_interaction_id: string; - interaction_id: string; - }>; + interactions: Interaction[]; }>; + const messageParserRunner = new MessageParserRunner(this.messageParsers); + const finalInteractions: Interaction[] = [...session.body.interactions]; + + /** + * Sort interactions according to create_time + */ + finalInteractions.sort((interactionA, interactionB) => { + const { create_time: createTimeA } = interactionA; + const { create_time: createTimeB } = interactionB; + const createTimeMSA = +new Date(createTimeA); + const createTimeMSB = +new Date(createTimeB); + if (isNaN(createTimeMSA) || isNaN(createTimeMSB)) { + return 0; + } + return createTimeMSA - createTimeMSB; + }); + let finalMessages: IMessage[] = []; + for (const interaction of finalInteractions) { + finalMessages = [...finalMessages, ...(await messageParserRunner.run(interaction))]; + } return { title: 'test', version: 1, createdTimeMs: Date.now(), updatedTimeMs: Date.now(), - messages: session.body.interactions - .filter((item) => !item.parent_interaction_id) - .reduce((total, current) => { - const inputItem: IInput = { - type: 'input', - contentType: 'text', - content: current.input, - }; - const outputItems: IOutput[] = [ - { - type: 'output', - contentType: 'markdown', - content: current.response, - traceId: current.interaction_id, - }, - ]; - return [...total, inputItem, ...outputItems]; - }, [] as IMessage[]), + messages: finalMessages, }; } diff --git a/server/types.ts b/server/types.ts index 625f51ba..9d9e7520 100644 --- a/server/types.ts +++ b/server/types.ts @@ -18,6 +18,7 @@ export interface Interaction { interaction_id: string; create_time: string; additional_info: Record; + parent_interaction_id: string; } export interface MessageParser { From 15ea29b5a1e011b22ea759da386199a892058610 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 27 Nov 2023 13:20:29 +0800 Subject: [PATCH 25/31] Add interaction into message props (#12) * feat: add mechannism to register messageParser Signed-off-by: SuZhou-Joe * feat: update Signed-off-by: SuZhou-Joe * feat: add interaction into message_bubble.tsx Signed-off-by: SuZhou-Joe * feat: update CHANGELOG Signed-off-by: SuZhou-Joe * fix: lint checker Signed-off-by: SuZhou-Joe * feat: remove useless code Signed-off-by: SuZhou-Joe * feat: update Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- CHANGELOG.md | 3 +- common/types/chat_saved_object_attributes.ts | 11 +++++++ public/hooks/use_chat_actions.tsx | 31 ++++++++++++++++--- public/hooks/use_chat_state.tsx | 15 +++++++-- public/tabs/chat/chat_page.tsx | 8 ++++- public/tabs/chat/chat_page_content.tsx | 14 ++++++++- public/tabs/chat/messages/message_bubble.tsx | 7 ++++- .../__tests__/build_outputs.test.ts | 4 +-- server/parsers/basic_input_output_parser.ts | 5 ++- server/routes/chat_routes.ts | 1 + .../agent_framework_storage_service.ts | 6 ++-- .../storage/saved_objects_storage_service.ts | 6 +++- server/types.ts | 12 +------ server/utils/message_parser_runner.test.ts | 15 +++++++++ server/utils/message_parser_runner.ts | 4 +-- 15 files changed, 109 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78c3fdb1..055c2ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,4 +5,5 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 📈 Features/Enhancements - Add support for registerMessageParser ([#5](https://github.com/opensearch-project/dashboards-assistant/pull/5)) -- Change implementation of basic_input_output to built-in parser ([#10](https://github.com/opensearch-project/dashboards-assistant/pull/10)) \ No newline at end of file +- Change implementation of basic_input_output to built-in parser ([#10](https://github.com/opensearch-project/dashboards-assistant/pull/10)) +- Add interactions into ChatState and pass specific interaction into message_bubble ([#12](https://github.com/opensearch-project/dashboards-assistant/pull/12)) \ No newline at end of file diff --git a/common/types/chat_saved_object_attributes.ts b/common/types/chat_saved_object_attributes.ts index fb3e0d2a..0421cd54 100644 --- a/common/types/chat_saved_object_attributes.ts +++ b/common/types/chat_saved_object_attributes.ts @@ -6,12 +6,23 @@ export const CHAT_SAVED_OBJECT = 'assistant-chat'; export const SAVED_OBJECT_VERSION = 1; +export interface Interaction { + input: string; + response: string; + conversation_id: string; + interaction_id: string; + create_time: string; + additional_info: Record; + parent_interaction_id?: string; +} + export interface ISession { title: string; version: number; createdTimeMs: number; updatedTimeMs: number; messages: IMessage[]; + interactions: Interaction[]; } export interface ISessionFindResponse { diff --git a/public/hooks/use_chat_actions.tsx b/public/hooks/use_chat_actions.tsx index 6561a89d..2dd26964 100644 --- a/public/hooks/use_chat_actions.tsx +++ b/public/hooks/use_chat_actions.tsx @@ -4,7 +4,11 @@ */ import { ASSISTANT_API } from '../../common/constants/llm'; -import { IMessage, ISuggestedAction } from '../../common/types/chat_saved_object_attributes'; +import { + IMessage, + ISuggestedAction, + Interaction, +} from '../../common/types/chat_saved_object_attributes'; import { useChatContext } from '../contexts/chat_context'; import { useCore } from '../contexts/core_context'; import { AssistantActions } from '../types'; @@ -14,6 +18,7 @@ interface SendResponse { sessionId: string; title: string; messages: IMessage[]; + interactions: Interaction[]; } interface SetParagraphResponse { @@ -56,7 +61,13 @@ export const useChatActions = (): AssistantActions => { if (!chatContext.title) { chatContext.setTitle(response.title); } - chatStateDispatch({ type: 'receive', payload: response.messages }); + chatStateDispatch({ + type: 'receive', + payload: { + messages: response.messages, + interactions: response.interactions, + }, + }); } catch (error) { if (abortController.signal.aborted) return; chatStateDispatch({ type: 'error', payload: error }); @@ -79,7 +90,13 @@ export const useChatActions = (): AssistantActions => { } const session = await core.services.sessionLoad.load(sessionId); if (session) { - chatStateDispatch({ type: 'receive', payload: session.messages }); + chatStateDispatch({ + type: 'receive', + payload: { + messages: session.messages, + interactions: session.interactions, + }, + }); } }; @@ -156,7 +173,13 @@ export const useChatActions = (): AssistantActions => { if (abortController.signal.aborted) { return; } - chatStateDispatch({ type: 'receive', payload: response.messages }); + chatStateDispatch({ + type: 'receive', + payload: { + messages: response.messages, + interactions: response.interactions, + }, + }); } catch (error) { if (abortController.signal.aborted) { return; diff --git a/public/hooks/use_chat_state.tsx b/public/hooks/use_chat_state.tsx index 9f3a66f5..13bb8542 100644 --- a/public/hooks/use_chat_state.tsx +++ b/public/hooks/use_chat_state.tsx @@ -5,10 +5,11 @@ import { produce } from 'immer'; import React, { useContext, useMemo, useReducer } from 'react'; -import { IMessage } from '../../common/types/chat_saved_object_attributes'; +import { IMessage, Interaction } from '../../common/types/chat_saved_object_attributes'; interface ChatState { messages: IMessage[]; + interactions: Interaction[]; llmResponding: boolean; llmError?: Error; } @@ -18,7 +19,13 @@ type ChatStateAction = | { type: 'abort' } | { type: 'reset' } | { type: 'send'; payload: IMessage } - | { type: 'receive'; payload: ChatState['messages'] } + | { + type: 'receive'; + payload: { + messages: ChatState['messages']; + interactions: ChatState['interactions']; + }; + } | { type: 'error'; payload: NonNullable | { body: NonNullable }; @@ -31,6 +38,7 @@ interface IChatStateContext { const ChatStateContext = React.createContext(null); const initialState: ChatState = { + interactions: [], messages: [], llmResponding: false, }; @@ -48,7 +56,8 @@ const chatStateReducer: React.Reducer = (state, acti break; case 'receive': - draft.messages = action.payload; + draft.messages = action.payload.messages; + draft.interactions = action.payload.interactions; draft.llmResponding = false; draft.llmError = undefined; break; diff --git a/public/tabs/chat/chat_page.tsx b/public/tabs/chat/chat_page.tsx index f8101d2b..025ed2b1 100644 --- a/public/tabs/chat/chat_page.tsx +++ b/public/tabs/chat/chat_page.tsx @@ -31,7 +31,13 @@ export const ChatPage: React.FC = (props) => { } const session = await core.services.sessionLoad.load(chatContext.sessionId); if (session) { - chatStateDispatch({ type: 'receive', payload: session.messages }); + chatStateDispatch({ + type: 'receive', + payload: { + messages: session.messages, + interactions: session.interactions, + }, + }); } }, [chatContext.sessionId, chatStateDispatch]); diff --git a/public/tabs/chat/chat_page_content.tsx b/public/tabs/chat/chat_page_content.tsx index 16ebc5b7..359318db 100644 --- a/public/tabs/chat/chat_page_content.tsx +++ b/public/tabs/chat/chat_page_content.tsx @@ -14,7 +14,11 @@ import { EuiText, } from '@elastic/eui'; import React, { useLayoutEffect, useRef } from 'react'; -import { IMessage, ISuggestedAction } from '../../../common/types/chat_saved_object_attributes'; +import { + IMessage, + ISuggestedAction, + Interaction, +} from '../../../common/types/chat_saved_object_attributes'; import { TermsAndConditions } from '../../components/terms_and_conditions'; import { useChatContext } from '../../contexts/chat_context'; import { useChatState } from '../../hooks/use_chat_state'; @@ -120,6 +124,13 @@ export const ChatPageContent: React.FC = React.memo((props // Only show suggestion on llm outputs after last user input const showSuggestions = i > lastInputIndex; + let interaction: Interaction | undefined; + if (message.type === 'output' && message.traceId) { + interaction = chatState.interactions.find( + (item) => item.interaction_id === message.traceId + ); + } + return ( @@ -129,6 +140,7 @@ export const ChatPageContent: React.FC = React.memo((props showRegenerate={isLatestOutput} shouldActionBarVisibleOnHover={!isLatestOutput} onRegenerate={chatActions.regenerate} + interaction={interaction} > {/* */} diff --git a/public/tabs/chat/messages/message_bubble.tsx b/public/tabs/chat/messages/message_bubble.tsx index 451e36af..4c0133ac 100644 --- a/public/tabs/chat/messages/message_bubble.tsx +++ b/public/tabs/chat/messages/message_bubble.tsx @@ -19,7 +19,11 @@ import React, { useCallback } from 'react'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import cx from 'classnames'; import chatIcon from '../../../assets/chat.svg'; -import { IMessage, IOutput } from '../../../../common/types/chat_saved_object_attributes'; +import { + IMessage, + IOutput, + Interaction, +} from '../../../../common/types/chat_saved_object_attributes'; import { useFeedback } from '../../../hooks/use_feed_back'; type MessageBubbleProps = { @@ -30,6 +34,7 @@ type MessageBubbleProps = { } & ( | { message: IMessage; + interaction?: Interaction; } | { loading: boolean; diff --git a/server/olly/utils/output_builders/__tests__/build_outputs.test.ts b/server/olly/utils/output_builders/__tests__/build_outputs.test.ts index c796f35a..77815a06 100644 --- a/server/olly/utils/output_builders/__tests__/build_outputs.test.ts +++ b/server/olly/utils/output_builders/__tests__/build_outputs.test.ts @@ -35,7 +35,7 @@ describe('build outputs', () => { it('sanitizes markdown outputs', () => { const outputs = buildOutputs( 'test question', - 'normal text image !!!!!!![](https://badurl) ![image](https://badurl) [good link](https://link)', + 'normal text image !!!!!!![](http://evil.com/) ![image](http://evil.com/) [good link](https://link)', 'test-session', {}, [] @@ -43,7 +43,7 @@ describe('build outputs', () => { expect(outputs).toEqual([ { content: - 'normal text [](https://badurl) [image](https://badurl) [good link](https://link)', + 'normal text [](http://evil.com/) [image](http://evil.com/) [good link](https://link)', contentType: 'markdown', traceId: 'test-session', suggestedActions: [], diff --git a/server/parsers/basic_input_output_parser.ts b/server/parsers/basic_input_output_parser.ts index 7880e4e4..7febe7b7 100644 --- a/server/parsers/basic_input_output_parser.ts +++ b/server/parsers/basic_input_output_parser.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IInput, IOutput } from '../../common/types/chat_saved_object_attributes'; -import { Interaction } from '../types'; +import { IInput, IOutput, Interaction } from '../../common/types/chat_saved_object_attributes'; export const BasicInputOutputParser = { order: 0, @@ -20,7 +19,7 @@ export const BasicInputOutputParser = { type: 'output', contentType: 'markdown', content: interaction.response, - traceId: interaction.parent_interaction_id, + traceId: interaction.interaction_id, }, ]; return [inputItem, ...outputItems]; diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index 946df488..632babe4 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -137,6 +137,7 @@ export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) messages: finalMessage.messages, sessionId: outputs.memoryId, title: finalMessage.title, + interactions: finalMessage.interactions, }, }); } catch (error) { diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts index 3448fd67..5393105e 100644 --- a/server/services/storage/agent_framework_storage_service.ts +++ b/server/services/storage/agent_framework_storage_service.ts @@ -6,15 +6,14 @@ import { ApiResponse } from '@opensearch-project/opensearch/.'; import { OpenSearchClient } from '../../../../../src/core/server'; import { - IInput, IMessage, - IOutput, ISession, ISessionFindResponse, + Interaction, } from '../../../common/types/chat_saved_object_attributes'; import { GetSessionsSchema } from '../../routes/chat_routes'; import { StorageService } from './storage_service'; -import { Interaction, MessageParser } from '../../types'; +import { MessageParser } from '../../types'; import { MessageParserRunner } from '../../utils/message_parser_runner'; export class AgentFrameworkStorageService implements StorageService { @@ -55,6 +54,7 @@ export class AgentFrameworkStorageService implements StorageService { createdTimeMs: Date.now(), updatedTimeMs: Date.now(), messages: finalMessages, + interactions: finalInteractions, }; } diff --git a/server/services/storage/saved_objects_storage_service.ts b/server/services/storage/saved_objects_storage_service.ts index 78fcffb4..f85bba48 100644 --- a/server/services/storage/saved_objects_storage_service.ts +++ b/server/services/storage/saved_objects_storage_service.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { MessageParser } from '../../types'; import { SavedObjectsClientContract } from '../../../../../src/core/server'; import { CHAT_SAVED_OBJECT, @@ -15,7 +16,10 @@ import { GetSessionsSchema } from '../../routes/chat_routes'; import { StorageService } from './storage_service'; export class SavedObjectsStorageService implements StorageService { - constructor(private readonly client: SavedObjectsClientContract) {} + constructor( + private readonly client: SavedObjectsClientContract, + private readonly messageParsers: MessageParser[] + ) {} private convertUpdatedTimeField(updatedAt: string | undefined) { return updatedAt ? new Date(updatedAt).getTime() : undefined; diff --git a/server/types.ts b/server/types.ts index 9d9e7520..5b692036 100644 --- a/server/types.ts +++ b/server/types.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IMessage } from '../common/types/chat_saved_object_attributes'; +import { IMessage, Interaction } from '../common/types/chat_saved_object_attributes'; import { ILegacyClusterClient, Logger } from '../../../src/core/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -11,16 +11,6 @@ export interface AssistantPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AssistantPluginStart {} -export interface Interaction { - input: string; - response: string; - conversation_id: string; - interaction_id: string; - create_time: string; - additional_info: Record; - parent_interaction_id: string; -} - export interface MessageParser { /** * The id of the parser, should be unique among the parsers. diff --git a/server/utils/message_parser_runner.test.ts b/server/utils/message_parser_runner.test.ts index e931f47a..ca4032a6 100644 --- a/server/utils/message_parser_runner.test.ts +++ b/server/utils/message_parser_runner.test.ts @@ -26,6 +26,11 @@ describe('MessageParserRunner', () => { await messageParserRunner.run({ response: 'output', input: 'input', + conversation_id: '', + interaction_id: '', + create_time: '', + additional_info: {}, + parent_interaction_id: '' }) ).toEqual([ { @@ -95,6 +100,11 @@ describe('MessageParserRunner', () => { await messageParserRunner.run({ response: 'output', input: 'input', + conversation_id: '', + interaction_id: '', + create_time: '', + additional_info: {}, + parent_interaction_id: '' }) ).toEqual([ { @@ -144,6 +154,11 @@ describe('MessageParserRunner', () => { await messageParserRunner.run({ response: 'output', input: 'input', + conversation_id: '', + interaction_id: '', + create_time: '', + additional_info: {}, + parent_interaction_id: '' }) ).toEqual([]); }); diff --git a/server/utils/message_parser_runner.ts b/server/utils/message_parser_runner.ts index 2f5d7d59..60534247 100644 --- a/server/utils/message_parser_runner.ts +++ b/server/utils/message_parser_runner.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IMessage } from '../../common/types/chat_saved_object_attributes'; -import { Interaction, MessageParser } from '../types'; +import { IMessage, Interaction } from '../../common/types/chat_saved_object_attributes'; +import { MessageParser } from '../types'; export class MessageParserRunner { constructor(private readonly messageParsers: MessageParser[]) {} From 979404dc96d37f6dbb04d41638f08aeb3ce08b2a Mon Sep 17 00:00:00 2001 From: gaobinlong Date: Tue, 28 Nov 2023 10:46:59 +0800 Subject: [PATCH 26/31] Integrate Memeory APIs of agent framework (#15) * Integrate Memeory APIs of agent framework Signed-off-by: gaobinlong * Add TODO to getSessions() Signed-off-by: gaobinlong --------- Signed-off-by: gaobinlong --- .../agent_framework_storage_service.ts | 118 +++++++++++++++++- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts index 5393105e..d80f5b7e 100644 --- a/server/services/storage/agent_framework_storage_service.ts +++ b/server/services/storage/agent_framework_storage_service.ts @@ -16,6 +16,12 @@ import { StorageService } from './storage_service'; import { MessageParser } from '../../types'; import { MessageParserRunner } from '../../utils/message_parser_runner'; +export interface SessionOptResponse { + success: boolean; + statusCode?: number | null; + message?: string; +} + export class AgentFrameworkStorageService implements StorageService { constructor( private readonly client: OpenSearchClient, @@ -58,8 +64,67 @@ export class AgentFrameworkStorageService implements StorageService { }; } + // TODO: return real update_time in the response once the agent framework supports update_time field async getSessions(query: GetSessionsSchema): Promise { - throw new Error('Method not implemented.'); + let sortField = ''; + if (query.sortField === 'updatedTimeMs') { + sortField = 'create_time'; + } + let searchFields: string[] = []; + if (query.search && query.searchFields) { + if (typeof query.searchFields === 'string') { + searchFields = [...searchFields, query.searchFields.replace('title', 'name')]; + } else { + searchFields = query.searchFields.map((item) => item.replace('title', 'name')); + } + } + + const requestParams = { + from: (query.page - 1) * query.perPage, + size: query.perPage, + ...(searchFields.length > 0 && { + query: { + multi_match: { + query: query.search, + fields: searchFields, + }, + }, + }), + ...(searchFields.length === 0 && { + query: { + match_all: {}, + }, + }), + ...(sortField && query.sortOrder && { sort: [{ [sortField]: query.sortOrder }] }), + }; + + const sessions = await this.client.transport.request({ + method: 'GET', + path: `/_plugins/_ml/memory/conversation/_search`, + body: requestParams, + }); + + return { + objects: sessions.body.hits.hits + .filter( + (hit: { + _source: { name: string; create_time: string }; + }): hit is RequiredKey => + hit._source !== null && hit._source !== undefined + ) + .map((item: { _id: string; _source: { name: string; create_time: string } }) => ({ + id: item._id, + title: item._source.name, + version: 1, + createdTimeMs: Date.parse(item._source.create_time), + updatedTimeMs: Date.parse(item._source.create_time), + messages: [] as IMessage[], + })), + total: + typeof sessions.body.hits.total === 'number' + ? sessions.body.hits.total + : sessions.body.hits.total.value, + }; } async saveMessages( @@ -67,12 +132,53 @@ export class AgentFrameworkStorageService implements StorageService { sessionId: string | undefined, messages: IMessage[] ): Promise<{ sessionId: string; messages: IMessage[] }> { - throw new Error('Method not implemented.'); + throw new Error('Method is not needed'); } - deleteSession(sessionId: string): Promise<{}> { - throw new Error('Method not implemented.'); + + async deleteSession(sessionId: string): Promise { + try { + const response = await this.client.transport.request({ + method: 'DELETE', + path: `/_plugins/_ml/memory/conversation/${sessionId}/_delete`, + }); + if (response.statusCode === 200) { + return { + success: true, + }; + } else { + return { + success: false, + statusCode: response.statusCode, + message: JSON.stringify(response.body), + }; + } + } catch (error) { + throw new Error('delete converstaion failed, reason:' + JSON.stringify(error.meta?.body)); + } } - updateSession(sessionId: string, title: string): Promise<{}> { - throw new Error('Method not implemented.'); + + async updateSession(sessionId: string, title: string): Promise { + try { + const response = await this.client.transport.request({ + method: 'PUT', + path: `/_plugins/_ml/memory/conversation/${sessionId}/_update`, + body: { + name: title, + }, + }); + if (response.statusCode === 200) { + return { + success: true, + }; + } else { + return { + success: false, + statusCode: response.statusCode, + message: JSON.stringify(response.body), + }; + } + } catch (error) { + throw new Error('update converstaion failed, reason:' + JSON.stringify(error.meta?.body)); + } } } From 37eaa5c7dfbfc1ca59f2e7d407d403426c41f853 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 29 Nov 2023 11:22:28 +0800 Subject: [PATCH 27/31] feat: add visualization card in customized parser (#16) * feat: add implement visualization in customized parser Signed-off-by: SuZhou-Joe * feat: add csv-parser lib Signed-off-by: SuZhou-Joe * feat: add csv-parser lib Signed-off-by: SuZhou-Joe * feat: add test cases Signed-off-by: SuZhou-Joe * feat: optimize Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- common/types/chat_saved_object_attributes.ts | 2 +- package.json | 1 + .../parsers/basic_input_output_parser.test.ts | 32 +++++ .../parsers/visualization_card_parser.test.ts | 116 ++++++++++++++++++ server/parsers/visualization_card_parser.ts | 48 ++++++++ server/plugin.ts | 3 +- server/utils/csv-parser-helper.test.ts | 21 ++++ server/utils/csv-parser-helper.ts | 25 ++++ yarn.lock | 9 +- 9 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 server/parsers/basic_input_output_parser.test.ts create mode 100644 server/parsers/visualization_card_parser.test.ts create mode 100644 server/parsers/visualization_card_parser.ts create mode 100644 server/utils/csv-parser-helper.test.ts create mode 100644 server/utils/csv-parser-helper.ts diff --git a/common/types/chat_saved_object_attributes.ts b/common/types/chat_saved_object_attributes.ts index 0421cd54..acfc8d71 100644 --- a/common/types/chat_saved_object_attributes.ts +++ b/common/types/chat_saved_object_attributes.ts @@ -12,7 +12,7 @@ export interface Interaction { conversation_id: string; interaction_id: string; create_time: string; - additional_info: Record; + additional_info?: Record; parent_interaction_id?: string; } diff --git a/package.json b/package.json index 1aaf6fab..d79d30a9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "autosize": "^6.0.1", + "csv-parser": "^3.0.0", "dompurify": "^2.4.1", "jsdom": "^22.1.0", "langchain": "^0.0.164", diff --git a/server/parsers/basic_input_output_parser.test.ts b/server/parsers/basic_input_output_parser.test.ts new file mode 100644 index 00000000..9f72f941 --- /dev/null +++ b/server/parsers/basic_input_output_parser.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BasicInputOutputParser } from './basic_input_output_parser'; + +describe('BasicInputOutputParser', () => { + it('return input and output', async () => { + expect( + await BasicInputOutputParser.parserProvider({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + }) + ).toEqual([ + { + type: 'input', + contentType: 'text', + content: 'input', + }, + { + type: 'output', + contentType: 'markdown', + content: 'response', + traceId: 'interaction_id', + }, + ]); + }); +}); diff --git a/server/parsers/visualization_card_parser.test.ts b/server/parsers/visualization_card_parser.test.ts new file mode 100644 index 00000000..63fd66fe --- /dev/null +++ b/server/parsers/visualization_card_parser.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisualizationCardParser } from './visualization_card_parser'; + +describe('VisualizationCardParser', () => { + it('return visualizations when there is VisualizationTool.output', async () => { + expect( + await VisualizationCardParser.parserProvider({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: { + 'VisualizationTool.output': [ + 'row_number,Id,title\n' + + '1,id1,[Flights] Total Flights\n' + + '2,id2,[Flights] Controls\n' + + '3,id3,[Flights] Airline Carrier', + ], + }, + }) + ).toEqual([ + { + content: 'id1', + contentType: 'visualization', + suggestedActions: [{ actionType: 'view_in_dashboards', message: 'View in Visualize' }], + type: 'output', + }, + { + content: 'id2', + contentType: 'visualization', + suggestedActions: [{ actionType: 'view_in_dashboards', message: 'View in Visualize' }], + type: 'output', + }, + { + content: 'id3', + contentType: 'visualization', + suggestedActions: [{ actionType: 'view_in_dashboards', message: 'View in Visualize' }], + type: 'output', + }, + ]); + }); + + it('return visualizations when there are multiple VisualizationTool.outputs', async () => { + expect( + await VisualizationCardParser.parserProvider({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: { + 'VisualizationTool.output': [ + 'row_number,Id,title\n' + '1,id1,[Flights] Total Flights\n', + 'row_number,Id,title\n' + '2,id2,[Flights] Controls\n', + ], + }, + }) + ).toEqual([ + { + content: 'id1', + contentType: 'visualization', + suggestedActions: [{ actionType: 'view_in_dashboards', message: 'View in Visualize' }], + type: 'output', + }, + { + content: 'id2', + contentType: 'visualization', + suggestedActions: [{ actionType: 'view_in_dashboards', message: 'View in Visualize' }], + type: 'output', + }, + ]); + }); + + it('do not return visualizations when VisualizationTool.output is null', async () => { + expect( + await VisualizationCardParser.parserProvider({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: {}, + }) + ).toEqual([]); + }); + + it('do not return visualizations when VisualizationTool.output is not in correct format', async () => { + expect( + await VisualizationCardParser.parserProvider({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: { + 'VisualizationTool.output': [ + 'row_number\n' + '1', + 'row_number,Id,title\n' + '2,id2,[Flights] Controls\n', + ], + }, + }) + ).toEqual([ + { + content: 'id2', + contentType: 'visualization', + suggestedActions: [{ actionType: 'view_in_dashboards', message: 'View in Visualize' }], + type: 'output', + }, + ]); + }); +}); diff --git a/server/parsers/visualization_card_parser.ts b/server/parsers/visualization_card_parser.ts new file mode 100644 index 00000000..b6afb731 --- /dev/null +++ b/server/parsers/visualization_card_parser.ts @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IMessage, Interaction } from '../../common/types/chat_saved_object_attributes'; +import { getJsonFromString } from '../utils/csv-parser-helper'; + +const extractIdsFromCsvString = async (csv: string) => { + const lines = (await getJsonFromString(csv)) as Array<{ Id: string }>; + return lines + .map((line) => line.Id) + .filter((v: T | null | undefined): v is T => v !== null && v !== undefined); +}; + +export const VisualizationCardParser = { + id: 'core_visualization', + async parserProvider(interaction: Interaction) { + const visualizationOutputs = interaction.additional_info?.['VisualizationTool.output'] as + | string[] + | undefined; + if (!visualizationOutputs) { + return []; + } + const visualizationIds = ( + await Promise.all(visualizationOutputs.map((output) => extractIdsFromCsvString(output))) + ).flatMap((id) => id); + + const visOutputs: IMessage[] = visualizationIds + /** + * Empty id will be filtered + */ + .filter((id) => id) + .map((id) => ({ + type: 'output', + content: id, + contentType: 'visualization', + suggestedActions: [ + { + message: 'View in Visualize', + actionType: 'view_in_dashboards', + }, + ], + })); + + return visOutputs; + }, +}; diff --git a/server/plugin.ts b/server/plugin.ts index 745e3b0c..521d855c 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -17,13 +17,13 @@ import { import { OpenSearchAlertingPlugin } from './adaptors/opensearch_alerting_plugin'; import { OpenSearchObservabilityPlugin } from './adaptors/opensearch_observability_plugin'; import { PPLPlugin } from './adaptors/ppl_plugin'; -import { AssistantServerConfig } from './config/schema'; import './fetch-polyfill'; import { setupRoutes } from './routes/index'; import { chatSavedObject } from './saved_objects/chat_saved_object'; import { AssistantPluginSetup, AssistantPluginStart, MessageParser } from './types'; import { chatConfigSavedObject } from './saved_objects/chat_config_saved_object'; import { BasicInputOutputParser } from './parsers/basic_input_output_parser'; +import { VisualizationCardParser } from './parsers/visualization_card_parser'; export class AssistantPlugin implements Plugin { private readonly logger: Logger; @@ -79,6 +79,7 @@ export class AssistantPlugin implements Plugin { + it('return correct answer', async () => { + expect(await getJsonFromString('title,id\n1,2')).toEqual([ + { + title: '1', + id: '2', + }, + ]); + }); + + it('return empty array when string is not in correct format', async () => { + expect(await getJsonFromString('1,2')).toEqual([]); + }); +}); diff --git a/server/utils/csv-parser-helper.ts b/server/utils/csv-parser-helper.ts new file mode 100644 index 00000000..690d4aee --- /dev/null +++ b/server/utils/csv-parser-helper.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Readable } from 'stream'; +import csvParser from 'csv-parser'; + +export const getJsonFromString = ( + csvString: string, + options?: csvParser.Options +): Promise> | string[][]> => { + const results: string[][] | Array> = []; + return new Promise((resolve, reject) => { + Readable.from(csvString) + .pipe(csvParser(options)) + .on('data', (data) => results.push(data)) + .on('end', () => { + resolve(results); + }) + .on('error', (err) => { + reject(err); + }); + }); +}; diff --git a/yarn.lock b/yarn.lock index 3f22bbf1..290e4f09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -545,6 +545,13 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmmirror.com/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + data-urls@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4" @@ -1354,7 +1361,7 @@ minimatch@^3.0.4, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== From 560daa72a492dd4b848e30535899882e05167e5f Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 30 Nov 2023 13:46:38 +0800 Subject: [PATCH 28/31] feat: sanitize content of basic_output (#24) Signed-off-by: SuZhou-Joe --- .../parsers/basic_input_output_parser.test.ts | 26 +++++++++++++++++++ server/parsers/basic_input_output_parser.ts | 10 ++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/server/parsers/basic_input_output_parser.test.ts b/server/parsers/basic_input_output_parser.test.ts index 9f72f941..6570cc47 100644 --- a/server/parsers/basic_input_output_parser.test.ts +++ b/server/parsers/basic_input_output_parser.test.ts @@ -29,4 +29,30 @@ describe('BasicInputOutputParser', () => { }, ]); }); + + it('sanitizes markdown outputs', async () => { + const outputs = await BasicInputOutputParser.parserProvider({ + input: 'test question', + response: + 'normal text image !!!!!!![](http://evil.com/) ![image](http://evil.com/) [good link](https://link)', + conversation_id: 'test-session', + interaction_id: 'interaction_id', + create_time: '', + }); + + expect(outputs).toEqual([ + { + type: 'input', + contentType: 'text', + content: 'test question', + }, + { + content: + 'normal text [](http://evil.com/) [image](http://evil.com/) [good link](https://link)', + contentType: 'markdown', + traceId: 'interaction_id', + type: 'output', + }, + ]); + }); }); diff --git a/server/parsers/basic_input_output_parser.ts b/server/parsers/basic_input_output_parser.ts index 7febe7b7..c1769059 100644 --- a/server/parsers/basic_input_output_parser.ts +++ b/server/parsers/basic_input_output_parser.ts @@ -3,8 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ +import createDOMPurify from 'dompurify'; +import { JSDOM } from 'jsdom'; import { IInput, IOutput, Interaction } from '../../common/types/chat_saved_object_attributes'; +const sanitize = (content: string) => { + const window = new JSDOM('').window; + const DOMPurify = createDOMPurify((window as unknown) as Window); + return DOMPurify.sanitize(content, { FORBID_TAGS: ['img'] }).replace(/!+\[/g, '['); +}; + export const BasicInputOutputParser = { order: 0, id: 'output_message', @@ -18,7 +26,7 @@ export const BasicInputOutputParser = { { type: 'output', contentType: 'markdown', - content: interaction.response, + content: sanitize(interaction.response), traceId: interaction.interaction_id, }, ]; From a5c20252dc4aeffb335c31c3751c0a32971e352f Mon Sep 17 00:00:00 2001 From: gaobinlong Date: Thu, 30 Nov 2023 17:53:44 +0800 Subject: [PATCH 29/31] Integrate list traces api of agent framework (#25) * Integrate list traces api of agent framework Signed-off-by: gaobinlong * Fix import issue Signed-off-by: gaobinlong * Fix some issues Signed-off-by: gaobinlong * Add some comments Signed-off-by: gaobinlong --------- Signed-off-by: gaobinlong --- common/constants/llm.ts | 1 + common/utils/llm_chat/traces.ts | 10 ++ public/chat_flyout.tsx | 11 ++- public/chat_header_button.tsx | 3 +- public/components/agent_framework_traces.tsx | 92 +++++++++++++++++++ .../agent_framework_traces_flyout_body.tsx | 73 +++++++++++++++ public/hooks/use_chat_actions.tsx | 7 +- .../hooks/use_fetch_agentframework_traces.ts | 39 ++++++++ public/tabs/chat/messages/message_bubble.tsx | 2 + public/tabs/chat/messages/message_footer.tsx | 9 +- public/tabs/chat_window_header.tsx | 5 +- public/tabs/history/chat_history_page.tsx | 9 +- public/utils/constants.ts | 12 +++ server/routes/chat_routes.ts | 28 ++++++ server/services/chat/olly_chat_service.ts | 1 - .../agent_framework_storage_service.ts | 33 +++++++ 16 files changed, 314 insertions(+), 21 deletions(-) create mode 100644 public/components/agent_framework_traces.tsx create mode 100644 public/components/agent_framework_traces_flyout_body.tsx create mode 100644 public/hooks/use_fetch_agentframework_traces.ts create mode 100644 public/utils/constants.ts diff --git a/common/constants/llm.ts b/common/constants/llm.ts index 53029273..8a68dba2 100644 --- a/common/constants/llm.ts +++ b/common/constants/llm.ts @@ -18,6 +18,7 @@ export const ASSISTANT_API = { FEEDBACK: `${API_BASE}/feedback`, ABORT_AGENT_EXECUTION: `${API_BASE}/abort`, REGENERATE: `${API_BASE}/regenerate`, + TRACE: `${API_BASE}/trace`, } as const; export const LLM_INDEX = { diff --git a/common/utils/llm_chat/traces.ts b/common/utils/llm_chat/traces.ts index 8ac0d314..73ee3088 100644 --- a/common/utils/llm_chat/traces.ts +++ b/common/utils/llm_chat/traces.ts @@ -7,6 +7,16 @@ import { Run } from 'langchain/callbacks'; import { AgentRun } from 'langchain/dist/callbacks/handlers/tracer'; import _ from 'lodash'; +export interface AgentFrameworkTrace { + interactionId: string; + parentInteractionId: string; + createTime: string; + input: string; + output: string; + origin: string; + traceNumber: number; +} + export interface LangchainTrace { id: Run['id']; parentRunId?: Run['parent_run_id']; diff --git a/public/chat_flyout.tsx b/public/chat_flyout.tsx index 04e9c34b..42d6e32c 100644 --- a/public/chat_flyout.tsx +++ b/public/chat_flyout.tsx @@ -10,7 +10,8 @@ import { useChatContext } from './contexts/chat_context'; import { ChatPage } from './tabs/chat/chat_page'; import { ChatWindowHeader } from './tabs/chat_window_header'; import { ChatHistoryPage } from './tabs/history/chat_history_page'; -import { LangchainTracesFlyoutBody } from './components/langchain_traces_flyout_body'; +import { AgentFrameworkTracesFlyoutBody } from './components/agent_framework_traces_flyout_body'; +import { TAB_ID } from './utils/constants'; let chatHistoryPageLoaded = false; @@ -31,15 +32,15 @@ export const ChatFlyout: React.FC = (props) => { if (!props.overrideComponent) { switch (chatContext.selectedTabId) { - case 'chat': + case TAB_ID.CHAT: chatPageVisible = true; break; - case 'history': + case TAB_ID.HISTORY: chatHistoryPageVisible = true; break; - case 'trace': + case TAB_ID.TRACE: chatTraceVisible = true; break; @@ -134,7 +135,7 @@ export const ChatFlyout: React.FC = (props) => { className={cs({ 'llm-chat-hidden': !chatHistoryPageVisible })} /> )} - {chatTraceVisible && chatContext.traceId && } + {chatTraceVisible && chatContext.traceId && } diff --git a/public/chat_header_button.tsx b/public/chat_header_button.tsx index c90a14c3..78d09eb5 100644 --- a/public/chat_header_button.tsx +++ b/public/chat_header_button.tsx @@ -15,6 +15,7 @@ import { ChatStateProvider } from './hooks/use_chat_state'; import './index.scss'; import chatIcon from './assets/chat.svg'; import { ActionExecutor, AssistantActions, ContentRenderer, UserAccount, TabId } from './types'; +import { TAB_ID } from './utils/constants'; interface HeaderChatButtonProps { application: ApplicationStart; @@ -33,7 +34,7 @@ export const HeaderChatButton: React.FC = (props) => { const [title, setTitle] = useState(); const [flyoutVisible, setFlyoutVisible] = useState(false); const [flyoutComponent, setFlyoutComponent] = useState(null); - const [selectedTabId, setSelectedTabId] = useState('chat'); + const [selectedTabId, setSelectedTabId] = useState(TAB_ID.CHAT); const [preSelectedTabId, setPreSelectedTabId] = useState(undefined); const [traceId, setTraceId] = useState(undefined); const [chatSize, setChatSize] = useState('dock-right'); diff --git a/public/components/agent_framework_traces.tsx b/public/components/agent_framework_traces.tsx new file mode 100644 index 00000000..903b494a --- /dev/null +++ b/public/components/agent_framework_traces.tsx @@ -0,0 +1,92 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiAccordion, + EuiCodeBlock, + EuiEmptyPrompt, + EuiLoadingContent, + EuiSpacer, + EuiText, + EuiMarkdownFormat, + EuiHorizontalRule, +} from '@elastic/eui'; +import React from 'react'; +import { useFetchAgentFrameworkTraces } from '../hooks/use_fetch_agentframework_traces'; + +interface AgentFrameworkTracesProps { + traceId: string; +} + +export const AgentFrameworkTraces: React.FC = (props) => { + const { data: traces, loading, error } = useFetchAgentFrameworkTraces(props.traceId); + + if (loading) { + return ( + <> + Loading... + + + ); + } + if (error) { + return ( + Error loading details} + body={error.toString()} + /> + ); + } + if (!traces?.length) { + return Data not available.; + } + + const question = traces[traces.length - 1].input; + const result = traces[traces.length - 1].output; + const questionAndResult = `# How was this generated +#### Question +${question} +#### Result +${result} +`; + + return ( + <> + {questionAndResult} + + + + +

Response

+
+ {traces + // if origin exists, it indicates that the trace was generated by a tool, we only show the non-empty traces of tools + .filter((trace) => trace.origin && (trace.input || trace.output)) + .map((trace, i) => { + const stepContent = `Step ${i + 1}`; + return ( +
+ + + {trace.input && ( + + Input: {trace.input} + + )} + {trace.output && ( + + Output: {trace.output} + + )} + + +
+ ); + })} + + ); +}; diff --git a/public/components/agent_framework_traces_flyout_body.tsx b/public/components/agent_framework_traces_flyout_body.tsx new file mode 100644 index 00000000..3aefdbfd --- /dev/null +++ b/public/components/agent_framework_traces_flyout_body.tsx @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonEmpty, + EuiFlyoutBody, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageHeader, + EuiButtonIcon, + EuiPageHeaderSection, +} from '@elastic/eui'; +import React from 'react'; +import { useChatContext } from '../contexts/chat_context'; +import { AgentFrameworkTraces } from './agent_framework_traces'; +import { TAB_ID } from '../utils/constants'; + +export const AgentFrameworkTracesFlyoutBody: React.FC = () => { + const chatContext = useChatContext(); + const traceId = chatContext.traceId; + if (!traceId) { + return null; + } + + // docked right or fullscreen with history open + const showBack = !chatContext.flyoutFullScreen || chatContext.preSelectedTabId === TAB_ID.HISTORY; + + return ( + + + + + + {showBack && ( + { + chatContext.setSelectedTabId( + chatContext.flyoutFullScreen ? TAB_ID.HISTORY : TAB_ID.CHAT + ); + }} + iconType="arrowLeft" + > + Back + + )} + + + {!showBack && ( + { + chatContext.setSelectedTabId(TAB_ID.CHAT); + }} + /> + )} + + + + + + + + + ); +}; diff --git a/public/hooks/use_chat_actions.tsx b/public/hooks/use_chat_actions.tsx index 2dd26964..e1975b46 100644 --- a/public/hooks/use_chat_actions.tsx +++ b/public/hooks/use_chat_actions.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { TAB_ID } from '../utils/constants'; import { ASSISTANT_API } from '../../common/constants/llm'; import { IMessage, @@ -52,7 +53,7 @@ export const useChatActions = (): AssistantActions => { !chatContext.sessionId && response.sessionId && core.services.sessions.options?.page === 1 && - chatContext.selectedTabId === 'history' + chatContext.selectedTabId === TAB_ID.HISTORY ) { core.services.sessions.reload(); } @@ -81,7 +82,7 @@ export const useChatActions = (): AssistantActions => { chatContext.setTitle(title); // Chat page will always visible in fullscreen mode, we don't need to change the tab anymore if (!chatContext.flyoutFullScreen) { - chatContext.setSelectedTabId('chat'); + chatContext.setSelectedTabId(TAB_ID.CHAT); } chatContext.setFlyoutComponent(null); if (!sessionId) { @@ -102,7 +103,7 @@ export const useChatActions = (): AssistantActions => { const openChatUI = () => { chatContext.setFlyoutVisible(true); - chatContext.setSelectedTabId('chat'); + chatContext.setSelectedTabId(TAB_ID.CHAT); }; const executeAction = async (suggestedAction: ISuggestedAction, message: IMessage) => { diff --git a/public/hooks/use_fetch_agentframework_traces.ts b/public/hooks/use_fetch_agentframework_traces.ts new file mode 100644 index 00000000..443e0960 --- /dev/null +++ b/public/hooks/use_fetch_agentframework_traces.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useReducer } from 'react'; +import { ASSISTANT_API } from '../../common/constants/llm'; +import { AgentFrameworkTrace } from '../../common/utils/llm_chat/traces'; +import { useCore } from '../contexts/core_context'; +import { GenericReducer, genericReducer } from './fetch_reducer'; + +export const useFetchAgentFrameworkTraces = (traceId: string) => { + const core = useCore(); + const reducer: GenericReducer = genericReducer; + const [state, dispatch] = useReducer(reducer, { loading: false }); + + useEffect(() => { + const abortController = new AbortController(); + dispatch({ type: 'request' }); + if (!traceId) { + dispatch({ type: 'success', payload: undefined }); + return; + } + + core.services.http + .get(`${ASSISTANT_API.TRACE}/${traceId}`) + .then((payload) => + dispatch({ + type: 'success', + payload, + }) + ) + .catch((error) => dispatch({ type: 'failure', error })); + + return () => abortController.abort(); + }, [traceId]); + + return { ...state }; +}; diff --git a/public/tabs/chat/messages/message_bubble.tsx b/public/tabs/chat/messages/message_bubble.tsx index 4c0133ac..096e0f60 100644 --- a/public/tabs/chat/messages/message_bubble.tsx +++ b/public/tabs/chat/messages/message_bubble.tsx @@ -188,6 +188,7 @@ export const MessageBubble: React.FC = React.memo((props) => {feedbackResult !== false ? ( feedbackOutput(true, feedbackResult)} @@ -197,6 +198,7 @@ export const MessageBubble: React.FC = React.memo((props) => {feedbackResult !== true ? ( feedbackOutput(false, feedbackResult)} diff --git a/public/tabs/chat/messages/message_footer.tsx b/public/tabs/chat/messages/message_footer.tsx index aa00fa89..8fcdd24b 100644 --- a/public/tabs/chat/messages/message_footer.tsx +++ b/public/tabs/chat/messages/message_footer.tsx @@ -7,9 +7,9 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@e import React from 'react'; import { IMessage } from '../../../../common/types/chat_saved_object_attributes'; import { FeedbackModal } from '../../../components/feedback_modal'; -import { LangchainTracesFlyoutBody } from '../../../components/langchain_traces_flyout_body'; import { useChatContext } from '../../../contexts/chat_context'; import { useCore } from '../../../contexts/core_context'; +import { AgentFrameworkTracesFlyoutBody } from '../../../components/agent_framework_traces_flyout_body'; interface MessageFooterProps { message: IMessage; @@ -31,12 +31,7 @@ export const MessageFooter: React.FC = React.memo((props) => size="xs" flush="left" onClick={() => { - chatContext.setFlyoutComponent( - chatContext.setFlyoutComponent(null)} - traceId={traceId} - /> - ); + chatContext.setFlyoutComponent(); }} > How was this generated? diff --git a/public/tabs/chat_window_header.tsx b/public/tabs/chat_window_header.tsx index 8ec18cdc..82cc32b5 100644 --- a/public/tabs/chat_window_header.tsx +++ b/public/tabs/chat_window_header.tsx @@ -22,6 +22,7 @@ import { useCore } from '../contexts/core_context'; import { useChatState } from '../hooks/use_chat_state'; import { useSaveChat } from '../hooks/use_save_chat'; import chatIcon from '../assets/chat.svg'; +import { TAB_ID } from '../utils/constants'; interface ChatWindowHeaderProps { flyoutFullScreen: boolean; toggleFlyoutFullScreen: () => void; @@ -157,10 +158,10 @@ export const ChatWindowHeader: React.FC = React.memo((pro chatContext.setFlyoutComponent(undefined); // Back to chat tab if history page already visible chatContext.setSelectedTabId( - chatContext.selectedTabId === 'history' ? 'chat' : 'history' + chatContext.selectedTabId === TAB_ID.HISTORY ? TAB_ID.CHAT : TAB_ID.HISTORY ); }} - display={chatContext.selectedTabId === 'history' ? 'fill' : undefined} + display={chatContext.selectedTabId === TAB_ID.HISTORY ? 'fill' : undefined} /> diff --git a/public/tabs/history/chat_history_page.tsx b/public/tabs/history/chat_history_page.tsx index 3dc8fefd..2dff9e6f 100644 --- a/public/tabs/history/chat_history_page.tsx +++ b/public/tabs/history/chat_history_page.tsx @@ -20,6 +20,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; import { useDebounce, useObservable } from 'react-use'; import cs from 'classnames'; +import { TAB_ID } from 'public/utils/constants'; import { useChatActions } from '../../hooks/use_chat_actions'; import { useChatContext } from '../../contexts/chat_context'; import { useCore } from '../../contexts/core_context'; @@ -64,7 +65,7 @@ export const ChatHistoryPage: React.FC = React.memo((props }, []); const handleBack = useCallback(() => { - setSelectedTabId('chat'); + setSelectedTabId(TAB_ID.CHAT); }, [setSelectedTabId]); const handleHistoryDeleted = useCallback( @@ -105,7 +106,11 @@ export const ChatHistoryPage: React.FC = React.memo((props {flyoutFullScreen ? ( - + ) : ( diff --git a/public/utils/constants.ts b/public/utils/constants.ts new file mode 100644 index 00000000..9c35746e --- /dev/null +++ b/public/utils/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum TAB_ID { + CHAT = 'chat', + COMPOSE = 'compose', + INSIGHTS = 'insights', + HISTORY = 'history', + TRACE = 'trace', +} diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index 632babe4..c234a42d 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -105,6 +105,15 @@ const updateSessionRoute = { }, }; +const getTracesRoute = { + path: `${ASSISTANT_API.TRACE}/{traceId}`, + validate: { + params: schema.object({ + traceId: schema.string(), + }), + }, +}; + export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) { const createStorageService = (context: RequestHandlerContext) => new AgentFrameworkStorageService( @@ -226,6 +235,25 @@ export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) } ); + router.get( + getTracesRoute, + async ( + context, + request, + response + ): Promise> => { + const storageService = createStorageService(context); + + try { + const getResponse = await storageService.getTraces(request.params.traceId); + return response.ok({ body: getResponse }); + } catch (error) { + context.assistant_plugin.logger.error(error); + return response.custom({ statusCode: error.statusCode || 500, body: error.message }); + } + } + ); + router.post( abortAgentExecutionRoute, async ( diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index 8dadc991..7e3723d1 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -12,7 +12,6 @@ import { LLMModelFactory } from '../../olly/models/llm_model_factory'; import { PPLTools } from '../../olly/tools/tool_sets/ppl'; import { PPLGenerationRequestSchema } from '../../routes/langchain_routes'; import { ChatService } from './chat_service'; -import { LLMRequestSchema } from '../../routes/chat_routes'; const MEMORY_ID_FIELD = 'memory_id'; diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts index d80f5b7e..06e7777a 100644 --- a/server/services/storage/agent_framework_storage_service.ts +++ b/server/services/storage/agent_framework_storage_service.ts @@ -4,6 +4,7 @@ */ import { ApiResponse } from '@opensearch-project/opensearch/.'; +import { AgentFrameworkTrace } from '../../../common/utils/llm_chat/traces'; import { OpenSearchClient } from '../../../../../src/core/server'; import { IMessage, @@ -181,4 +182,36 @@ export class AgentFrameworkStorageService implements StorageService { throw new Error('update converstaion failed, reason:' + JSON.stringify(error.meta?.body)); } } + + async getTraces(interactionId: string): Promise { + try { + const response = (await this.client.transport.request({ + method: 'GET', + path: `/_plugins/_ml/memory/trace/${interactionId}/_list`, + })) as ApiResponse<{ + traces: Array<{ + conversation_id: string; + interaction_id: string; + create_time: string; + input: string; + response: string; + origin: string; + parent_interaction_id: string; + trace_number: number; + }>; + }>; + + return response.body.traces.map((item) => ({ + interactionId: item.interaction_id, + parentInteractionId: item.parent_interaction_id, + input: item.input, + output: item.response, + createTime: item.create_time, + origin: item.origin, + traceNumber: item.trace_number, + })); + } catch (error) { + throw new Error('get traces failed, reason:' + JSON.stringify(error.meta?.body)); + } + } } From 7082f75c998b19311800c500d8f40c4b00096f1c Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 1 Dec 2023 11:32:17 +0800 Subject: [PATCH 30/31] feat: retrieve conversation metadata when loading conversation (#27) * feat: retrieve conversation metadata when loading conversation Signed-off-by: SuZhou-Joe * feat: change /_plugins/_ml to a constant Signed-off-by: SuZhou-Joe * feat: change import Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- common/types/chat_saved_object_attributes.ts | 2 +- public/hooks/use_sessions.ts | 4 +- server/routes/chat_routes.ts | 4 +- server/services/chat/olly_chat_service.ts | 3 +- .../agent_framework_storage_service.ts | 60 ++++++++++--------- 5 files changed, 38 insertions(+), 35 deletions(-) diff --git a/common/types/chat_saved_object_attributes.ts b/common/types/chat_saved_object_attributes.ts index acfc8d71..118ca75f 100644 --- a/common/types/chat_saved_object_attributes.ts +++ b/common/types/chat_saved_object_attributes.ts @@ -18,7 +18,7 @@ export interface Interaction { export interface ISession { title: string; - version: number; + version?: number; createdTimeMs: number; updatedTimeMs: number; messages: IMessage[]; diff --git a/public/hooks/use_sessions.ts b/public/hooks/use_sessions.ts index 1f0789bd..8c863683 100644 --- a/public/hooks/use_sessions.ts +++ b/public/hooks/use_sessions.ts @@ -42,9 +42,9 @@ export const usePatchSession = () => { dispatch({ type: 'request', abortController }); return core.services.http .put(`${ASSISTANT_API.SESSION}/${sessionId}`, { - query: { + body: JSON.stringify({ title, - }, + }), signal: abortController.signal, }) .then((payload) => dispatch({ type: 'success', payload })) diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index c234a42d..2d557236 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -99,7 +99,7 @@ const updateSessionRoute = { params: schema.object({ sessionId: schema.string(), }), - query: schema.object({ + body: schema.object({ title: schema.string(), }), }, @@ -225,7 +225,7 @@ export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) try { const getResponse = await storageService.updateSession( request.params.sessionId, - request.query.title + request.body.title ); return response.ok({ body: getResponse }); } catch (error) { diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index 7e3723d1..93e08f9c 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -12,6 +12,7 @@ import { LLMModelFactory } from '../../olly/models/llm_model_factory'; import { PPLTools } from '../../olly/tools/tool_sets/ppl'; import { PPLGenerationRequestSchema } from '../../routes/langchain_routes'; import { ChatService } from './chat_service'; +import { ML_COMMONS_BASE_API } from '../../olly/models/constants'; const MEMORY_ID_FIELD = 'memory_id'; @@ -49,7 +50,7 @@ export class OllyChatService implements ChatService { } const agentFrameworkResponse = (await opensearchClient.transport.request({ method: 'POST', - path: `/_plugins/_ml/agents/${rootAgentId}/_execute`, + path: `${ML_COMMONS_BASE_API}/agents/${rootAgentId}/_execute`, body: { parameters: parametersPayload, }, diff --git a/server/services/storage/agent_framework_storage_service.ts b/server/services/storage/agent_framework_storage_service.ts index 06e7777a..bb01126d 100644 --- a/server/services/storage/agent_framework_storage_service.ts +++ b/server/services/storage/agent_framework_storage_service.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ApiResponse } from '@opensearch-project/opensearch/.'; +import { TransportRequestPromise, ApiResponse } from '@opensearch-project/opensearch/lib/Transport'; import { AgentFrameworkTrace } from '../../../common/utils/llm_chat/traces'; import { OpenSearchClient } from '../../../../../src/core/server'; import { @@ -16,6 +16,7 @@ import { GetSessionsSchema } from '../../routes/chat_routes'; import { StorageService } from './storage_service'; import { MessageParser } from '../../types'; import { MessageParserRunner } from '../../utils/message_parser_runner'; +import { ML_COMMONS_BASE_API } from '../../olly/models/constants'; export interface SessionOptResponse { success: boolean; @@ -29,37 +30,38 @@ export class AgentFrameworkStorageService implements StorageService { private readonly messageParsers: MessageParser[] = [] ) {} async getSession(sessionId: string): Promise { - const session = (await this.client.transport.request({ - method: 'GET', - path: `/_plugins/_ml/memory/conversation/${sessionId}/_list`, - })) as ApiResponse<{ - interactions: Interaction[]; - }>; + const [interactionsResp, conversation] = await Promise.all([ + this.client.transport.request({ + method: 'GET', + path: `${ML_COMMONS_BASE_API}/memory/conversation/${sessionId}/_list`, + }) as TransportRequestPromise< + ApiResponse<{ + interactions: Interaction[]; + }> + >, + this.client.transport.request({ + method: 'GET', + path: `${ML_COMMONS_BASE_API}/memory/conversation/${sessionId}`, + }) as TransportRequestPromise< + ApiResponse<{ + conversation_id: string; + create_time: string; + updated_time: string; + name: string; + }> + >, + ]); const messageParserRunner = new MessageParserRunner(this.messageParsers); - const finalInteractions: Interaction[] = [...session.body.interactions]; + const finalInteractions = interactionsResp.body.interactions; - /** - * Sort interactions according to create_time - */ - finalInteractions.sort((interactionA, interactionB) => { - const { create_time: createTimeA } = interactionA; - const { create_time: createTimeB } = interactionB; - const createTimeMSA = +new Date(createTimeA); - const createTimeMSB = +new Date(createTimeB); - if (isNaN(createTimeMSA) || isNaN(createTimeMSB)) { - return 0; - } - return createTimeMSA - createTimeMSB; - }); let finalMessages: IMessage[] = []; for (const interaction of finalInteractions) { finalMessages = [...finalMessages, ...(await messageParserRunner.run(interaction))]; } return { - title: 'test', - version: 1, - createdTimeMs: Date.now(), - updatedTimeMs: Date.now(), + title: conversation.body.name, + createdTimeMs: +new Date(conversation.body.create_time), + updatedTimeMs: +new Date(conversation.body.updated_time), messages: finalMessages, interactions: finalInteractions, }; @@ -101,7 +103,7 @@ export class AgentFrameworkStorageService implements StorageService { const sessions = await this.client.transport.request({ method: 'GET', - path: `/_plugins/_ml/memory/conversation/_search`, + path: `${ML_COMMONS_BASE_API}/memory/conversation/_search`, body: requestParams, }); @@ -140,7 +142,7 @@ export class AgentFrameworkStorageService implements StorageService { try { const response = await this.client.transport.request({ method: 'DELETE', - path: `/_plugins/_ml/memory/conversation/${sessionId}/_delete`, + path: `${ML_COMMONS_BASE_API}/memory/conversation/${sessionId}/_delete`, }); if (response.statusCode === 200) { return { @@ -162,7 +164,7 @@ export class AgentFrameworkStorageService implements StorageService { try { const response = await this.client.transport.request({ method: 'PUT', - path: `/_plugins/_ml/memory/conversation/${sessionId}/_update`, + path: `${ML_COMMONS_BASE_API}/memory/conversation/${sessionId}/_update`, body: { name: title, }, @@ -187,7 +189,7 @@ export class AgentFrameworkStorageService implements StorageService { try { const response = (await this.client.transport.request({ method: 'GET', - path: `/_plugins/_ml/memory/trace/${interactionId}/_list`, + path: `${ML_COMMONS_BASE_API}/memory/trace/${interactionId}/_list`, })) as ApiResponse<{ traces: Array<{ conversation_id: string; From 9d32765c5d6ae4582447c8e5bcec9c6b2beaa203 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Fri, 1 Dec 2023 04:40:07 +0000 Subject: [PATCH 31/31] Utilize barrel file for cleaner imports Example of using index.ts and importing one line instead of two. Signed-off-by: Kawika Avilla --- public/contexts/index.ts | 7 +++++++ public/hooks/index.ts | 6 ++++++ public/tabs/history/chat_history_page.tsx | 3 +-- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 public/contexts/index.ts create mode 100644 public/hooks/index.ts diff --git a/public/contexts/index.ts b/public/contexts/index.ts new file mode 100644 index 00000000..469c4a1c --- /dev/null +++ b/public/contexts/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useChatContext } from './chat_context'; +export { useCore } from './core_context'; diff --git a/public/hooks/index.ts b/public/hooks/index.ts new file mode 100644 index 00000000..ebd4d336 --- /dev/null +++ b/public/hooks/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useSaveChat } from './use_save_chat'; diff --git a/public/tabs/history/chat_history_page.tsx b/public/tabs/history/chat_history_page.tsx index 2dff9e6f..c013204f 100644 --- a/public/tabs/history/chat_history_page.tsx +++ b/public/tabs/history/chat_history_page.tsx @@ -22,8 +22,7 @@ import { useDebounce, useObservable } from 'react-use'; import cs from 'classnames'; import { TAB_ID } from 'public/utils/constants'; import { useChatActions } from '../../hooks/use_chat_actions'; -import { useChatContext } from '../../contexts/chat_context'; -import { useCore } from '../../contexts/core_context'; +import { useChatContext, useCore } from '../../contexts'; import { ChatHistorySearchList } from './chat_history_search_list'; interface ChatHistoryPageProps {