diff --git a/source/infrastructure/lib/api/api-stack.ts b/source/infrastructure/lib/api/api-stack.ts index a3bac1d96..7c26f14d7 100644 --- a/source/infrastructure/lib/api/api-stack.ts +++ b/source/infrastructure/lib/api/api-stack.ts @@ -11,10 +11,10 @@ * and limitations under the License. * *********************************************************************************************************************/ -import { Aws, Duration, Size, StackProps } from "aws-cdk-lib"; +import { Aws, Size, StackProps } from "aws-cdk-lib"; import * as apigw from "aws-cdk-lib/aws-apigateway"; import * as s3 from "aws-cdk-lib/aws-s3"; -import { Function, Runtime, Code, Architecture } from 'aws-cdk-lib/aws-lambda'; +import { Runtime, Code } from 'aws-cdk-lib/aws-lambda'; import { JsonSchemaType, JsonSchemaVersion, Model } from "aws-cdk-lib/aws-apigateway"; import { Construct } from "constructs"; import { join } from "path"; @@ -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,29 +368,32 @@ 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 }, }) - } + } ); } if (props.config.chat.enabled) { - const chatHistoryLambda = new LambdaFunction(this, "ChatHistoryLambda", { - handler: "rating.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, MESSAGES_TABLE_NAME: messagesTableName, @@ -396,27 +402,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")), - 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, - MESSAGES_BY_SESSION_ID_INDEX_NAME: "bySessionId", - }, - 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 +412,7 @@ export class ApiConstruct extends Construct { }, layers: [apiLambdaOnlineSourceLayer], statements: [this.iamHelper.dynamodbStatement, - this.iamHelper.logStatement], + this.iamHelper.logStatement], }); @@ -450,13 +436,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,20 +457,15 @@ export class ApiConstruct extends Construct { }, layers: [apiLambdaOnlineSourceLayer], statements: [this.iamHelper.dynamodbStatement, - this.iamHelper.logStatement], - }); - - // Define the API Gateway Lambda Integration with proxy and no integration responses - const lambdaChatHistoryIntegration = new apigw.LambdaIntegration(chatHistoryLambda.function, { - proxy: true, + this.iamHelper.logStatement], }); - 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, @@ -494,9 +475,38 @@ export class ApiConstruct extends Construct { const apiResourceCheckDefaultChatbot = apiResourceChatbotManagement.addResource('default-chatbot'); apiResourceCheckDefaultChatbot.addMethod("GET", lambdaChatbotIntegration, this.genMethodOption(api, auth, null)); const apiResourceCheckChatbot = apiResourceChatbotManagement.addResource('check-chatbot'); - apiResourceCheckChatbot.addMethod("POST", lambdaChatbotIntegration, this.genMethodOption(api, auth, null)); + apiResourceCheckChatbot.addMethod("POST", lambdaChatbotIntegration, { + ...this.genMethodOption(api, auth, { + item: {type: JsonSchemaType.STRING || JsonSchemaType.NULL}, + reason: {type: JsonSchemaType.STRING || JsonSchemaType.NULL}, + result: {type: JsonSchemaType.BOOLEAN} + }), + 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'] + }, + model: { "type": JsonSchemaType.STRING }, + type: { "type": JsonSchemaType.STRING } + } + ) + }); const apiResourceCheckIndex = apiResourceChatbotManagement.addResource('check-index'); - apiResourceCheckIndex.addMethod("POST", lambdaChatbotIntegration, this.genMethodOption(api, auth, null)); + apiResourceCheckIndex.addMethod("POST", lambdaChatbotIntegration, { + ...this.genMethodOption(api, auth, { + reason: {type: JsonSchemaType.STRING || JsonSchemaType.NULL}, + result: {type: JsonSchemaType.BOOLEAN} + }), + requestModels: this.genRequestModel(api, { + "index": { "type": JsonSchemaType.STRING }, + "model": { "type": JsonSchemaType.STRING }, + }) + }); const apiResourceListIndex = apiResourceChatbotManagement.addResource('indexes').addResource('{chatbotId}'); apiResourceListIndex.addMethod("GET", lambdaChatbotIntegration, this.genMethodOption(api, auth, null)); const apiResourceEditChatBot = apiResourceChatbotManagement.addResource('edit-chatbot'); @@ -504,16 +514,13 @@ 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: { - qq: { type: JsonSchemaType.STRING }, - qd: { type: JsonSchemaType.STRING }, - intention: { type: JsonSchemaType.STRING } - } + type: JsonSchemaType.ARRAY, + items: {type: JsonSchemaType.STRING} }, + modelType: {type: JsonSchemaType.STRING}, Message: {type: JsonSchemaType.STRING}, }), requestModels: this.genRequestModel(api, { @@ -524,41 +531,61 @@ export class ApiConstruct extends Construct { 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 } + required: ['qq','qd','intention'] + }, + modelId: { "type": JsonSchemaType.STRING }, + modelName: { "type": JsonSchemaType.STRING }, + operatorType: { "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 } + // }, + // 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'], + } }, - 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, + 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 +643,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 +706,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 +758,7 @@ export class ApiConstruct extends Construct { { ...this.genMethodOption(api, auth, { Items: { - type: JsonSchemaType.ARRAY, + type: JsonSchemaType.ARRAY, items: { type: JsonSchemaType.OBJECT, properties: { @@ -748,7 +778,7 @@ export class ApiConstruct extends Construct { } } }, - required: ['s3Path', 's3Prefix', 'createTime', 'status','executionId'], + required: ['s3Path', 's3Prefix', 'createTime', 'status', 'executionId'], } }, Count: { type: JsonSchemaType.INTEGER } @@ -760,8 +790,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 +823,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 +831,7 @@ export class ApiConstruct extends Construct { // const key = api.addApiKey('ApiKey', { // value: apiKeyValue, // }); - + // plan.addApiKey(key); // plan.addApiStage({ // stage: api.deploymentStage @@ -822,11 +852,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 +892,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, 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)})} diff --git a/source/lambda/etl/chatbot_management.py b/source/lambda/etl/chatbot_management.py index a638c4206..4c7dd8e14 100644 --- a/source/lambda/etl/chatbot_management.py +++ b/source/lambda/etl/chatbot_management.py @@ -360,9 +360,6 @@ def __edit_chatbot(event, group_name): update_time ) - - - # 3.更新index表 return { "chatbotId": chatbot_id, diff --git a/source/lambda/intention/intention.py b/source/lambda/intention/intention.py index b81f818c2..8c0503050 100644 --- a/source/lambda/intention/intention.py +++ b/source/lambda/intention/intention.py @@ -108,7 +108,7 @@ def __init__( self.docsearch = docsearch self.embedding_model_endpoint = embedding_model_endpoint - def aos_ingestion(self, documents: List[Document]) -> None: + def aos_ingestion(self, documents: List[Document], index: str) -> None: texts = [doc.page_content for doc in documents] metadatas = [doc.metadata for doc in documents] embeddings_vectors = self.docsearch.embedding_function.embed_documents( @@ -127,7 +127,7 @@ def aos_ingestion(self, documents: List[Document]) -> None: embeddings_vectors = embeddings_vectors_list metadatas = metadata_list self.docsearch._OpenSearchVectorSearch__add( - texts, embeddings_vectors, metadatas=metadatas + texts, embeddings_vectors, metadatas=metadatas, index=index ) @@ -435,7 +435,7 @@ def __save_2_aos(modelId: str, index: str, qaListParam: list, bucket: str, prefi worker = OpenSearchIngestionWorker(docsearch, embedding_model_endpoint) intention_list = convert_qa_list(qaList, bucket, prefix) - worker.aos_ingestion(intention_list) + worker.aos_ingestion(intention_list, index) else: index_exists = aos_client.indices.exists(index=index) if not index_exists: 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/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/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..c020eb623 100644 --- a/source/portal/package.json +++ b/source/portal/package.json @@ -24,6 +24,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", 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/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..6d888fc5a 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/chatbot/ChatBot.tsx b/source/portal/src/pages/chatbot/ChatBot.tsx index 5b9ee02c9..3471889d1 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'; @@ -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([]); @@ -91,23 +94,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 +118,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 +153,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); @@ -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, }, @@ -176,12 +178,13 @@ 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 { + messageId: msg.messageId, type: msg.role, message: { data: messageContent, @@ -204,21 +207,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 +229,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); @@ -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, @@ -385,7 +390,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, @@ -425,6 +430,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { return [ ...prev, { + messageId: '', type: 'human', message: { data: userMessage, @@ -447,8 +453,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 +466,7 @@ const ChatBot: React.FC = (props: ChatBotProps) => { // TODO // setModelOption(optionList?.[0]?.value ?? ''); } - if(localModel){ + if (localModel) { setModelOption(localModel) } else { setModelOption(optionList?.[0]?.value ?? ''); @@ -476,6 +482,50 @@ const ChatBot: React.FC = (props: ChatBotProps) => { } }, [modelOption]); + 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 = 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 ( = (props: ChatBotProps) => {
{messages.map((msg, index) => ( - +
+ + {msg.type === 'ai' && index !== 0 && ( +
+
+ )} +
))} {aiSpeaking && ( - +
+ + {isMessageEnd && ( +
+
+ )} +
)}
diff --git a/source/portal/src/pages/components/AddIntention.tsx b/source/portal/src/pages/components/AddIntention.tsx index 122cee93e..b6142a94a 100644 --- a/source/portal/src/pages/components/AddIntention.tsx +++ b/source/portal/src/pages/components/AddIntention.tsx @@ -76,6 +76,8 @@ const AddIntention: React.FC = (props: AddIntentionProps) => const [advanceExpand, setAdvanceExpand] = useState(false); // const [selectedIndexOption, setSelectedIndexOption] = useState(indexNameOptions[0]); + + const changeIndexOption = (option: SelectedOption)=>{ setSelectedIndexOption(option) setIndexName(option.value) @@ -90,7 +92,7 @@ const AddIntention: React.FC = (props: AddIntentionProps) => s3Prefix: prefix, chatbotId: selectedBotOption?.value.toLocaleLowerCase() ?? 'admin', // groupName: selectedBotOption?.value, - index: indexName ? indexName.trim() : undefined, + index: indexName ? indexName.trim() : indexNameOptions[0]?.value, model: model?.value ?? DEFAULT_EMBEDDING_MODEL, // tag: indexName ? indexName.trim() : undefined, }, 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/pages/home/Home.tsx b/source/portal/src/pages/home/Home.tsx index 990f11cd1..dc5f770ff 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,281 @@ 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: t('tour.home'), + disableBeacon: true, + }, + { + target: 'a[href="/chats"]', + content: t('tour.chat'), + disableBeacon: true, + }, + { + target: 'a[href="/sessions"]', + content: t('tour.session'), + disableBeacon: true, + }, + { + target: 'a[href="/chatbot-management"]', + content: t('tour.chatbot'), + disableBeacon: true, + }, + { + target: 'a[href="/intention"]', + content: t('tour.intention'), + disableBeacon: true, + }, + ]; + + const kbStep = { + target: 'a[href="/library"]', + content: t('tour.kb'), + disableBeacon: true, + }; + + const promptsStep = { + target: 'a[href="/prompts"]', + content: t('tour.prompt'), + 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 === STATUS.FINISHED || status === STATUS.SKIPPED) { + 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' as const, + 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' as const, + 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', + }, + buttonBack: { + color: '#5f6b7a', + padding: '8px 16px', + fontSize: '14px', + fontWeight: '500', + marginRight: '8px', + backgroundColor: 'transparent', + border: 'none', + cursor: 'pointer', + transition: 'color 0.2s ease', + }, + buttonSkip: { + color: '#5f6b7a', + fontSize: '14px', + padding: '8px', + backgroundColor: 'transparent', + border: 'none', + cursor: 'pointer', + transition: 'color 0.2s ease', + }, + spotlight: { + backgroundColor: 'transparent', + borderRadius: '4px', + boxShadow: '0 0 0 4px rgba(9, 114, 211, 0.3)', + zIndex: 9998, + }, + beacon: { + display: 'none' + } + }; + 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 b502a7d80..2d522dc2b 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; @@ -73,6 +73,7 @@ export type SessionHistoryResponse = { }; export type SessionMessage = { + messageId: string; role: 'ai' | 'human'; content: string; createTimestamp: string; @@ -226,7 +227,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[]; }; @@ -278,4 +279,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; + diff --git a/source/script/build.sh b/source/script/build.sh index 95dc4e66e..edb18e1b2 100644 --- a/source/script/build.sh +++ b/source/script/build.sh @@ -51,6 +51,11 @@ build_frontend() { modules_prepared="" cd .. +if $ui_enabled; then + build_frontend + modules_prepared="${modules_prepared}Frontend, " +fi + if $knowledge_base_enabled && $knowledge_base_intelliagent_enabled && $knowledge_base_models_enabled; then prepare_etl_model modules_prepared="${modules_prepared}ETL Model, " @@ -61,11 +66,6 @@ if $knowledge_base_enabled && $knowledge_base_intelliagent_enabled && $opensearc modules_prepared="${modules_prepared}Online Model, " fi -if $ui_enabled; then - build_frontend - modules_prepared="${modules_prepared}Frontend, " -fi - aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws # Remove the trailing comma and space