Skip to content

Commit

Permalink
Merge pull request aws-samples#287 from azaylamba/user-feedback
Browse files Browse the repository at this point in the history
Enhancement: Add user feedback for responses
  • Loading branch information
bigadsoleiman authored Feb 12, 2024
2 parents 418a31f + f246de3 commit 6f0ad46
Show file tree
Hide file tree
Showing 16 changed files with 346 additions and 1 deletion.
10 changes: 10 additions & 0 deletions lib/chatbot-api/chatbot-s3-buckets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NagSuppressions } from "cdk-nag";

export class ChatBotS3Buckets extends Construct {
public readonly filesBucket: s3.Bucket;
public readonly userFeedbackBucket: s3.Bucket;

constructor(scope: Construct, id: string) {
super(scope, id);
Expand Down Expand Up @@ -39,7 +40,16 @@ export class ChatBotS3Buckets extends Construct {
],
});

const userFeedbackBucket = new s3.Bucket(this, "UserFeedbackBucket", {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
enforceSSL: true,
serverAccessLogsBucket: logsBucket,
});

this.filesBucket = filesBucket;
this.userFeedbackBucket = userFeedbackBucket;

/**
* CDK NAG suppression
Expand Down
2 changes: 2 additions & 0 deletions lib/chatbot-api/functions/api-handler/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from routes.semantic_search import router as semantic_search_router
from routes.documents import router as documents_router
from routes.kendra import router as kendra_router
from routes.user_feedback import router as user_feedback_router

tracer = Tracer()
logger = Logger()
Expand All @@ -30,6 +31,7 @@
app.include_router(semantic_search_router)
app.include_router(documents_router)
app.include_router(kendra_router)
app.include_router(user_feedback_router)


@logger.inject_lambda_context(
Expand Down
37 changes: 37 additions & 0 deletions lib/chatbot-api/functions/api-handler/routes/user_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import genai_core.types
import genai_core.auth
import genai_core.user_feedback
from pydantic import BaseModel
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler.appsync import Router

tracer = Tracer()
router = Router()
logger = Logger()


class CreateUserFeedbackRequest(BaseModel):
sessionId: str
key: str
feedback: str
prompt: str
completion: str
model: str


@router.resolver(field_name="addUserFeedback")
@tracer.capture_method
def user_feedback(input: dict):
request = CreateUserFeedbackRequest(**input)

userId = genai_core.auth.get_user_id(router)

if userId is None:
raise genai_core.types.CommonError("User not found")

result = genai_core.user_feedback.add_user_feedback(
request.sessionId, request.key, request.feedback, request.prompt, request.completion, request.model, userId)

return {
"feedback_id": result["feedback_id"],
}
3 changes: 3 additions & 0 deletions lib/chatbot-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class ChatBotApi extends Construct {
public readonly sessionsTable: dynamodb.Table;
public readonly byUserIdIndex: string;
public readonly filesBucket: s3.Bucket;
public readonly userFeedbackBucket: s3.Bucket;
public readonly graphqlApi: appsync.GraphqlApi;

constructor(scope: Construct, id: string, props: ChatBotApiProps) {
Expand Down Expand Up @@ -87,6 +88,7 @@ export class ChatBotApi extends Construct {
sessionsTable: chatTables.sessionsTable,
byUserIdIndex: chatTables.byUserIdIndex,
api,
userFeedbackBucket: chatBuckets.userFeedbackBucket,
});

const realtimeBackend = new RealtimeGraphqlApiBackend(this, "Realtime", {
Expand Down Expand Up @@ -114,6 +116,7 @@ export class ChatBotApi extends Construct {
this.messagesTopic = realtimeBackend.messagesTopic;
this.sessionsTable = chatTables.sessionsTable;
this.byUserIdIndex = chatTables.byUserIdIndex;
this.userFeedbackBucket = chatBuckets.userFeedbackBucket;
this.filesBucket = chatBuckets.filesBucket;
this.graphqlApi = api;

Expand Down
4 changes: 4 additions & 0 deletions lib/chatbot-api/rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Shared } from "../shared";
import * as appsync from "aws-cdk-lib/aws-appsync";
import { parse } from "graphql";
import { readFileSync } from "fs";
import * as s3 from "aws-cdk-lib/aws-s3";

export interface ApiResolversProps {
readonly shared: Shared;
Expand All @@ -22,6 +23,7 @@ export interface ApiResolversProps {
readonly userPool: cognito.UserPool;
readonly sessionsTable: dynamodb.Table;
readonly byUserIdIndex: string;
readonly userFeedbackBucket: s3.Bucket;
readonly modelsParameter: ssm.StringParameter;
readonly models: SageMakerModelEndpoint[];
readonly api: appsync.GraphqlApi;
Expand Down Expand Up @@ -62,6 +64,7 @@ export class ApiResolvers extends Construct {
API_KEYS_SECRETS_ARN: props.shared.apiKeysSecret.secretArn,
SESSIONS_TABLE_NAME: props.sessionsTable.tableName,
SESSIONS_BY_USER_ID_INDEX_NAME: props.byUserIdIndex,
USER_FEEDBACK_BUCKET_NAME: props.userFeedbackBucket?.bucketName ?? "",
UPLOAD_BUCKET_NAME: props.ragEngines?.uploadBucket?.bucketName ?? "",
PROCESSING_BUCKET_NAME:
props.ragEngines?.processingBucket?.bucketName ?? "",
Expand Down Expand Up @@ -255,6 +258,7 @@ export class ApiResolvers extends Construct {
props.shared.configParameter.grantRead(apiHandler);
props.modelsParameter.grantRead(apiHandler);
props.sessionsTable.grantReadWriteData(apiHandler);
props.userFeedbackBucket.grantReadWrite(apiHandler);
props.ragEngines?.uploadBucket.grantReadWrite(apiHandler);
props.ragEngines?.processingBucket.grantReadWrite(apiHandler);

Expand Down
14 changes: 14 additions & 0 deletions lib/chatbot-api/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ type DocumentResult @aws_cognito_user_pools {
status: String
}

type UserFeedbackResult @aws_cognito_user_pools {
feedback_id: String!
}

input DocumentSubscriptionStatusInput {
workspaceId: String!
documentId: String!
Expand Down Expand Up @@ -235,6 +239,15 @@ type SessionHistoryItem @aws_cognito_user_pools {
metadata: String
}

input UserFeedbackInput {
sessionId: String!
key: Int!
feedback: String!
prompt: String!
completion: String!
model: String!
}

input TextDocumentInput {
workspaceId: String!
title: String!
Expand Down Expand Up @@ -296,6 +309,7 @@ type Mutation {
deleteWorkspace(workspaceId: String!): Boolean @aws_cognito_user_pools
addTextDocument(input: TextDocumentInput!): DocumentResult
@aws_cognito_user_pools
addUserFeedback(input: UserFeedbackInput!): UserFeedbackResult @aws_cognito_user_pools
addQnADocument(input: QnADocumentInput!): DocumentResult
@aws_cognito_user_pools
setDocumentSubscriptionStatus(
Expand Down
51 changes: 51 additions & 0 deletions lib/shared/layers/python-sdk/python/genai_core/user_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os
import uuid
import boto3
import json
from pydantic import BaseModel
from datetime import datetime

dynamodb = boto3.resource("dynamodb")
s3_client = boto3.client("s3")

USER_FEEDBACK_BUCKET_NAME = os.environ.get("USER_FEEDBACK_BUCKET_NAME")


def add_user_feedback(
sessionId: str,
key: str,
feedback: str,
prompt: str,
completion: str,
model: str,
userId: str
):
feedbackId = str(uuid.uuid4())
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")

item = {
"feedbackId": feedbackId,
"sessionId": sessionId,
"userId": userId,
"key": key,
"prompt": prompt,
"completion": completion,
"model": model,
"feedback": feedback,
"createdAt": timestamp
}

response = s3_client.put_object(
Bucket=USER_FEEDBACK_BUCKET_NAME,
Key=feedbackId,
Body=json.dumps(item),
ContentType="application/json",
StorageClass='STANDARD_IA',
)
print(response)

return {
"feedback_id": feedbackId
}


57 changes: 57 additions & 0 deletions lib/user-interface/react-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions lib/user-interface/react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@
"@cloudscape-design/components": "^3.0.405",
"@cloudscape-design/design-tokens": "^3.0.28",
"@cloudscape-design/global-styles": "^1.0.13",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"aws-amplify": "^5.3.12",
"luxon": "^3.4.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-json-view-lite": "^0.9.8",
"react-markdown": "^9.0.0",
"remark-gfm": "^4.0.0",
Expand Down
10 changes: 10 additions & 0 deletions lib/user-interface/react-app/src/common/api-client/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SessionsClient } from "./sessions-client";
import { SemanticSearchClient } from "./semantic-search-client";
import { DocumentsClient } from "./documents-client";
import { KendraClient } from "./kendra-client";
import { UserFeedbackClient } from "./user-feedback-client";

export class ApiClient {
private _healthClient: HealthClient | undefined;
Expand All @@ -21,6 +22,7 @@ export class ApiClient {
private _semanticSearchClient: SemanticSearchClient | undefined;
private _documentsClient: DocumentsClient | undefined;
private _kendraClient: KendraClient | undefined;
private _userFeedbackClient: UserFeedbackClient | undefined;

public get health() {
if (!this._healthClient) {
Expand Down Expand Up @@ -102,5 +104,13 @@ export class ApiClient {
return this._kendraClient;
}

public get userFeedback() {
if(!this._userFeedbackClient) {
this._userFeedbackClient = new UserFeedbackClient(this._appConfig);
}

return this._userFeedbackClient;
}

constructor(protected _appConfig: AppConfig) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { GraphQLResult } from "@aws-amplify/api-graphql";
import { API, GraphQLQuery } from "@aws-amplify/api";
import { AddUserFeedbackMutation } from "../../API.ts";
import {
addUserFeedback
} from "../../graphql/mutations.ts";
import { FeedbackData } from "../../components/chatbot/types.ts";

export class UserFeedbackClient {

async addUserFeedback(params: {
feedbackData: FeedbackData
}
): Promise<GraphQLResult<GraphQLQuery<AddUserFeedbackMutation>>> {
const result = API.graphql<GraphQLQuery<AddUserFeedbackMutation>>({
query: addUserFeedback,
variables: {
input: {
sessionId: params.feedbackData.sessionId,
key: params.feedbackData.key,
feedback: params.feedbackData.feedback,
prompt: params.feedbackData.prompt,
completion: params.feedbackData.completion,
model: params.feedbackData.model,
},
},
});
return result;
}

}
Loading

0 comments on commit 6f0ad46

Please sign in to comment.