From 43a34112907a7e4ab6a2d9eaec37f5fadbb6d60d Mon Sep 17 00:00:00 2001 From: Ning Lv Date: Wed, 20 Nov 2024 12:16:27 +0800 Subject: [PATCH 01/12] feat: add tour guide --- source/portal/package-lock.json | 130 ++++++++ source/portal/package.json | 3 +- source/portal/src/layout/CommonLayout.tsx | 36 +-- source/portal/src/pages/home/Home.tsx | 344 ++++++++++++++++++---- source/portal/src/types/index.ts | 21 +- 5 files changed, 458 insertions(+), 76 deletions(-) diff --git a/source/portal/package-lock.json b/source/portal/package-lock.json index fa83327bf..2eb5c8b39 100644 --- a/source/portal/package-lock.json +++ b/source/portal/package-lock.json @@ -22,6 +22,7 @@ "react-avatar": "^5.0.3", "react-dom": "^18.2.0", "react-i18next": "^14.1.1", + "react-joyride": "^2.9.3", "react-markdown": "^9.0.1", "react-oidc-context": "^3.1.0", "react-router-dom": "^6.22.3", @@ -1071,6 +1072,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@gilbarbara/deep-equal": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz", + "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2551,12 +2557,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-diff": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", + "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -4382,6 +4401,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-lite": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-1.2.1.tgz", + "integrity": "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==" + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -6017,6 +6041,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -6181,6 +6215,41 @@ "react": "^18.2.0" } }, + "node_modules/react-floater": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz", + "integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==", + "dependencies": { + "deepmerge": "^4.3.1", + "is-lite": "^0.8.2", + "popper.js": "^1.16.0", + "prop-types": "^15.8.1", + "tree-changes": "^0.9.1" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-floater/node_modules/@gilbarbara/deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==" + }, + "node_modules/react-floater/node_modules/is-lite": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz", + "integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==" + }, + "node_modules/react-floater/node_modules/tree-changes": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz", + "integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==", + "dependencies": { + "@gilbarbara/deep-equal": "^0.1.1", + "is-lite": "^0.8.2" + } + }, "node_modules/react-i18next": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz", @@ -6202,11 +6271,53 @@ } } }, + "node_modules/react-innertext": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", + "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", + "peerDependencies": { + "@types/react": ">=0.0.0 <=99", + "react": ">=0.0.0 <=99" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-joyride": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.9.3.tgz", + "integrity": "sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw==", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "deep-diff": "^1.0.2", + "deepmerge": "^4.3.1", + "is-lite": "^1.2.1", + "react-floater": "^0.7.9", + "react-innertext": "^1.1.5", + "react-is": "^16.13.1", + "scroll": "^3.0.1", + "scrollparent": "^2.1.0", + "tree-changes": "^0.11.2", + "type-fest": "^4.27.0" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-joyride/node_modules/type-fest": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.27.0.tgz", + "integrity": "sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-keyed-flatten-children": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/react-keyed-flatten-children/-/react-keyed-flatten-children-1.3.0.tgz", @@ -6627,6 +6738,16 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scroll": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scroll/-/scroll-3.0.1.tgz", + "integrity": "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==" + }, + "node_modules/scrollparent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz", + "integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==" + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -6952,6 +7073,15 @@ "node": ">=8.0" } }, + "node_modules/tree-changes": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.11.2.tgz", + "integrity": "sha512-4gXlUthrl+RabZw6lLvcCDl6KfJOCmrC16BC5CRdut1EAH509Omgg0BfKLY+ViRlzrvYOTWR0FMS2SQTwzumrw==", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "is-lite": "^1.2.0" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", diff --git a/source/portal/package.json b/source/portal/package.json index 0a071d4ce..c7c268cba 100644 --- a/source/portal/package.json +++ b/source/portal/package.json @@ -32,7 +32,8 @@ "remark-gfm": "^4.0.0", "remark-html": "^16.0.1", "sass": "^1.74.1", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "react-joyride": "^2.9.3" }, "devDependencies": { "@types/lodash": "^4.17.0", diff --git a/source/portal/src/layout/CommonLayout.tsx b/source/portal/src/layout/CommonLayout.tsx index 565a75963..f81ce7d96 100644 --- a/source/portal/src/layout/CommonLayout.tsx +++ b/source/portal/src/layout/CommonLayout.tsx @@ -21,6 +21,7 @@ import { useAuth } from 'react-oidc-context'; import ConfigContext from 'src/context/config-context'; import { useLocation, useNavigate } from 'react-router-dom'; import CustomBreadCrumb, { BreadCrumbType } from './CustomBreadCrumb'; +import { CustomNavigationItem } from 'src/types'; interface CommonLayoutProps { activeHref: string; @@ -154,6 +155,7 @@ const CommonLayout: React.FC = ({ { if (!e.detail.external) { e.preventDefault(); @@ -165,45 +167,38 @@ const CommonLayout: React.FC = ({ type: 'link', text: t('homeSidebar'), href: '/', + id: 'home-sidebar', + itemID: 'home-nav' }, { type: 'section', text: t('chatSpace'), + id: 'chat-space', items: [ { type: 'link', text: t('chat'), href: '/chats', + id: 'chat', + itemID: 'chat-nav' }, { type: 'link', text: t('sessionHistory'), href: '/sessions', + id: 'session-history', + itemID: 'session-history-nav' }, ], }, { type: 'section', text: t('settings'), - items: layoutItems as readonly any[], - // href: '/chatbot-management', - // }, - // { - // type: 'link', - // text: t('intention'), - // href: '/intention', - // }, - // { - // type: 'link', - // text: t('docLibrary'), - // href: '/library', - // }, - // { - // type: 'link', - // text: t('prompt'), - // href: '/prompts', - // }, - // ], + items: layoutItems.map((item, index) => ({ + ...item, + itemID: `settings-nav-${index}`, + className: item.text.toLowerCase().replace(/\s+/g, '-'), + })), }, { type: 'divider' }, { @@ -211,8 +206,9 @@ const CommonLayout: React.FC = ({ text: t('documentation'), href: 'https://github.com/aws-samples/Intelli-Agent', external: true, + itemID: 'docs-nav' }, - ]} + ] as CustomNavigationItem[]} /> } content={<>{isLoading ? : children}} diff --git a/source/portal/src/pages/home/Home.tsx b/source/portal/src/pages/home/Home.tsx index 990f11cd1..d5e8cbfd9 100644 --- a/source/portal/src/pages/home/Home.tsx +++ b/source/portal/src/pages/home/Home.tsx @@ -6,7 +6,7 @@ import { Header, SpaceBetween, } from '@cloudscape-design/components'; -import React from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import CommonLayout from 'src/layout/CommonLayout'; import GetStarted from './comps/GetStarted'; @@ -15,66 +15,302 @@ import BenefitsFeatures from './comps/BenefitsFeatures'; import UseCases from './comps/UseCases'; import BANNER from 'src/assets/images/banner.jpeg'; import { useNavigate } from 'react-router-dom'; +import Joyride, { CallBackProps, STATUS, ACTIONS } from 'react-joyride'; +import ConfigContext from 'src/context/config-context'; const Home: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); + const [runTour, setRunTour] = useState(false); + const config = useContext(ConfigContext); + + const baseSteps = [ + { + target: '.home-banner', + content: 'This is the banner section.', + disableBeacon: true, + }, + { + target: 'a[href="/chats"]', + content: 'Click here to start chatting with our AI assistant.', + disableBeacon: true, + }, + { + target: 'a[href="/sessions"]', + content: 'Session history contains all your chat history, you can resume the chat by choosing the chat history', + disableBeacon: true, + }, + { + target: 'a[href="/chatbot-management"]', + content: 'Manage your chatbots here.', + disableBeacon: true, + }, + { + target: 'a[href="/intention"]', + content: 'Define your intentions here.', + disableBeacon: true, + }, + ]; + + const kbStep = { + target: 'a[href="/library"]', + content: 'Access your document library here.', + disableBeacon: true, + }; + + const promptsStep = { + target: 'a[href="/prompts"]', + content: 'Manage your prompts here.', + disableBeacon: true, + }; + + const steps = [ + ...baseSteps, + ...(config?.kbEnabled === 'true' ? [kbStep] : []), + promptsStep, + ].filter(step => { + return document.querySelector(step.target as string) !== null; + }); + + const handleJoyrideCallback = (data: CallBackProps) => { + const { status, action } = data; + if ([STATUS.FINISHED, STATUS.SKIPPED].includes(status)) { + setRunTour(false); + localStorage.setItem('tourCompleted', 'true'); + } else if (action === ACTIONS.START) { + setRunTour(true); + } + }; + + const resetTour = () => { + localStorage.removeItem('tourCompleted'); + setRunTour(true); + }; + + useEffect(() => { + const tourCompleted = localStorage.getItem('tourCompleted'); + if (!tourCompleted) { + setRunTour(true); + } + }, []); + + const joyrideStyles = { + options: { + zIndex: 9999, + arrowColor: '#fff', + backgroundColor: '#fff', + primaryColor: '#0972d3', + textColor: '#16191f', + overlayColor: 'rgba(0, 0, 0, 0.5)', + width: 400, + }, + overlay: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + mixBlendMode: 'hard-light', + zIndex: 9998 + }, + tooltip: { + zIndex: 9999, + backgroundColor: '#fff', + borderRadius: '8px', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + padding: '16px', + fontSize: '14px', + animation: 'fade-in 0.3s ease-in-out', + }, + tooltipContainer: { + textAlign: 'left', + padding: '8px 0', + }, + tooltipTitle: { + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '8px', + color: '#16191f', + }, + tooltipContent: { + color: '#5f6b7a', + lineHeight: '1.5', + }, + buttonNext: { + backgroundColor: '#0972d3', + padding: '8px 16px', + fontSize: '14px', + fontWeight: '500', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + transition: 'background-color 0.2s ease', + '&:hover': { + backgroundColor: '#033160', + }, + }, + buttonBack: { + color: '#5f6b7a', + padding: '8px 16px', + fontSize: '14px', + fontWeight: '500', + marginRight: '8px', + backgroundColor: 'transparent', + border: 'none', + cursor: 'pointer', + transition: 'color 0.2s ease', + '&:hover': { + color: '#16191f', + }, + }, + buttonSkip: { + color: '#5f6b7a', + fontSize: '14px', + padding: '8px', + backgroundColor: 'transparent', + border: 'none', + cursor: 'pointer', + transition: 'color 0.2s ease', + '&:hover': { + color: '#16191f', + }, + }, + spotlight: { + backgroundColor: 'transparent', + borderRadius: '4px', + boxShadow: '0 0 0 4px rgba(9, 114, 211, 0.3)', + zIndex: 9998, + }, + beacon: { + display: 'none' + }, + floaterStyles: { + arrow: { + length: 8, + margin: 4, + }, + wrapper: { + padding: 0, + margin: 0, + transition: 'transform 0.2s ease-in-out', + } + } + }; + return ( - // - - - - {t('awsSolutionGuidance')} | {t('mead')} - -
- - - } - description={t('projectDescription')} + <> + + + + + + {t('awsSolutionGuidance')} | {t('mead')} + +
+ + + + } + description={t('projectDescription')} + > + {t('solutionName')} + {t('subTitle')} +
+ + } + > +
+ - {t('solutionName')} - {t('subTitle')} -
+ +
+ banner +
+ + +
+ + + + + - } - > -
- - -
- banner -
- - -
- - - - -
-
-
-
+ +
+ ); }; diff --git a/source/portal/src/types/index.ts b/source/portal/src/types/index.ts index 6c878a6f6..ebeca0bff 100644 --- a/source/portal/src/types/index.ts +++ b/source/portal/src/types/index.ts @@ -247,4 +247,23 @@ export interface ExecutionResponse { export interface SelectedOption { value: string; label: string; -} \ No newline at end of file +} + +import { SideNavigationProps } from "@cloudscape-design/components"; + +// Extend the Link type to include id +export interface CustomLink extends SideNavigationProps.Link { + id?: string; + itemID?: string; + className?: string; +} + +// Extend the Section type to include id +export interface CustomSection extends SideNavigationProps.Section { + id?: string; + 'data-testid'?: string; +} + +// Create a union type for all navigation items +export type CustomNavigationItem = CustomLink | CustomSection | SideNavigationProps.Divider; + From 197d7e95d8fc13c2cc0b81a4c0b29d5778c4f47e Mon Sep 17 00:00:00 2001 From: NingLyu Date: Tue, 26 Nov 2024 01:13:43 +0000 Subject: [PATCH 02/12] chore: update tour guide --- .../tools/common_tools/rag.py | 3 +- source/portal/src/pages/home/Home.tsx | 37 ++++--------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py index e706ffcdd..5463e2b37 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py @@ -30,7 +30,8 @@ def rag_tool(retriever_config: dict, query=None): unique_figure_list = [dict(t) for t in unique_set] state['extra_response']['figures'] = unique_figure_list - context_md = format_rag_data(output["result"]["docs"], state.get("qq_match_contexts", {})) + context_md = format_rag_data( + output["result"]["docs"], state.get("qq_match_contexts", {})) send_trace( f"\n\n{context_md}\n\n", enable_trace=state["enable_trace"]) # send_trace( diff --git a/source/portal/src/pages/home/Home.tsx b/source/portal/src/pages/home/Home.tsx index d5e8cbfd9..474c6a14e 100644 --- a/source/portal/src/pages/home/Home.tsx +++ b/source/portal/src/pages/home/Home.tsx @@ -27,7 +27,7 @@ const Home: React.FC = () => { const baseSteps = [ { target: '.home-banner', - content: 'This is the banner section.', + content: 'Deploying this solution using the default parameters will build the environment in Amazon Web Services.', disableBeacon: true, }, { @@ -42,25 +42,25 @@ const Home: React.FC = () => { }, { target: 'a[href="/chatbot-management"]', - content: 'Manage your chatbots here.', + content: 'You can create/edit/delete the chatbots. Each chatbot has at least one index for Intention/QD/QQ, Intention index stores the chatbot intentions, QD index stores the knowledges, QQ index stores the FAQ.', disableBeacon: true, }, { target: 'a[href="/intention"]', - content: 'Define your intentions here.', + content: 'Manage your intentions here. The intentions are uploaded via excel files, the chatbot will chat according to the intentions you provided, if no intention is provided, it will retrive knowledges by default.', disableBeacon: true, }, ]; const kbStep = { target: 'a[href="/library"]', - content: 'Access your document library here.', + content: 'You can create/update/delete knowledges. Choose index type as QD to inject a knowledge, and choose QQ index type to inject FAQ (only in excel format)', disableBeacon: true, }; const promptsStep = { target: 'a[href="/prompts"]', - content: 'Manage your prompts here.', + content: 'Manage your prompts here. Conversation summary prompt will rewrite the queries in your chat history, RAG prompt is for how to use the retrieved knowledges to help LLM generate responses, tool calling prompt defines how the agent choose and invoke tools.', disableBeacon: true, }; @@ -74,7 +74,7 @@ const Home: React.FC = () => { const handleJoyrideCallback = (data: CallBackProps) => { const { status, action } = data; - if ([STATUS.FINISHED, STATUS.SKIPPED].includes(status)) { + if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { setRunTour(false); localStorage.setItem('tourCompleted', 'true'); } else if (action === ACTIONS.START) { @@ -106,7 +106,7 @@ const Home: React.FC = () => { }, overlay: { backgroundColor: 'rgba(0, 0, 0, 0.5)', - mixBlendMode: 'hard-light', + mixBlendMode: 'hard-light' as const, zIndex: 9998 }, tooltip: { @@ -119,7 +119,7 @@ const Home: React.FC = () => { animation: 'fade-in 0.3s ease-in-out', }, tooltipContainer: { - textAlign: 'left', + textAlign: 'left' as const, padding: '8px 0', }, tooltipTitle: { @@ -141,9 +141,6 @@ const Home: React.FC = () => { borderRadius: '4px', cursor: 'pointer', transition: 'background-color 0.2s ease', - '&:hover': { - backgroundColor: '#033160', - }, }, buttonBack: { color: '#5f6b7a', @@ -155,9 +152,6 @@ const Home: React.FC = () => { border: 'none', cursor: 'pointer', transition: 'color 0.2s ease', - '&:hover': { - color: '#16191f', - }, }, buttonSkip: { color: '#5f6b7a', @@ -167,9 +161,6 @@ const Home: React.FC = () => { border: 'none', cursor: 'pointer', transition: 'color 0.2s ease', - '&:hover': { - color: '#16191f', - }, }, spotlight: { backgroundColor: 'transparent', @@ -179,17 +170,6 @@ const Home: React.FC = () => { }, beacon: { display: 'none' - }, - floaterStyles: { - arrow: { - length: 8, - margin: 4, - }, - wrapper: { - padding: 0, - margin: 0, - transition: 'transform 0.2s ease-in-out', - } } }; @@ -216,7 +196,6 @@ const Home: React.FC = () => { callback={handleJoyrideCallback} run={runTour} scrollToFirstStep={true} - disableBeacon={true} disableOverlayClose={true} hideBackButton={false} spotlightClicks={false} From 1791e5535621d71e672a3ebe7552b097566f0be7 Mon Sep 17 00:00:00 2001 From: Xu Han Date: Fri, 29 Nov 2024 05:52:51 +0000 Subject: [PATCH 03/12] chore: update setup_env.py to fix npm install issue --- source/script/setup_env.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/script/setup_env.sh b/source/script/setup_env.sh index 58d583bbb..dd92806d6 100644 --- a/source/script/setup_env.sh +++ b/source/script/setup_env.sh @@ -6,7 +6,13 @@ sudo yum groupinstall "Development Tools" -y sudo yum install python3 -y sudo yum install python3-pip -y # Install Node.js and npm -sudo yum install nodejs npm -y +node_version="v22.11.0" +file_name="node-${node_version}-linux-x64" +wget "https://nodejs.org/dist/${node_version}/${file_name}.tar.xz" +sudo tar xvf "${file_name}.tar.xz" --directory=/usr/local +sudo mv "/usr/local/${file_name}" /usr/local/nodejs +sudo ln -sf /usr/local/nodejs/bin/node /usr/bin/node +sudo ln -sf /usr/local/nodejs/bin/npm /usr/bin/npm # Install AWS CLI pip3 install awscli # Install Docker From d865ffbbba3dcf9a983cbb5deb8f885941f85bb2 Mon Sep 17 00:00:00 2001 From: Xu Han Date: Mon, 2 Dec 2024 06:59:21 +0000 Subject: [PATCH 04/12] chore: fomatting api stack --- source/infrastructure/lib/api/api-stack.ts | 343 +++++++++++---------- 1 file changed, 178 insertions(+), 165 deletions(-) diff --git a/source/infrastructure/lib/api/api-stack.ts b/source/infrastructure/lib/api/api-stack.ts index a3bac1d96..3d9f9ecde 100644 --- a/source/infrastructure/lib/api/api-stack.ts +++ b/source/infrastructure/lib/api/api-stack.ts @@ -159,7 +159,7 @@ export class ApiConstruct extends Construct { this.iamHelper.endpointStatement, ], }); - + const aosLambda = new LambdaFunction(this, "AOSLambda", { code: Code.fromAsset(join(__dirname, "../../../lambda/aos")), vpc: vpc, @@ -222,7 +222,7 @@ export class ApiConstruct extends Construct { const apiResourceStepFunction = api.root.addResource("knowledge-base"); const apiKBExecution = apiResourceStepFunction.addResource("executions"); - if ( props.knowledgeBaseStackOutputs.sfnOutput !== undefined) { + if (props.knowledgeBaseStackOutputs.sfnOutput !== undefined) { // Integration with Step Function to trigger ETL process // Lambda function to trigger Step Function const sfnLambda = new LambdaFunction(this, "StepFunctionLambda", { @@ -262,52 +262,55 @@ export class ApiConstruct extends Construct { apiKBExecution.addMethod( "GET", new apigw.LambdaIntegration(executionManagementLambda.function), - {...this.genMethodOption(api, auth, { - Items: {type: JsonSchemaType.ARRAY, items: { - type: JsonSchemaType.OBJECT, - properties: { - s3Prefix: { type: JsonSchemaType.STRING }, - offline: { type: JsonSchemaType.STRING }, - s3Bucket: { type: JsonSchemaType.STRING }, - executionId: { type: JsonSchemaType.STRING }, - executionStatus: { type: JsonSchemaType.STRING }, - qaEnhance: { type: JsonSchemaType.STRING }, - operationType: { type: JsonSchemaType.STRING }, - uiStatus: { type: JsonSchemaType.STRING }, - createTime: { type: JsonSchemaType.STRING }, // Consider using format: 'date-time' - sfnExecutionId: { type: JsonSchemaType.STRING }, - embeddingModelType: { type: JsonSchemaType.STRING }, - groupName: { type: JsonSchemaType.STRING }, - chatbotId: { type: JsonSchemaType.STRING }, - indexType: { type: JsonSchemaType.STRING }, - indexId: { type: JsonSchemaType.STRING }, + { + ...this.genMethodOption(api, auth, { + Items: { + type: JsonSchemaType.ARRAY, items: { + type: JsonSchemaType.OBJECT, + properties: { + s3Prefix: { type: JsonSchemaType.STRING }, + offline: { type: JsonSchemaType.STRING }, + s3Bucket: { type: JsonSchemaType.STRING }, + executionId: { type: JsonSchemaType.STRING }, + executionStatus: { type: JsonSchemaType.STRING }, + qaEnhance: { type: JsonSchemaType.STRING }, + operationType: { type: JsonSchemaType.STRING }, + uiStatus: { type: JsonSchemaType.STRING }, + createTime: { type: JsonSchemaType.STRING }, // Consider using format: 'date-time' + sfnExecutionId: { type: JsonSchemaType.STRING }, + embeddingModelType: { type: JsonSchemaType.STRING }, + groupName: { type: JsonSchemaType.STRING }, + chatbotId: { type: JsonSchemaType.STRING }, + indexType: { type: JsonSchemaType.STRING }, + indexId: { type: JsonSchemaType.STRING }, + }, + required: ['s3Prefix', + 'offline', + 's3Bucket', + 'executionId', + 'executionStatus', + 'qaEnhance', + 'operationType', + 'uiStatus', + 'createTime', + 'sfnExecutionId', + 'embeddingModelType', + 'groupName', + 'chatbotId', + 'indexType', + 'indexId'], + } }, - required: ['s3Prefix', - 'offline', - 's3Bucket', - 'executionId', - 'executionStatus', - 'qaEnhance', - 'operationType', - 'uiStatus', - 'createTime', - 'sfnExecutionId', - 'embeddingModelType', - 'groupName', - 'chatbotId', - 'indexType', - 'indexId'], - } - }, - Count: { type: JsonSchemaType.INTEGER }, - Config: { type: JsonSchemaType.OBJECT, - properties: { - MaxItems: { type: JsonSchemaType.INTEGER }, - PageSize: { type: JsonSchemaType.INTEGER }, - StartingToken: { type: JsonSchemaType.NULL } - } - } - }), + Count: { type: JsonSchemaType.INTEGER }, + Config: { + type: JsonSchemaType.OBJECT, + properties: { + MaxItems: { type: JsonSchemaType.INTEGER }, + PageSize: { type: JsonSchemaType.INTEGER }, + StartingToken: { type: JsonSchemaType.NULL } + } + } + }), requestParameters: { 'method.request.querystring.max_items': false, 'method.request.querystring.page_size': false @@ -327,7 +330,7 @@ export class ApiConstruct extends Construct { }) } ); - + const apiGetExecutionById = apiKBExecution.addResource("{executionId}"); apiGetExecutionById.addMethod( "GET", @@ -345,7 +348,7 @@ export class ApiConstruct extends Construct { s3Path: { type: JsonSchemaType.STRING }, status: { type: JsonSchemaType.STRING }, }, - required: ['s3Prefix', 's3Bucket', 'createTime', 's3Path', 'status','executionId'], + required: ['s3Prefix', 's3Bucket', 'createTime', 's3Path', 'status', 'executionId'], } }, Count: { type: JsonSchemaType.INTEGER } @@ -365,22 +368,24 @@ export class ApiConstruct extends Construct { apiUploadDoc.addMethod( "POST", new apigw.LambdaIntegration(uploadDocLambda.function), - {... + { + ... this.genMethodOption(api, auth, { - data: { type: JsonSchemaType.OBJECT, - properties: { - s3Bucket: { type: JsonSchemaType.STRING }, - s3Prefix: { type: JsonSchemaType.STRING }, - url: {type: JsonSchemaType.STRING} - } - }, + data: { + type: JsonSchemaType.OBJECT, + properties: { + s3Bucket: { type: JsonSchemaType.STRING }, + s3Prefix: { type: JsonSchemaType.STRING }, + url: { type: JsonSchemaType.STRING } + } + }, message: { type: JsonSchemaType.STRING } }), requestModels: this.genRequestModel(api, { "content_type": { "type": JsonSchemaType.STRING }, "file_name": { "type": JsonSchemaType.STRING }, }) - } + } ); } @@ -396,7 +401,7 @@ export class ApiConstruct extends Construct { }, statements: [this.iamHelper.dynamodbStatement], }); - + const listSessionsLambda = new LambdaFunction(this, "ListSessionsLambda", { handler: "list_sessions.lambda_handler", code: Code.fromAsset(join(__dirname, "../../../lambda/ddb")), @@ -406,7 +411,7 @@ export class ApiConstruct extends Construct { }, statements: [this.iamHelper.dynamodbStatement], }); - + const listMessagesLambda = new LambdaFunction(this, "ListMessagesLambda", { handler: "list_messages.lambda_handler", code: Code.fromAsset(join(__dirname, "../../../lambda/ddb")), @@ -416,7 +421,7 @@ export class ApiConstruct extends Construct { }, statements: [this.iamHelper.dynamodbStatement], }); - + const promptManagementLambda = new LambdaFunction(this, "PromptManagementLambda", { runtime: Runtime.PYTHON_3_12, code: Code.fromAsset(join(__dirname, "../../../lambda/prompt_management")), @@ -426,7 +431,7 @@ export class ApiConstruct extends Construct { }, layers: [apiLambdaOnlineSourceLayer], statements: [this.iamHelper.dynamodbStatement, - this.iamHelper.logStatement], + this.iamHelper.logStatement], }); @@ -450,13 +455,13 @@ export class ApiConstruct extends Construct { }, layers: [apiLambdaOnlineSourceLayer], statements: [this.iamHelper.dynamodbStatement, - this.iamHelper.logStatement, - this.iamHelper.secretStatement, - this.iamHelper.esStatement, - this.iamHelper.s3Statement, - this.iamHelper.bedrockStatement, - this.iamHelper.endpointStatement, - ], + this.iamHelper.logStatement, + this.iamHelper.secretStatement, + this.iamHelper.esStatement, + this.iamHelper.s3Statement, + this.iamHelper.bedrockStatement, + this.iamHelper.endpointStatement, + ], }); const chatbotManagementLambda = new LambdaFunction(this, "ChatbotManagementLambda", { @@ -471,7 +476,7 @@ export class ApiConstruct extends Construct { }, layers: [apiLambdaOnlineSourceLayer], statements: [this.iamHelper.dynamodbStatement, - this.iamHelper.logStatement], + this.iamHelper.logStatement], }); // Define the API Gateway Lambda Integration with proxy and no integration responses @@ -504,61 +509,66 @@ export class ApiConstruct extends Construct { const apiResourceChatbots = apiResourceChatbotManagement.addResource("chatbots"); apiResourceChatbots.addMethod("POST", lambdaChatbotIntegration, { ...this.genMethodOption(api, auth, { - chatbotId: {type: JsonSchemaType.STRING}, - groupName: {type: JsonSchemaType.STRING}, + chatbotId: { type: JsonSchemaType.STRING }, + groupName: { type: JsonSchemaType.STRING }, indexIds: { - type: JsonSchemaType.OBJECT, - properties: { + type: JsonSchemaType.OBJECT, + properties: { qq: { type: JsonSchemaType.STRING }, qd: { type: JsonSchemaType.STRING }, intention: { type: JsonSchemaType.STRING } - } + } }, - Message: {type: JsonSchemaType.STRING}, + Message: { type: JsonSchemaType.STRING }, }), requestModels: this.genRequestModel(api, { "chatbotId": { "type": JsonSchemaType.STRING }, - "index": {type: JsonSchemaType.OBJECT, - properties: { - qq: { type: JsonSchemaType.STRING }, - qd: { type: JsonSchemaType.STRING }, - intention: { type: JsonSchemaType.STRING } - }, - required: ['qq','qd','intention']}, - modelId: { "type": JsonSchemaType.STRING }, - modelName: { "type": JsonSchemaType.STRING } - }) - }); - apiResourceChatbots.addMethod("GET", lambdaChatbotIntegration, {...this.genMethodOption(api, auth, { - Items: {type: JsonSchemaType.ARRAY, items: { - type: JsonSchemaType.OBJECT, - properties: { - ChatbotId: { type: JsonSchemaType.STRING }, - ModelName: { type: JsonSchemaType.STRING }, - ModelId: { type: JsonSchemaType.STRING }, - LastModifiedTime: { type: JsonSchemaType.STRING } + "index": { + type: JsonSchemaType.OBJECT, + properties: { + qq: { type: JsonSchemaType.STRING }, + qd: { type: JsonSchemaType.STRING }, + intention: { type: JsonSchemaType.STRING } + }, + required: ['qq', 'qd', 'intention'] }, - required: ['ChatbotId', - 'ModelName', - 'ModelId', - 'LastModifiedTime'], - } - }, - Count: { type: JsonSchemaType.INTEGER }, - Config: { type: JsonSchemaType.OBJECT, - properties: { - MaxItems: { type: JsonSchemaType.INTEGER }, - PageSize: { type: JsonSchemaType.INTEGER }, - StartingToken: { type: JsonSchemaType.NULL } - } - }, - chatbot_ids: { - type: JsonSchemaType.ARRAY, items: { - type: JsonSchemaType.STRING, + modelId: { "type": JsonSchemaType.STRING }, + modelName: { "type": JsonSchemaType.STRING } + }) + }); + apiResourceChatbots.addMethod("GET", lambdaChatbotIntegration, { + ...this.genMethodOption(api, auth, { + Items: { + type: JsonSchemaType.ARRAY, items: { + type: JsonSchemaType.OBJECT, + properties: { + ChatbotId: { type: JsonSchemaType.STRING }, + ModelName: { type: JsonSchemaType.STRING }, + ModelId: { type: JsonSchemaType.STRING }, + LastModifiedTime: { type: JsonSchemaType.STRING } + }, + required: ['ChatbotId', + 'ModelName', + 'ModelId', + 'LastModifiedTime'], + } + }, + Count: { type: JsonSchemaType.INTEGER }, + Config: { + type: JsonSchemaType.OBJECT, + properties: { + MaxItems: { type: JsonSchemaType.INTEGER }, + PageSize: { type: JsonSchemaType.INTEGER }, + StartingToken: { type: JsonSchemaType.NULL } + } + }, + chatbot_ids: { + type: JsonSchemaType.ARRAY, items: { + type: JsonSchemaType.STRING, + } } - } - }) - , + }) + , requestParameters: { 'method.request.querystring.max_items': false, 'method.request.querystring.page_size': false @@ -616,14 +626,14 @@ export class ApiConstruct extends Construct { const apiResourcePromptManagementModels = apiResourcePromptManagement.addResource("models") apiResourcePromptManagementModels.addMethod("GET", lambdaPromptIntegration, this.genMethodOption(api, auth, null)); - + const apiResourcePromptManagementScenes = apiResourcePromptManagement.addResource("scenes") apiResourcePromptManagementScenes.addMethod("GET", lambdaPromptIntegration, this.genMethodOption(api, auth, null)); - + const apiResourcePrompt = apiResourcePromptManagement.addResource("prompts"); apiResourcePrompt.addMethod("POST", lambdaPromptIntegration, this.genMethodOption(api, auth, null)); apiResourcePrompt.addMethod("GET", lambdaPromptIntegration, this.genMethodOption(api, auth, null)); - + const apiResourcePromptProxy = apiResourcePrompt.addResource("{proxy+}") apiResourcePromptProxy.addMethod("POST", lambdaPromptIntegration, this.genMethodOption(api, auth, null)); apiResourcePromptProxy.addMethod("DELETE", lambdaPromptIntegration, this.genMethodOption(api, auth, null)); @@ -679,43 +689,46 @@ export class ApiConstruct extends Construct { "s3Prefix": { "type": JsonSchemaType.STRING } }) }); - apiResourceExecutionManagement.addMethod("GET", lambdaIntentionIntegration, {...this.genMethodOption(api, auth, { - Items: {type: JsonSchemaType.ARRAY, items: { - type: JsonSchemaType.OBJECT, - properties: { - model: { type: JsonSchemaType.STRING }, - executionStatus: { type: JsonSchemaType.STRING }, - index: { type: JsonSchemaType.STRING }, - fileName: { type: JsonSchemaType.STRING }, - createTime: { type: JsonSchemaType.STRING }, - createBy: { type: JsonSchemaType.STRING }, - executionId: { type: JsonSchemaType.STRING }, - - chatbotId: { type: JsonSchemaType.STRING }, - details: { type: JsonSchemaType.STRING }, - tag: { type: JsonSchemaType.STRING }, + apiResourceExecutionManagement.addMethod("GET", lambdaIntentionIntegration, { + ...this.genMethodOption(api, auth, { + Items: { + type: JsonSchemaType.ARRAY, items: { + type: JsonSchemaType.OBJECT, + properties: { + model: { type: JsonSchemaType.STRING }, + executionStatus: { type: JsonSchemaType.STRING }, + index: { type: JsonSchemaType.STRING }, + fileName: { type: JsonSchemaType.STRING }, + createTime: { type: JsonSchemaType.STRING }, + createBy: { type: JsonSchemaType.STRING }, + executionId: { type: JsonSchemaType.STRING }, + + chatbotId: { type: JsonSchemaType.STRING }, + details: { type: JsonSchemaType.STRING }, + tag: { type: JsonSchemaType.STRING }, + }, + required: ['model', + 'executionStatus', + 'index', + 'fileName', + 'createTime', + 'createBy', + 'executionId', + 'chatbotId', + 'details', + 'tag'], + } }, - required: ['model', - 'executionStatus', - 'index', - 'fileName', - 'createTime', - 'createBy', - 'executionId', - 'chatbotId', - 'details', - 'tag'], - } - }, - Count: { type: JsonSchemaType.INTEGER }, - Config: { type: JsonSchemaType.OBJECT, - properties: { - MaxItems: { type: JsonSchemaType.INTEGER }, - PageSize: { type: JsonSchemaType.INTEGER }, - StartingToken: { type: JsonSchemaType.NULL } - } - } - }), + Count: { type: JsonSchemaType.INTEGER }, + Config: { + type: JsonSchemaType.OBJECT, + properties: { + MaxItems: { type: JsonSchemaType.INTEGER }, + PageSize: { type: JsonSchemaType.INTEGER }, + StartingToken: { type: JsonSchemaType.NULL } + } + } + }), requestParameters: { 'method.request.querystring.max_items': false, 'method.request.querystring.page_size': false @@ -728,7 +741,7 @@ export class ApiConstruct extends Construct { { ...this.genMethodOption(api, auth, { Items: { - type: JsonSchemaType.ARRAY, + type: JsonSchemaType.ARRAY, items: { type: JsonSchemaType.OBJECT, properties: { @@ -748,7 +761,7 @@ export class ApiConstruct extends Construct { } } }, - required: ['s3Path', 's3Prefix', 'createTime', 'status','executionId'], + required: ['s3Path', 's3Prefix', 'createTime', 'status', 'executionId'], } }, Count: { type: JsonSchemaType.INTEGER } @@ -760,8 +773,8 @@ export class ApiConstruct extends Construct { ); // const apiUploadIntention = apiResourceIntentionManagement.addResource("upload"); // apiUploadIntention.addMethod("POST", lambdaIntentionIntegration, this.genMethodOption(api, auth, null)) - - + + // Define the API Gateway Lambda Integration with proxy and no integration responses const lambdaExecutorIntegration = new apigw.LambdaIntegration( props.chatStackOutputs.lambdaOnlineMain, @@ -793,7 +806,7 @@ export class ApiConstruct extends Construct { // const plan = api.addUsagePlan('ExternalUsagePlan', { // name: 'external-api-usage-plan' // }); - + // This is not safe, but for the purpose of the test, we will use this // For deployment, we suggest user manually create the key and use it on the console @@ -801,7 +814,7 @@ export class ApiConstruct extends Construct { // const key = api.addApiKey('ApiKey', { // value: apiKeyValue, // }); - + // plan.addApiKey(key); // plan.addApiStage({ // stage: api.deploymentStage @@ -822,11 +835,11 @@ export class ApiConstruct extends Construct { counter += 1; } return apiKeyValue; -} + } - genMethodOption =(api: apigw.RestApi, auth: apigw.RequestAuthorizer, properties: any)=>{ + genMethodOption = (api: apigw.RestApi, auth: apigw.RequestAuthorizer, properties: any) => { let responseModel = apigw.Model.EMPTY_MODEL - if(properties!==null){ + if (properties !== null) { responseModel = new Model(this, `ResponseModel-${Math.random().toString(36).substr(2, 9)}`, { restApi: api, schema: { @@ -862,8 +875,8 @@ export class ApiConstruct extends Construct { ] }; } - - genRequestModel = (api: apigw.RestApi, properties: any) =>{ + + genRequestModel = (api: apigw.RestApi, properties: any) => { return { 'application/json': new Model(this, `PostModel-${Math.random().toString(36).substr(2, 9)}`, { restApi: api, From a5b08c52024255651b712fe20e06cef70d7802a9 Mon Sep 17 00:00:00 2001 From: NingLyu Date: Mon, 2 Dec 2024 09:05:07 +0000 Subject: [PATCH 05/12] chore: i18n --- source/portal/src/locale/en.json | 15 +++++++++++++++ source/portal/src/locale/zh.json | 15 +++++++++++++++ source/portal/src/pages/home/Home.tsx | 26 +++++++++++++------------- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/source/portal/src/locale/en.json b/source/portal/src/locale/en.json index eed82e57f..cb8bcbbb5 100644 --- a/source/portal/src/locale/en.json +++ b/source/portal/src/locale/en.json @@ -192,6 +192,21 @@ "repeatedIndexName": "The index is duplicated with a previous entry, please modify it", "repeatedIndex": "The index name is already in use. Please choose a different name" }, + "tour":{ + "home": "Deploying this solution using the default parameters will build the environment in Amazon Web Services.", + "chat": "Click here to start chatting with the AI assistant.", + "session": "Session history contains all your chat history, you can resume the chat by choosing the chat history.", + "chatbot": "You can create/edit/delete the chatbots. Each chatbot has at least one index for Intention/QD/QQ, Intention index stores the chatbot intentions, QD index stores the knowledges, QQ index stores the FAQ.", + "intention": "Manage your intentions here. The intentions are uploaded via excel files, the chatbot will chat according to the intentions you provided, if no intention is provided, it will retrive knowledges by default.", + "kb": "You can create/update/delete knowledges. Choose index type as QD to inject a knowledge, and choose QQ index type to inject FAQ (only in excel format).", + "prompt": "Manage your prompts here. Conversation summary prompt will rewrite the queries in your chat history, RAG prompt is for how to use the retrieved knowledges to help LLM generate responses, tool calling prompt defines how the agent choose and invoke tools.", + "restartTour": "Start tour", + "previous": "Previous", + "close": "Close", + "finish": "Finish", + "next": "Next", + "skip": "Skip tour" + }, "solutionName": "AI Customer Service", "subTitle": "Streamlined Workflow for Building Agent-Based Applications", "projectDescription": "AI Customer Service offers a streamlined workflow for developing scalable, production-grade agent-based applications", diff --git a/source/portal/src/locale/zh.json b/source/portal/src/locale/zh.json index 9688092d3..10d220eeb 100644 --- a/source/portal/src/locale/zh.json +++ b/source/portal/src/locale/zh.json @@ -191,6 +191,21 @@ "indexValid": "索引已经被其他模型使用,请变更索引名称", "repeatedIndex": "索引名称和现有索引重复,请更换名称" }, + "tour":{ + "home": "使用默认参数部署此解决方案将在亚马逊网络服务(Amazon Web Services)中构建此环境。", + "chat": "点击此处开始与人工智能助手对话。", + "session": "会话历史包含您所有的聊天记录,您可以通过选择聊天历史来恢复对话。", + "chatbot": "您可以创建/编辑/删除聊天机器人。每个聊天机器人至少有一个用于意图/QD/QQ的索引,意图索引存储聊天机器人的意图,QD索引存储知识,QQ索引存储常见问题解答。", + "intention": "在此管理您的意图。意图通过Excel文件上传,聊天机器人将根据您提供的意图进行对话。如果没有提供意图,它将默认检索知识库。", + "kb": "您可以创建/更新/删除知识。选择索引类型为QD以注入知识,选择QQ索引类型以注入常见问题解答(仅支持Excel格式)。", + "prompt": "在此管理您的提示。对话摘要提示将重写您聊天历史中的查询,RAG提示用于如何使用检索到的知识来帮助大语言模型生成回复,工具调用提示定义了代理如何选择和调用工具。", + "restartTour": "开始向导", + "previous": "上一步", + "close": "关闭", + "finish": "完成", + "next": "下一步", + "skip": "跳过向导" + }, "solutionName": "AI Customer Service", "subTitle": "构建基于代理的应用程序的优化工作流", "projectDescription": "AI Customer Service提供一个高效简洁的工作流程,用于开发可扩展的、生产级别的、基于 agent(代理)的应用", diff --git a/source/portal/src/pages/home/Home.tsx b/source/portal/src/pages/home/Home.tsx index 474c6a14e..dc5f770ff 100644 --- a/source/portal/src/pages/home/Home.tsx +++ b/source/portal/src/pages/home/Home.tsx @@ -27,40 +27,40 @@ const Home: React.FC = () => { const baseSteps = [ { target: '.home-banner', - content: 'Deploying this solution using the default parameters will build the environment in Amazon Web Services.', + content: t('tour.home'), disableBeacon: true, }, { target: 'a[href="/chats"]', - content: 'Click here to start chatting with our AI assistant.', + content: t('tour.chat'), disableBeacon: true, }, { target: 'a[href="/sessions"]', - content: 'Session history contains all your chat history, you can resume the chat by choosing the chat history', + content: t('tour.session'), disableBeacon: true, }, { target: 'a[href="/chatbot-management"]', - content: 'You can create/edit/delete the chatbots. Each chatbot has at least one index for Intention/QD/QQ, Intention index stores the chatbot intentions, QD index stores the knowledges, QQ index stores the FAQ.', + content: t('tour.chatbot'), disableBeacon: true, }, { target: 'a[href="/intention"]', - content: 'Manage your intentions here. The intentions are uploaded via excel files, the chatbot will chat according to the intentions you provided, if no intention is provided, it will retrive knowledges by default.', + content: t('tour.intention'), disableBeacon: true, }, ]; const kbStep = { target: 'a[href="/library"]', - content: 'You can create/update/delete knowledges. Choose index type as QD to inject a knowledge, and choose QQ index type to inject FAQ (only in excel format)', + content: t('tour.kb'), disableBeacon: true, }; const promptsStep = { target: 'a[href="/prompts"]', - content: 'Manage your prompts here. Conversation summary prompt will rewrite the queries in your chat history, RAG prompt is for how to use the retrieved knowledges to help LLM generate responses, tool calling prompt defines how the agent choose and invoke tools.', + content: t('tour.prompt'), disableBeacon: true, }; @@ -222,11 +222,11 @@ const Home: React.FC = () => { } }} locale={{ - back: 'Previous', - close: 'Close', - last: 'Finish', - next: 'Next', - skip: 'Skip tour', + back: t('tour.previous'), + close: t('tour.close'), + last: t('tour.finish'), + next: t('tour.next'), + skip: t('tour.skip'), }} /> @@ -255,7 +255,7 @@ const Home: React.FC = () => { variant="normal" onClick={resetTour} > - {t('button.restartTour')} + {t('tour.restartTour')} } From bb722bd9a215524050540a4c19fda0e90c72b36b Mon Sep 17 00:00:00 2001 From: NingLyu Date: Mon, 2 Dec 2024 09:24:16 +0000 Subject: [PATCH 06/12] fix: update text --- source/portal/src/locale/zh.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/portal/src/locale/zh.json b/source/portal/src/locale/zh.json index 10d220eeb..6d888fc5a 100644 --- a/source/portal/src/locale/zh.json +++ b/source/portal/src/locale/zh.json @@ -198,7 +198,7 @@ "chatbot": "您可以创建/编辑/删除聊天机器人。每个聊天机器人至少有一个用于意图/QD/QQ的索引,意图索引存储聊天机器人的意图,QD索引存储知识,QQ索引存储常见问题解答。", "intention": "在此管理您的意图。意图通过Excel文件上传,聊天机器人将根据您提供的意图进行对话。如果没有提供意图,它将默认检索知识库。", "kb": "您可以创建/更新/删除知识。选择索引类型为QD以注入知识,选择QQ索引类型以注入常见问题解答(仅支持Excel格式)。", - "prompt": "在此管理您的提示。对话摘要提示将重写您聊天历史中的查询,RAG提示用于如何使用检索到的知识来帮助大语言模型生成回复,工具调用提示定义了代理如何选择和调用工具。", + "prompt": "在此管理您的提示词。对话摘要提示词将重写您聊天历史中的查询,RAG提示词用于如何使用检索到的知识来帮助大语言模型生成回复,工具调用提示词定义了代理如何选择和调用工具。", "restartTour": "开始向导", "previous": "上一步", "close": "关闭", From ab385e48e69aa2ca765323b0112eb73628c27a89 Mon Sep 17 00:00:00 2001 From: Xu Han Date: Mon, 2 Dec 2024 10:03:21 +0000 Subject: [PATCH 07/12] chore: update chat history api --- source/infrastructure/lib/api/api-stack.ts | 44 +-- .../chat_history/chat_history_management.py | 300 ++++++++++++++++++ source/lambda/ddb/list_messages.py | 100 ------ source/lambda/ddb/list_sessions.py | 113 ------- source/lambda/ddb/rating.py | 207 ------------ 5 files changed, 310 insertions(+), 454 deletions(-) create mode 100644 source/lambda/chat_history/chat_history_management.py delete mode 100644 source/lambda/ddb/list_messages.py delete mode 100644 source/lambda/ddb/list_sessions.py delete mode 100644 source/lambda/ddb/rating.py diff --git a/source/infrastructure/lib/api/api-stack.ts b/source/infrastructure/lib/api/api-stack.ts index 3d9f9ecde..3a9e533cf 100644 --- a/source/infrastructure/lib/api/api-stack.ts +++ b/source/infrastructure/lib/api/api-stack.ts @@ -390,33 +390,14 @@ export class ApiConstruct extends Construct { } if (props.config.chat.enabled) { - const chatHistoryLambda = new LambdaFunction(this, "ChatHistoryLambda", { - handler: "rating.lambda_handler", - code: Code.fromAsset(join(__dirname, "../../../lambda/ddb")), - environment: { - SESSIONS_TABLE_NAME: sessionsTableName, - MESSAGES_TABLE_NAME: messagesTableName, - SESSIONS_BY_TIMESTAMP_INDEX_NAME: "byTimestamp", - MESSAGES_BY_SESSION_ID_INDEX_NAME: "bySessionId", - }, - statements: [this.iamHelper.dynamodbStatement], - }); - const listSessionsLambda = new LambdaFunction(this, "ListSessionsLambda", { - handler: "list_sessions.lambda_handler", - code: Code.fromAsset(join(__dirname, "../../../lambda/ddb")), + const chatHistoryManagementLambda = new LambdaFunction(this, "ChatHistoryManagementLambda", { + code: Code.fromAsset(join(__dirname, "../../../lambda/chat_history")), + handler: "chat_history_management.lambda_handler", environment: { SESSIONS_TABLE_NAME: sessionsTableName, - SESSIONS_BY_TIMESTAMP_INDEX_NAME: "byTimestamp", - }, - statements: [this.iamHelper.dynamodbStatement], - }); - - const listMessagesLambda = new LambdaFunction(this, "ListMessagesLambda", { - handler: "list_messages.lambda_handler", - code: Code.fromAsset(join(__dirname, "../../../lambda/ddb")), - environment: { MESSAGES_TABLE_NAME: messagesTableName, + SESSIONS_BY_TIMESTAMP_INDEX_NAME: "byTimestamp", MESSAGES_BY_SESSION_ID_INDEX_NAME: "bySessionId", }, statements: [this.iamHelper.dynamodbStatement], @@ -479,17 +460,12 @@ export class ApiConstruct extends Construct { this.iamHelper.logStatement], }); - // Define the API Gateway Lambda Integration with proxy and no integration responses - const lambdaChatHistoryIntegration = new apigw.LambdaIntegration(chatHistoryLambda.function, { - proxy: true, - }); - - const apiResourceDdb = api.root.addResource("chat-history"); - apiResourceDdb.addMethod("POST", lambdaChatHistoryIntegration, this.genMethodOption(api, auth, null),); - const apiResourceListSessions = apiResourceDdb.addResource("sessions"); - apiResourceListSessions.addMethod("GET", new apigw.LambdaIntegration(listSessionsLambda.function), this.genMethodOption(api, auth, null),); - const apiResourceListMessages = apiResourceDdb.addResource("messages"); - apiResourceListMessages.addMethod("GET", new apigw.LambdaIntegration(listMessagesLambda.function), this.genMethodOption(api, auth, null),); + const apiResourceSessions = api.root.addResource("sessions"); + apiResourceSessions.addMethod("GET", new apigw.LambdaIntegration(chatHistoryManagementLambda.function), this.genMethodOption(api, auth, null),); + const apiResourceMessages = apiResourceSessions.addResource('{sessionId}').addResource("messages"); + apiResourceMessages.addMethod("GET", new apigw.LambdaIntegration(chatHistoryManagementLambda.function), this.genMethodOption(api, auth, null),); + const apiResourceMessageFeedback = apiResourceMessages.addResource("{messageId}").addResource("feedback"); + apiResourceMessageFeedback.addMethod("POST", new apigw.LambdaIntegration(chatHistoryManagementLambda.function), this.genMethodOption(api, auth, null),); const lambdaChatbotIntegration = new apigw.LambdaIntegration(chatbotManagementLambda.function, { proxy: true, diff --git a/source/lambda/chat_history/chat_history_management.py b/source/lambda/chat_history/chat_history_management.py new file mode 100644 index 000000000..a3040b049 --- /dev/null +++ b/source/lambda/chat_history/chat_history_management.py @@ -0,0 +1,300 @@ +""" +Lambda function for managing chat history operations. +Provides REST API endpoints for listing sessions, messages, +and managing message ratings. +""" + +import json +import logging +import os +from dataclasses import dataclass +from datetime import datetime +from decimal import Decimal +from typing import Any, Dict, Optional + +import boto3 +from boto3.dynamodb.conditions import Key +from botocore.paginate import TokenEncoder + +# Configure logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +@dataclass +class AwsResources: + """Centralized AWS resource management""" + + dynamodb = boto3.resource("dynamodb") + dynamodb_client = boto3.client("dynamodb") + + def __post_init__(self): + # Initialize DynamoDB tables + self.sessions_table = self.dynamodb.Table(Config.SESSIONS_TABLE_NAME) + self.messages_table = self.dynamodb.Table(Config.MESSAGES_TABLE_NAME) + + +class Config: + """Configuration constants""" + + SESSIONS_TABLE_NAME = os.environ["SESSIONS_TABLE_NAME"] + MESSAGES_TABLE_NAME = os.environ["MESSAGES_TABLE_NAME"] + SESSIONS_BY_TIMESTAMP_INDEX = os.environ["SESSIONS_BY_TIMESTAMP_INDEX_NAME"] + MESSAGES_BY_SESSION_ID_INDEX = os.environ["MESSAGES_BY_SESSION_ID_INDEX_NAME"] + DEFAULT_PAGE_SIZE = 50 + DEFAULT_MAX_ITEMS = 50 + + CORS_HEADERS = { + "Content-Type": "application/json", + "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", + } + + +# Initialize AWS resources +aws_resources = AwsResources() +token_encoder = TokenEncoder() + + +class DecimalEncoder(json.JSONEncoder): + """Custom JSON encoder for Decimal types""" + + def default(self, o): + if isinstance(o, Decimal): + return str(o) + return super(DecimalEncoder, self).default(o) + + +class PaginationConfig: + @staticmethod + def get_query_parameter(event: Dict[str, Any], parameter_name: str, default_value: Any = None) -> Any: + """Extract query parameter from event with default value""" + if event.get("queryStringParameters") and parameter_name in event["queryStringParameters"]: + return event["queryStringParameters"][parameter_name] + return default_value + + @classmethod + def get_pagination_config(cls, event: Dict[str, Any]) -> Dict[str, Any]: + """Build pagination configuration from event parameters""" + return { + "MaxItems": int(cls.get_query_parameter(event, "max_items", Config.DEFAULT_MAX_ITEMS)), + "PageSize": int(cls.get_query_parameter(event, "page_size", Config.DEFAULT_PAGE_SIZE)), + "StartingToken": cls.get_query_parameter(event, "starting_token"), + } + + +class ChatHistoryManager: + """Handles chat history related database operations""" + + @staticmethod + def get_session(session_id: str, user_id: str) -> Optional[Dict]: + """Retrieve session details from DynamoDB""" + response = aws_resources.sessions_table.get_item(Key={"sessionId": session_id, "userId": user_id}) + return response.get("Item") + + @staticmethod + def get_message(message_id: str, session_id: str) -> Optional[Dict]: + """Retrieve message details from DynamoDB""" + response = aws_resources.messages_table.get_item(Key={"messageId": message_id, "sessionId": session_id}) + return response.get("Item") + + @staticmethod + def list_sessions(user_id: str, pagination_config: Dict[str, Any]) -> Dict[str, Any]: + """List sessions for a user with pagination""" + paginator = aws_resources.dynamodb_client.get_paginator("query") + + response_iterator = paginator.paginate( + TableName=Config.SESSIONS_TABLE_NAME, + IndexName=Config.SESSIONS_BY_TIMESTAMP_INDEX, + KeyConditionExpression="userId = :user_id", + ExpressionAttributeValues={":user_id": {"S": user_id}}, + ScanIndexForward=False, + PaginationConfig=pagination_config, + ) + + return ChatHistoryManager._process_paginated_response( + response_iterator, + ["sessionId", "userId", "createTimestamp", "latestQuestion"], + pagination_config=pagination_config, + ) + + @staticmethod + def list_messages(session_id: str, pagination_config: Dict[str, Any]) -> Dict[str, Any]: + """List messages for a session with pagination""" + paginator = aws_resources.dynamodb_client.get_paginator("query") + + response_iterator = paginator.paginate( + TableName=Config.MESSAGES_TABLE_NAME, + IndexName=Config.MESSAGES_BY_SESSION_ID_INDEX, + KeyConditionExpression="sessionId = :session_id", + ExpressionAttributeValues={":session_id": {"S": session_id}}, + ScanIndexForward=False, + PaginationConfig=pagination_config, + ) + + return ChatHistoryManager._process_paginated_response( + response_iterator, + ["messageId", "role", "content", "createTimestamp"], + pagination_config=pagination_config, + is_messages_list=True, + ) + + @staticmethod + def _process_paginated_response( + response_iterator, keys: list, pagination_config: Dict[str, Any] = None, is_messages_list: bool = False + ) -> Dict[str, Any]: + """Process paginated responses from DynamoDB""" + output = {} + processed_items = [] + + for page in response_iterator: + items = page["Items"] + + for item in items: + processed_item = {key: item.get(key, {"S": ""})["S"] for key in keys} + # special handling for AI messages while listing messages + if is_messages_list and item.get("role", {}).get("S") == "ai": + processed_item["additional_kwargs"] = json.loads(item["additional_kwargs"]["S"]) + processed_items.append(processed_item) + + if "LastEvaluatedKey" in page: + output["LastEvaluatedKey"] = token_encoder.encode({"ExclusiveStartKey": page["LastEvaluatedKey"]}) + break + + # Sort based on createTimestamp + # For sessions list: descending order (newest first) + # For messages list: ascending order (oldest first) + if "createTimestamp" in keys: + processed_items.sort( + key=lambda x: x["createTimestamp"], + reverse=not is_messages_list, # False for messages (ascending), True for sessions (descending) + ) + + output["Items"] = processed_items + output["Config"] = pagination_config + output["Count"] = len(processed_items) + return output + + @staticmethod + def add_feedback( + session_id: str, user_id: str, message_id: str, feedback_type: str, feedback_reason: str, suggest_message: Dict + ) -> Dict[str, Any]: + """Add feedback to a message""" + # First verify the session belongs to the user + session = ChatHistoryManager.get_session(session_id, user_id) + if not session: + return {"added": False, "error": "Session not found or unauthorized"} + + message = ChatHistoryManager.get_message(message_id, session_id) + if not message: + return {"added": False, "error": "Message not found"} + + try: + current_timestamp = datetime.utcnow().isoformat() + "Z" + + # Update message with feedback + aws_resources.messages_table.update_item( + Key={"messageId": message_id, "sessionId": session_id}, + UpdateExpression="SET feedbackType = :ft, feedbackReason = :fr, suggestMessage = :sm, lastModifiedTimestamp = :t", + ExpressionAttributeValues={ + ":ft": feedback_type, + ":fr": feedback_reason, + ":sm": suggest_message, + ":t": current_timestamp, + }, + ) + + # Update session last modified time + aws_resources.sessions_table.update_item( + Key={"sessionId": session_id, "userId": user_id}, + UpdateExpression="SET lastModifiedTimestamp = :t", + ExpressionAttributeValues={":t": current_timestamp}, + ) + + return {"added": True} + except Exception as e: + logger.error("Error adding feedback: %s", str(e)) + return {"added": False, "error": str(e)} + + +class ApiResponse: + """Standardized API response handler""" + + @staticmethod + def success(data: Any, status_code: int = 200) -> Dict: + return {"statusCode": status_code, "headers": Config.CORS_HEADERS, "body": json.dumps(data, cls=DecimalEncoder)} + + @staticmethod + def error(message: str, status_code: int = 500) -> Dict: + logger.error("Error: %s", message) + return {"statusCode": status_code, "headers": Config.CORS_HEADERS, "body": json.dumps({"error": str(message)})} + + +class ApiHandler: + """API endpoint handlers""" + + @staticmethod + def list_sessions(event: Dict) -> Dict: + """Handle GET /chat-history/sessions endpoint""" + try: + claims = json.loads(event["requestContext"]["authorizer"]["claims"]) + user_id = claims["cognito:username"] + pagination_config = PaginationConfig.get_pagination_config(event) + result = ChatHistoryManager.list_sessions(user_id, pagination_config) + return ApiResponse.success(result) + except Exception as e: + return ApiResponse.error(str(e)) + + @staticmethod + def list_messages(event: Dict) -> Dict: + """Handle GET /chat-history/sessions/{sessionId}/messages endpoint""" + try: + session_id = event["pathParameters"]["sessionId"] + pagination_config = PaginationConfig.get_pagination_config(event) + result = ChatHistoryManager.list_messages(session_id, pagination_config) + return ApiResponse.success(result) + except Exception as e: + return ApiResponse.error(str(e)) + + @staticmethod + def add_feedback(event: Dict) -> Dict: + """Handle POST /sessions/{sessionId}/messages/{messageId}/feedback endpoint""" + try: + # Extract path parameters + session_id = event["pathParameters"]["sessionId"] + message_id = event["pathParameters"]["messageId"] + claims = json.loads(event["requestContext"]["authorizer"]["claims"]) + user_id = claims["cognito:username"] + + # Parse request body + body = json.loads(event["body"]) + result = ChatHistoryManager.add_feedback( + session_id=session_id, + user_id=user_id, + message_id=message_id, + feedback_type=body["feedback_type"], + feedback_reason=body["feedback_reason"], + suggest_message=body["suggest_message"], + ) + return ApiResponse.success(result) + except Exception as e: + return ApiResponse.error(str(e)) + + +def lambda_handler(event: Dict, context: Any) -> Dict: + """Routes API requests to appropriate handlers based on HTTP method and path""" + logger.info("Received event: %s", json.dumps(event)) + + routes = { + # More RESTful paths + ("GET", "/sessions"): ApiHandler.list_sessions, + ("GET", "/sessions/{sessionId}/messages"): ApiHandler.list_messages, + ("POST", "/sessions/{sessionId}/messages/{messageId}/feedback"): ApiHandler.add_feedback, + } + + handler = routes.get((event["httpMethod"], event["resource"])) + if not handler: + return ApiResponse.error("Route not found", 404) + + return handler(event) diff --git a/source/lambda/ddb/list_messages.py b/source/lambda/ddb/list_messages.py deleted file mode 100644 index 97effbc50..000000000 --- a/source/lambda/ddb/list_messages.py +++ /dev/null @@ -1,100 +0,0 @@ -import json -import logging -import os - -import boto3 -from botocore.paginate import TokenEncoder - -DEFAULT_MAX_ITEMS = 50 -DEFAULT_SIZE = 50 -logger = logging.getLogger() -logger.setLevel(logging.INFO) -client = boto3.client("dynamodb") -encoder = TokenEncoder() - -dynamodb = boto3.resource("dynamodb") -messages_table_name = os.getenv("MESSAGES_TABLE_NAME") -messages_table_gsi_name = os.getenv("MESSAGES_BY_SESSION_ID_INDEX_NAME") - -resp_header = { - "Content-Type": "application/json", - "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*", -} - - -def get_query_parameter(event, parameter_name, default_value=None): - if ( - event.get("queryStringParameters") - and parameter_name in event["queryStringParameters"] - ): - return event["queryStringParameters"][parameter_name] - return default_value - - -def lambda_handler(event, context): - - logger.info(event) - - max_items = get_query_parameter(event, "max_items", DEFAULT_MAX_ITEMS) - page_size = get_query_parameter(event, "page_size", DEFAULT_SIZE) - starting_token = get_query_parameter(event, "starting_token") - session_id = get_query_parameter(event, "session_id") - - config = { - "MaxItems": int(max_items), - "PageSize": int(page_size), - "StartingToken": starting_token, - } - - # Use query after adding a filter - paginator = client.get_paginator("query") - - response_iterator = paginator.paginate( - TableName=messages_table_name, - IndexName=messages_table_gsi_name, - PaginationConfig=config, - KeyConditionExpression="sessionId = :session_id", - ExpressionAttributeValues={":session_id": {"S": session_id}}, - ScanIndexForward=False, - ) - - output = {} - for page in response_iterator: - print(page) - page_items = page["Items"] - page_json = [] - for item in page_items: - item_json = {} - for key in ["role", "content", "createTimestamp"]: - item_json[key] = item[key]["S"] - if item["role"]["S"] == "ai": - item_json["additional_kwargs"] = json.loads(item["additional_kwargs"]["S"]) - page_json.append(item_json) - - if "LastEvaluatedKey" in page: - output["LastEvaluatedKey"] = encoder.encode( - {"ExclusiveStartKey": page["LastEvaluatedKey"]} - ) - break - - chat_history = sorted(page_json, key=lambda x: x["createTimestamp"]) - output["Items"] = chat_history - output["Config"] = config - output["Count"] = len(chat_history) - - try: - return { - "statusCode": 200, - "headers": resp_header, - "body": json.dumps(output), - } - except Exception as e: - logger.error("Error: %s", str(e)) - - return { - "statusCode": 500, - "headers": resp_header, - "body": json.dumps(f"Error: {str(e)}"), - } diff --git a/source/lambda/ddb/list_sessions.py b/source/lambda/ddb/list_sessions.py deleted file mode 100644 index 9e82882ef..000000000 --- a/source/lambda/ddb/list_sessions.py +++ /dev/null @@ -1,113 +0,0 @@ -import json -import logging -import os - -import boto3 -from botocore.paginate import TokenEncoder - -DEFAULT_MAX_ITEMS = 50 -DEFAULT_SIZE = 50 -logger = logging.getLogger() -logger.setLevel(logging.INFO) -client = boto3.client("dynamodb") -encoder = TokenEncoder() - -dynamodb = boto3.resource("dynamodb") -sessions_table_name = os.getenv("SESSIONS_TABLE_NAME") -sessions_table_gsi_name = os.getenv("SESSIONS_BY_TIMESTAMP_INDEX_NAME") - -resp_header = { - "Content-Type": "application/json", - "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*", -} - - -def get_query_parameter(event, parameter_name, default_value=None): - if ( - event.get("queryStringParameters") - and parameter_name in event["queryStringParameters"] - ): - return event["queryStringParameters"][parameter_name] - return default_value - - -def lambda_handler(event, context): - - logger.info(event) - authorizer_type = ( - event["requestContext"].get("authorizer", {}).get("authorizerType") - ) - if authorizer_type == "lambda_authorizer": - claims = json.loads(event["requestContext"]["authorizer"]["claims"]) - if "cognito:username" in claims: - cognito_username = claims["cognito:username"] - else: - cognito_username = get_query_parameter( - event, "UserName", "default_user_id" - ) - else: - logger.error("Invalid authorizer type") - return { - "statusCode": 403, - "headers": resp_header, - "body": json.dumps({"error": "Invalid authorizer type"}), - } - - max_items = get_query_parameter(event, "max_items", DEFAULT_MAX_ITEMS) - page_size = get_query_parameter(event, "page_size", DEFAULT_SIZE) - starting_token = get_query_parameter(event, "starting_token") - - config = { - "MaxItems": int(max_items), - "PageSize": int(page_size), - "StartingToken": starting_token, - } - - # Use query after adding a filter - paginator = client.get_paginator("query") - - response_iterator = paginator.paginate( - TableName=sessions_table_name, - IndexName=sessions_table_gsi_name, - PaginationConfig=config, - KeyConditionExpression="userId = :user_id", - ExpressionAttributeValues={":user_id": {"S": cognito_username}}, - ScanIndexForward=False, - ) - - output = {} - - for page in response_iterator: - page_items = page["Items"] - page_json = [] - for item in page_items: - item_json = {} - for key in ["sessionId", "userId", "createTimestamp", "latestQuestion"]: - item_json[key] = item.get(key, {"S": ""})["S"] - page_json.append(item_json) - output["Items"] = page_json - if "LastEvaluatedKey" in page: - output["LastEvaluatedKey"] = encoder.encode( - {"ExclusiveStartKey": page["LastEvaluatedKey"]} - ) - break - - output["Config"] = config - output["Count"] = len(page_json) - - try: - return { - "statusCode": 200, - "headers": resp_header, - "body": json.dumps(output), - } - except Exception as e: - logger.error("Error: %s", str(e)) - - return { - "statusCode": 500, - "headers": resp_header, - "body": json.dumps(f"Error: {str(e)}"), - } diff --git a/source/lambda/ddb/rating.py b/source/lambda/ddb/rating.py deleted file mode 100644 index 11c233661..000000000 --- a/source/lambda/ddb/rating.py +++ /dev/null @@ -1,207 +0,0 @@ -import json -import logging -import os -import time -from decimal import Decimal - -import boto3 -from botocore.exceptions import ClientError - -logger = logging.getLogger() -# logging.basicConfig(format='%(asctime)s,%(module)s,%(processName)s,%(levelname)s,%(message)s', level=logging.INFO, stream=sys.stderr) -logger.setLevel(logging.INFO) - - -# Custom JSON encoder to handle decimal values -class DecimalEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, Decimal): - return str(o) # Convert decimal to string - return super(DecimalEncoder, self).default(o) - - -""" -Sample Item: -{'userId': '268b8afa-3d5a-4147-9707-1975415a1732', -'History': [{'type': 'human', 'data': {'type': 'human', 'content': 'Hi', 'additional_kwargs': {}, 'example': False}}, -{'type': 'ai', 'data': {'type': 'ai', 'content': ' Hello!', 'additional_kwargs': {'mode': 'chain', 'modelKwargs': {'maxTokens': Decimal('512'), 'temperature': Decimal('0.6'), 'streaming': True, 'topP': Decimal('0.9')}, 'modelId': 'anthropic.claude-v2', 'documents': [], 'sessionId': 'cc8700e8-f8ea-4f43-8951-964d813e5a96', 'userId': '268b8afa-3d5a-4147-9707-1975415a1732', 'prompts': [['\n\nHuman: The following is a friendly conversation between a human and an AI. If the AI does not know the answer to a question, it truthfully says it does not know.\n\nCurrent conversation:\n\n\nQuestion: Hi\n\nAssistant:']]}, 'example': False}}], -'sessionId': 'cc8700e8-f8ea-4f43-8951-964d813e5a96', -'StartTime': '2023-12-25T06:52:42.618249'} -""" - - -def get_session(sessions_table, session_id, user_id): - response = {} - try: - response = sessions_table.get_item( - Key={"sessionId": session_id, "userId": user_id} - ) - except ClientError as error: - if error.response["Error"]["Code"] == "ResourceNotFoundException": - print("No record found with session id: %s", session_id) - else: - print(error) - - return response.get("Item", {}) - - -def get_message(messages_table, message_id, session_id): - response = {} - try: - response = messages_table.get_item( - Key={"messageId": message_id, "sessionId": session_id} - ) - except ClientError as error: - if error.response["Error"]["Code"] == "ResourceNotFoundException": - print("No record found with message id: %s", message_id) - else: - print(error) - - return response.get("Item", {}) - - -def add_feedback( - sessions_table, - messages_table, - session_id, - user_id, - message_id, - feedback_type, - feedback_reason, - suggest_message, -) -> None: - """ - Sample feedback: - { - "type" : "thumbs_down", - "suggest_message" : { - "role": "user", - "content": "标准回答, abc..", - } - } - """ - - message = get_message(messages_table, message_id, session_id) - - if not message: - return { - "added": False, - "error": "Failed to add feedback. No messages found in session.", - } - - try: - current_timestamp = Decimal.from_float(time.time()) - messages_table.update_item( - Key={"messageId": message_id, "sessionId": session_id}, - UpdateExpression="SET feedbackType = :ft, feedbackReason = :fr, suggestMessage = :sm, lastModifiedTimestamp = :t", - ExpressionAttributeValues={ - ":ft": feedback_type, - ":fr": feedback_reason, - ":sm": suggest_message, - ":t": current_timestamp, - }, - ReturnValues="UPDATED_NEW", - ) - sessions_table.update_item( - Key={"sessionId": session_id, "userId": user_id}, - UpdateExpression="SET lastModifiedTimestamp = :t", - ExpressionAttributeValues={":t": current_timestamp}, - ReturnValues="UPDATED_NEW", - ) - response = {"added": True} - - except Exception as err: - print(err) - response = {"added": False, "error": str(err)} - - return response - - -def get_feedback(messages_table, message_id, session_id): - message = get_message(messages_table, message_id, session_id) - - if message: - return { - "feedback_type": message.get("feedbackType", ""), - "feedback_reason": message.get("feedbackReason", ""), - "suggest_message": message.get("suggestMessage", ""), - } - else: - return {} - - -def lambda_handler(event, context): - dynamodb = boto3.resource("dynamodb") - sessions_table_name = os.getenv("SESSIONS_TABLE_NAME") - messages_table_name = os.getenv("MESSAGES_TABLE_NAME") - - sessions_table = dynamodb.Table(sessions_table_name) - messages_table = dynamodb.Table(messages_table_name) - - http_method = event["httpMethod"] - body = json.loads(event["body"]) - - required_fields = ["operation"] - - if not all(field in body for field in required_fields): - return { - "statusCode": 400, - "body": json.dumps({"message": "Missing required fields"}), - } - - operation = body["operation"] - session_id = body.get("session_id", "") - user_id = body.get("user_id", "default_user_id") - message_id = body.get("message_id", None) - feedback_type = body.get("feedback_type", None) - feedback_reason = body.get("feedback_reason", None) - suggest_message = body.get("suggest_message", None) - - operations_mapping = { - "POST": { - "get_session": lambda: get_session(sessions_table, session_id, user_id), - "get_message": lambda: get_message(messages_table, message_id, session_id), - "add_feedback": lambda: add_feedback( - sessions_table, - messages_table, - session_id, - user_id, - message_id, - feedback_type, - feedback_reason, - suggest_message, - ), - "get_feedback": lambda: get_feedback( - messages_table, message_id, session_id - ), - } - } - - resp_header = { - "Content-Type": "application/json", - "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*", - } - - try: - if ( - http_method in operations_mapping - and operation in operations_mapping[http_method] - ): - response = operations_mapping[http_method][operation]() - logger.info( - "http_method: {}, operation: {}, response: {}".format( - http_method, operation, response - ) - ) - return { - "statusCode": 200, - "headers": resp_header, - "body": json.dumps(response, cls=DecimalEncoder), - } - else: - raise Exception(f"Invalid {http_method} operation: {operation}") - except Exception as e: - # Return an error response - return {"statusCode": 500, "body": json.dumps({"error": str(e)})} From 3dc929ba9a70b2045095ed9cc587a7562fc1f37c Mon Sep 17 00:00:00 2001 From: Xu Han Date: Mon, 2 Dec 2024 10:05:43 +0000 Subject: [PATCH 08/12] chore: format front end code --- source/portal/src/pages/chatbot/ChatBot.tsx | 88 ++++++++++----------- source/portal/src/types/index.ts | 4 +- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/source/portal/src/pages/chatbot/ChatBot.tsx b/source/portal/src/pages/chatbot/ChatBot.tsx index 5b9ee02c9..419dcaa77 100644 --- a/source/portal/src/pages/chatbot/ChatBot.tsx +++ b/source/portal/src/pages/chatbot/ChatBot.tsx @@ -91,23 +91,23 @@ const ChatBot: React.FC = (props: ChatBotProps) => { // ); const [chatbotList, setChatbotList] = useState([]); const [chatbotOption, setChatbotOption] = useState(null as any); - const [useChatHistory, setUseChatHistory] = useState(localStorage.getItem(USE_CHAT_HISTORY)==null || localStorage.getItem(USE_CHAT_HISTORY)=="true" ?true:false); - const [enableTrace, setEnableTrace] = useState(localStorage.getItem(ENABLE_TRACE)==null || localStorage.getItem(ENABLE_TRACE)=="true" ?true:false); + const [useChatHistory, setUseChatHistory] = useState(localStorage.getItem(USE_CHAT_HISTORY) == null || localStorage.getItem(USE_CHAT_HISTORY) == "true" ? true : false); + const [enableTrace, setEnableTrace] = useState(localStorage.getItem(ENABLE_TRACE) == null || localStorage.getItem(ENABLE_TRACE) == "true" ? true : false); const [showTrace, setShowTrace] = useState(enableTrace); - const [onlyRAGTool, setOnlyRAGTool] = useState(localStorage.getItem(ONLY_RAG_TOOL)==null || localStorage.getItem(ONLY_RAG_TOOL)=="false" ?false:true); + const [onlyRAGTool, setOnlyRAGTool] = useState(localStorage.getItem(ONLY_RAG_TOOL) == null || localStorage.getItem(ONLY_RAG_TOOL) == "false" ? false : true); // const [useWebSearch, setUseWebSearch] = useState(false); // const [googleAPIKey, setGoogleAPIKey] = useState(''); const [retailGoods, setRetailGoods] = useState( RETAIL_GOODS_LIST[0], ); const [scenario, setScenario] = useState( - localScenario==null?SCENARIO_LIST[0]:JSON.parse(localScenario), + localScenario == null ? SCENARIO_LIST[0] : JSON.parse(localScenario), ); const [sessionId, setSessionId] = useState(historySessionId); - const [temperature, setTemperature] = useState(localTemperature?localTemperature:'0.01'); - const [maxToken, setMaxToken] = useState(localMaxToken?localMaxToken:'1000'); + const [temperature, setTemperature] = useState(localTemperature ? localTemperature : '0.01'); + const [maxToken, setMaxToken] = useState(localMaxToken ? localMaxToken : '1000'); const [endPoint, setEndPoint] = useState(''); const [showEndpoint, setShowEndpoint] = useState(false); @@ -115,7 +115,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { const [showMessageError, setShowMessageError] = useState(false); // const [googleAPIKeyError, setGoogleAPIKeyError] = useState(false); const [isMessageEnd, setIsMessageEnd] = useState(false); - const [additionalConfig, setAdditionalConfig] = useState(localConfig?localConfig:''); + const [additionalConfig, setAdditionalConfig] = useState(localConfig ? localConfig : ''); // validation const [modelError, setModelError] = useState(''); @@ -150,8 +150,8 @@ const ChatBot: React.FC = (props: ChatBotProps) => { } ); setChatbotList(getChatbots); - const localChatBot =localStorage.getItem(CURRENT_CHAT_BOT) - setChatbotOption(localChatBot!==null?JSON.parse(localChatBot):getChatbots[0]) + const localChatBot = localStorage.getItem(CURRENT_CHAT_BOT) + setChatbotOption(localChatBot !== null ? JSON.parse(localChatBot) : getChatbots[0]) // setChatbotOption(getChatbots[0]) } catch (error) { console.error(error); @@ -204,21 +204,21 @@ const ChatBot: React.FC = (props: ChatBotProps) => { } else { setSessionId(uuidv4()); } - getWorkspaceList(); + getWorkspaceList(); }, []); - useEffect(()=>{ - if(chatbotOption){ - localStorage.setItem(CURRENT_CHAT_BOT, JSON.stringify(chatbotOption)) + useEffect(() => { + if (chatbotOption) { + localStorage.setItem(CURRENT_CHAT_BOT, JSON.stringify(chatbotOption)) } - },[chatbotOption]) + }, [chatbotOption]) - useEffect(()=>{ - localStorage.setItem(USE_CHAT_HISTORY, useChatHistory?"true":"false") - },[useChatHistory]) + useEffect(() => { + localStorage.setItem(USE_CHAT_HISTORY, useChatHistory ? "true" : "false") + }, [useChatHistory]) useEffect(() => { - localStorage.setItem(ENABLE_TRACE, enableTrace?"true":"false") + localStorage.setItem(ENABLE_TRACE, enableTrace ? "true" : "false") if (enableTrace) { setShowTrace(true); } else { @@ -226,39 +226,39 @@ const ChatBot: React.FC = (props: ChatBotProps) => { } }, [enableTrace]); - useEffect(()=>{ - if(scenario){ + useEffect(() => { + if (scenario) { localStorage.setItem(SCENARIO, JSON.stringify(scenario)) - } - },[scenario]) + } + }, [scenario]) - useEffect(()=>{ - localStorage.setItem(ONLY_RAG_TOOL, onlyRAGTool?"true":"false") - },[onlyRAGTool]) + useEffect(() => { + localStorage.setItem(ONLY_RAG_TOOL, onlyRAGTool ? "true" : "false") + }, [onlyRAGTool]) - useEffect(()=>{ - if(modelOption){ + useEffect(() => { + if (modelOption) { localStorage.setItem(MODEL_OPTION, modelOption) } - },[modelOption]) + }, [modelOption]) - useEffect(()=>{ - if(maxToken){ + useEffect(() => { + if (maxToken) { localStorage.setItem(MAX_TOKEN, maxToken) - } - },[maxToken]) + } + }, [maxToken]) - useEffect(()=>{ - if(temperature){ + useEffect(() => { + if (temperature) { localStorage.setItem(TEMPERATURE, temperature) - } - },[temperature]) + } + }, [temperature]) - useEffect(()=>{ - if(additionalConfig){ + useEffect(() => { + if (additionalConfig) { localStorage.setItem(ADITIONAL_SETTRINGS, additionalConfig) - } - },[additionalConfig]) + } + }, [additionalConfig]) const handleAIMessage = (message: MessageDataType) => { console.info('handleAIMessage:', message); @@ -385,7 +385,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { user_id: auth?.user?.profile?.['cognito:username'] || 'default_user_id', chatbot_config: { group_name: groupName?.[0] ?? 'Admin', - chatbot_id: chatbotOption.value?? 'admin', + chatbot_id: chatbotOption.value ?? 'admin', goods_id: retailGoods.value, chatbot_mode: 'agent', use_history: useChatHistory, @@ -447,8 +447,8 @@ const ChatBot: React.FC = (props: ChatBotProps) => { }; }); setModelList(optionList); - - + + } else if (scenario.value === 'retail') { optionList = LLM_BOT_RETAIL_MODEL_LIST.map((item) => { return { @@ -460,7 +460,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { // TODO // setModelOption(optionList?.[0]?.value ?? ''); } - if(localModel){ + if (localModel) { setModelOption(localModel) } else { setModelOption(optionList?.[0]?.value ?? ''); diff --git a/source/portal/src/types/index.ts b/source/portal/src/types/index.ts index b502a7d80..5a499fd49 100644 --- a/source/portal/src/types/index.ts +++ b/source/portal/src/types/index.ts @@ -13,7 +13,7 @@ export type LibraryListItem = { qaEnhance: string; operationType: string; sfnExecutionId: string; - indexType: string; + indexType: string; chatbotId: string; createTime: string; indexId: string; @@ -226,7 +226,7 @@ export type ChatbotItem = { export type ChatbotDetailResponse = { chatbotId: string; updateTime: string; - model: {model_endpoint: string, model_name: string}; + model: { model_endpoint: string, model_name: string }; index: IndexItem[]; }; From 9e6bf36f5deb4383a6358175fd39709ccb762433 Mon Sep 17 00:00:00 2001 From: Xu Han Date: Mon, 2 Dec 2024 11:54:49 +0000 Subject: [PATCH 09/12] feat: naive version of clicking feedback --- source/portal/src/pages/chatbot/ChatBot.tsx | 64 ++++++++++++++----- .../src/pages/history/SessionHistory.tsx | 5 +- source/portal/src/types/index.ts | 1 + 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/source/portal/src/pages/chatbot/ChatBot.tsx b/source/portal/src/pages/chatbot/ChatBot.tsx index 419dcaa77..66d3e74eb 100644 --- a/source/portal/src/pages/chatbot/ChatBot.tsx +++ b/source/portal/src/pages/chatbot/ChatBot.tsx @@ -33,6 +33,7 @@ import { MessageDataType, SessionMessage } from 'src/types'; import { isValidJson } from 'src/utils/utils'; interface MessageType { + messageId: string; type: 'ai' | 'human'; message: { data: string; @@ -65,6 +66,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { const [loadingHistory, setLoadingHistory] = useState(false); const [messages, setMessages] = useState([ { + messageId: uuidv4(), type: 'ai', message: { data: t('welcomeMessage'), @@ -83,6 +85,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { ); const [currentAIMessage, setCurrentAIMessage] = useState(''); const [currentMonitorMessage, setCurrentMonitorMessage] = useState(''); + const [currentAIMessageId, setCurrentAIMessageId] = useState(''); const [aiSpeaking, setAiSpeaking] = useState(false); const [modelOption, setModelOption] = useState(''); const [modelList, setModelList] = useState([]); @@ -163,10 +166,9 @@ const ChatBot: React.FC = (props: ChatBotProps) => { try { setLoadingHistory(true); const data = await fetchData({ - url: `chat-history/messages`, + url: `sessions/${historySessionId}/messages`, method: 'get', params: { - session_id: historySessionId, page_size: 9999, max_items: 9999, }, @@ -182,6 +184,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { // }); // } return { + messageId: msg.messageId, type: msg.role, message: { data: messageContent, @@ -281,6 +284,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { // }); } } else if (message.message_type === 'END') { + setCurrentAIMessageId(message.message_id); setIsMessageEnd(true); } }; @@ -305,6 +309,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { return [ ...prev, { + messageId: currentAIMessageId, type: 'ai', message: { data: currentAIMessage, @@ -425,6 +430,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { return [ ...prev, { + messageId: '', type: 'human', message: { data: userMessage, @@ -476,6 +482,15 @@ const ChatBot: React.FC = (props: ChatBotProps) => { } }, [modelOption]); + const handleThumbUpClick = (index: number) => { + console.log(`${messages[index].messageId}`) + console.log(`Thumb up clicked for message at index ${index}, sessionId: ${sessionId}, messageId: ${messages[index].messageId}`); + }; + + const handleThumbDownClick = (index: number) => { + console.log(`Thumb down clicked for message at index ${index}, sessionId: ${sessionId}, messageId: ${messages[index].message.data}`); + }; + return ( = (props: ChatBotProps) => {
{messages.map((msg, index) => ( - +
+ + {msg.type === 'ai' && ( +
+ + +
+ )} +
))} {aiSpeaking && ( - +
+ + {isMessageEnd && ( +
+ + +
+ )} +
)}
diff --git a/source/portal/src/pages/history/SessionHistory.tsx b/source/portal/src/pages/history/SessionHistory.tsx index c580c4a74..19abc0d05 100644 --- a/source/portal/src/pages/history/SessionHistory.tsx +++ b/source/portal/src/pages/history/SessionHistory.tsx @@ -50,7 +50,7 @@ const SessionHistory: React.FC = () => { }; try { const data = await fetchData({ - url: 'chat-history/sessions', + url: 'sessions', method: 'get', params, }); @@ -130,8 +130,7 @@ const SessionHistory: React.FC = () => { selectedItems={selectedItems} ariaLabels={{ allItemsSelectionLabel: ({ selectedItems }) => - `${selectedItems.length} ${ - selectedItems.length === 1 ? t('item') : t('items') + `${selectedItems.length} ${selectedItems.length === 1 ? t('item') : t('items') } ${t('selected')}`, }} columnDefinitions={[ diff --git a/source/portal/src/types/index.ts b/source/portal/src/types/index.ts index 5a499fd49..799562a2d 100644 --- a/source/portal/src/types/index.ts +++ b/source/portal/src/types/index.ts @@ -73,6 +73,7 @@ export type SessionHistoryResponse = { }; export type SessionMessage = { + messageId: string; role: 'ai' | 'human'; content: string; createTimestamp: string; From 49aeef2990148033f519bdb4f3500b2e64d6abb5 Mon Sep 17 00:00:00 2001 From: Xu Han Date: Mon, 2 Dec 2024 16:16:33 +0000 Subject: [PATCH 10/12] feat: remove rating for first sentence and move rating to right side of chat --- source/portal/src/pages/chatbot/ChatBot.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/portal/src/pages/chatbot/ChatBot.tsx b/source/portal/src/pages/chatbot/ChatBot.tsx index 66d3e74eb..bb28169c0 100644 --- a/source/portal/src/pages/chatbot/ChatBot.tsx +++ b/source/portal/src/pages/chatbot/ChatBot.tsx @@ -505,8 +505,8 @@ const ChatBot: React.FC = (props: ChatBotProps) => { type={msg.type} message={msg.message} /> - {msg.type === 'ai' && ( -
+ {msg.type === 'ai' && index !== 0 && ( +
@@ -525,7 +525,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { }} /> {isMessageEnd && ( -
+
From 2130ebef4efccf3e15f5f45533c638b249fbacb8 Mon Sep 17 00:00:00 2001 From: Xu Han Date: Mon, 2 Dec 2024 17:22:27 +0000 Subject: [PATCH 11/12] feat: support withdraw feedback on frontend --- source/portal/src/pages/chatbot/ChatBot.tsx | 79 +++++++++++++++++---- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/source/portal/src/pages/chatbot/ChatBot.tsx b/source/portal/src/pages/chatbot/ChatBot.tsx index bb28169c0..bebe842f2 100644 --- a/source/portal/src/pages/chatbot/ChatBot.tsx +++ b/source/portal/src/pages/chatbot/ChatBot.tsx @@ -16,7 +16,7 @@ import { SpaceBetween, StatusIndicator, Textarea, - Toggle, + Toggle } from '@cloudscape-design/components'; import useWebSocket, { ReadyState } from 'react-use-websocket'; import { identity } from 'lodash'; @@ -482,13 +482,48 @@ const ChatBot: React.FC = (props: ChatBotProps) => { } }, [modelOption]); - const handleThumbUpClick = (index: number) => { - console.log(`${messages[index].messageId}`) - console.log(`Thumb up clicked for message at index ${index}, sessionId: ${sessionId}, messageId: ${messages[index].messageId}`); + const [feedbackGiven, setFeedbackGiven] = useState<{ [key: string]: 'thumb_up' | 'thumb_down' | null }>({}); + + const handleThumbUpClick = async (index: number) => { + const currentFeedback = feedbackGiven[index]; + const newFeedback = currentFeedback === 'thumb_up' ? null : 'thumb_up'; + + try { + await fetchData({ + url: `sessions/${sessionId}/messages/${messages[index].messageId}/feedback`, + method: 'post', + data: { + feedback_type: newFeedback || '', + feedback_reason: '', + suggest_message: '' + } + }); + setFeedbackGiven(prev => ({ ...prev, [index]: newFeedback })); + console.log('Thumb up feedback sent successfully'); + } catch (error) { + console.error('Error sending thumb up feedback:', error); + } }; - const handleThumbDownClick = (index: number) => { - console.log(`Thumb down clicked for message at index ${index}, sessionId: ${sessionId}, messageId: ${messages[index].message.data}`); + const handleThumbDownClick = async (index: number) => { + const currentFeedback = feedbackGiven[index]; + const newFeedback = currentFeedback === 'thumb_down' ? null : 'thumb_down'; + + try { + await fetchData({ + url: `sessions/${sessionId}/messages/${messages[index].messageId}/feedback`, + method: 'post', + data: { + feedback_type: newFeedback || '', + feedback_reason: '', + suggest_message: '' + } + }); + setFeedbackGiven(prev => ({ ...prev, [index]: newFeedback })); + console.log('Thumb down feedback sent successfully'); + } catch (error) { + console.error('Error sending thumb down feedback:', error); + } }; return ( @@ -506,9 +541,19 @@ const ChatBot: React.FC = (props: ChatBotProps) => { message={msg.message} /> {msg.type === 'ai' && index !== 0 && ( -
- - +
+
)}
@@ -525,9 +570,19 @@ const ChatBot: React.FC = (props: ChatBotProps) => { }} /> {isMessageEnd && ( -
- - +
+
)}
From a2c65653ffc0f12197edba83d486139140f3bba5 Mon Sep 17 00:00:00 2001 From: NingLyu Date: Tue, 3 Dec 2024 04:06:29 +0000 Subject: [PATCH 12/12] feat: support showing markdown image --- .../online/common_logic/common_utils/prompt_utils.py | 1 + source/portal/src/pages/chatbot/ChatBot.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index 25f81c0a3..14024f701 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -128,6 +128,7 @@ def prompt_template_render(self, prompt_template: dict): CLAUDE_RAG_SYSTEM_PROMPT = """You are a customer service agent, and answering user's query. You ALWAYS follow these response rules when writing your response: +- 如果 里面的内容包含markdown格式的图片,如 ![image](https://www.demo.com/demo.png),请保留这个markdown格式的图片,并将他原封不动的输出到回答内容的最后,注意:不要修改这个markdown格式的图片. - NERVER say "根据搜索结果/大家好/谢谢/根据这个文档...". - 回答简单明了 - 如果问题与 里面的内容不相关,直接回答 "根据内部知识库,找不到相关内容。" diff --git a/source/portal/src/pages/chatbot/ChatBot.tsx b/source/portal/src/pages/chatbot/ChatBot.tsx index 5b9ee02c9..b91ed85b8 100644 --- a/source/portal/src/pages/chatbot/ChatBot.tsx +++ b/source/portal/src/pages/chatbot/ChatBot.tsx @@ -176,11 +176,11 @@ const ChatBot: React.FC = (props: ChatBotProps) => { sessionMessage.map((msg) => { let messageContent = msg.content; // Handle AI images message - // if (msg.role === 'ai' && msg.additional_kwargs?.figure?.length > 0) { - // msg.additional_kwargs.figure.forEach((item) => { - // messageContent += ` \n ![${item.content_type}](/${encodeURIComponent(item.figure_path)})`; - // }); - // } + if (msg.role === 'ai' && msg.additional_kwargs?.figure?.length > 0) { + msg.additional_kwargs.figure.forEach((item) => { + messageContent += ` \n ![${item.content_type}](/${encodeURIComponent(item.figure_path)})`; + }); + } return { type: msg.role, message: {