Skip to content

Commit 6f0ad46

Browse files
Merge pull request #287 from azaylamba/user-feedback
Enhancement: Add user feedback for responses
2 parents 418a31f + f246de3 commit 6f0ad46

File tree

16 files changed

+346
-1
lines changed

16 files changed

+346
-1
lines changed

Diff for: lib/chatbot-api/chatbot-s3-buckets/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NagSuppressions } from "cdk-nag";
55

66
export class ChatBotS3Buckets extends Construct {
77
public readonly filesBucket: s3.Bucket;
8+
public readonly userFeedbackBucket: s3.Bucket;
89

910
constructor(scope: Construct, id: string) {
1011
super(scope, id);
@@ -39,7 +40,16 @@ export class ChatBotS3Buckets extends Construct {
3940
],
4041
});
4142

43+
const userFeedbackBucket = new s3.Bucket(this, "UserFeedbackBucket", {
44+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
45+
removalPolicy: cdk.RemovalPolicy.DESTROY,
46+
autoDeleteObjects: true,
47+
enforceSSL: true,
48+
serverAccessLogsBucket: logsBucket,
49+
});
50+
4251
this.filesBucket = filesBucket;
52+
this.userFeedbackBucket = userFeedbackBucket;
4353

4454
/**
4555
* CDK NAG suppression

Diff for: lib/chatbot-api/functions/api-handler/index.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from routes.semantic_search import router as semantic_search_router
1515
from routes.documents import router as documents_router
1616
from routes.kendra import router as kendra_router
17+
from routes.user_feedback import router as user_feedback_router
1718

1819
tracer = Tracer()
1920
logger = Logger()
@@ -30,6 +31,7 @@
3031
app.include_router(semantic_search_router)
3132
app.include_router(documents_router)
3233
app.include_router(kendra_router)
34+
app.include_router(user_feedback_router)
3335

3436

3537
@logger.inject_lambda_context(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import genai_core.types
2+
import genai_core.auth
3+
import genai_core.user_feedback
4+
from pydantic import BaseModel
5+
from aws_lambda_powertools import Logger, Tracer
6+
from aws_lambda_powertools.event_handler.appsync import Router
7+
8+
tracer = Tracer()
9+
router = Router()
10+
logger = Logger()
11+
12+
13+
class CreateUserFeedbackRequest(BaseModel):
14+
sessionId: str
15+
key: str
16+
feedback: str
17+
prompt: str
18+
completion: str
19+
model: str
20+
21+
22+
@router.resolver(field_name="addUserFeedback")
23+
@tracer.capture_method
24+
def user_feedback(input: dict):
25+
request = CreateUserFeedbackRequest(**input)
26+
27+
userId = genai_core.auth.get_user_id(router)
28+
29+
if userId is None:
30+
raise genai_core.types.CommonError("User not found")
31+
32+
result = genai_core.user_feedback.add_user_feedback(
33+
request.sessionId, request.key, request.feedback, request.prompt, request.completion, request.model, userId)
34+
35+
return {
36+
"feedback_id": result["feedback_id"],
37+
}

Diff for: lib/chatbot-api/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class ChatBotApi extends Construct {
3232
public readonly sessionsTable: dynamodb.Table;
3333
public readonly byUserIdIndex: string;
3434
public readonly filesBucket: s3.Bucket;
35+
public readonly userFeedbackBucket: s3.Bucket;
3536
public readonly graphqlApi: appsync.GraphqlApi;
3637

3738
constructor(scope: Construct, id: string, props: ChatBotApiProps) {
@@ -87,6 +88,7 @@ export class ChatBotApi extends Construct {
8788
sessionsTable: chatTables.sessionsTable,
8889
byUserIdIndex: chatTables.byUserIdIndex,
8990
api,
91+
userFeedbackBucket: chatBuckets.userFeedbackBucket,
9092
});
9193

9294
const realtimeBackend = new RealtimeGraphqlApiBackend(this, "Realtime", {
@@ -114,6 +116,7 @@ export class ChatBotApi extends Construct {
114116
this.messagesTopic = realtimeBackend.messagesTopic;
115117
this.sessionsTable = chatTables.sessionsTable;
116118
this.byUserIdIndex = chatTables.byUserIdIndex;
119+
this.userFeedbackBucket = chatBuckets.userFeedbackBucket;
117120
this.filesBucket = chatBuckets.filesBucket;
118121
this.graphqlApi = api;
119122

Diff for: lib/chatbot-api/rest-api.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Shared } from "../shared";
1414
import * as appsync from "aws-cdk-lib/aws-appsync";
1515
import { parse } from "graphql";
1616
import { readFileSync } from "fs";
17+
import * as s3 from "aws-cdk-lib/aws-s3";
1718

1819
export interface ApiResolversProps {
1920
readonly shared: Shared;
@@ -22,6 +23,7 @@ export interface ApiResolversProps {
2223
readonly userPool: cognito.UserPool;
2324
readonly sessionsTable: dynamodb.Table;
2425
readonly byUserIdIndex: string;
26+
readonly userFeedbackBucket: s3.Bucket;
2527
readonly modelsParameter: ssm.StringParameter;
2628
readonly models: SageMakerModelEndpoint[];
2729
readonly api: appsync.GraphqlApi;
@@ -62,6 +64,7 @@ export class ApiResolvers extends Construct {
6264
API_KEYS_SECRETS_ARN: props.shared.apiKeysSecret.secretArn,
6365
SESSIONS_TABLE_NAME: props.sessionsTable.tableName,
6466
SESSIONS_BY_USER_ID_INDEX_NAME: props.byUserIdIndex,
67+
USER_FEEDBACK_BUCKET_NAME: props.userFeedbackBucket?.bucketName ?? "",
6568
UPLOAD_BUCKET_NAME: props.ragEngines?.uploadBucket?.bucketName ?? "",
6669
PROCESSING_BUCKET_NAME:
6770
props.ragEngines?.processingBucket?.bucketName ?? "",
@@ -255,6 +258,7 @@ export class ApiResolvers extends Construct {
255258
props.shared.configParameter.grantRead(apiHandler);
256259
props.modelsParameter.grantRead(apiHandler);
257260
props.sessionsTable.grantReadWriteData(apiHandler);
261+
props.userFeedbackBucket.grantReadWrite(apiHandler);
258262
props.ragEngines?.uploadBucket.grantReadWrite(apiHandler);
259263
props.ragEngines?.processingBucket.grantReadWrite(apiHandler);
260264

Diff for: lib/chatbot-api/schema/schema.graphql

+14
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ type DocumentResult @aws_cognito_user_pools {
8989
status: String
9090
}
9191

92+
type UserFeedbackResult @aws_cognito_user_pools {
93+
feedback_id: String!
94+
}
95+
9296
input DocumentSubscriptionStatusInput {
9397
workspaceId: String!
9498
documentId: String!
@@ -235,6 +239,15 @@ type SessionHistoryItem @aws_cognito_user_pools {
235239
metadata: String
236240
}
237241

242+
input UserFeedbackInput {
243+
sessionId: String!
244+
key: Int!
245+
feedback: String!
246+
prompt: String!
247+
completion: String!
248+
model: String!
249+
}
250+
238251
input TextDocumentInput {
239252
workspaceId: String!
240253
title: String!
@@ -296,6 +309,7 @@ type Mutation {
296309
deleteWorkspace(workspaceId: String!): Boolean @aws_cognito_user_pools
297310
addTextDocument(input: TextDocumentInput!): DocumentResult
298311
@aws_cognito_user_pools
312+
addUserFeedback(input: UserFeedbackInput!): UserFeedbackResult @aws_cognito_user_pools
299313
addQnADocument(input: QnADocumentInput!): DocumentResult
300314
@aws_cognito_user_pools
301315
setDocumentSubscriptionStatus(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import os
2+
import uuid
3+
import boto3
4+
import json
5+
from pydantic import BaseModel
6+
from datetime import datetime
7+
8+
dynamodb = boto3.resource("dynamodb")
9+
s3_client = boto3.client("s3")
10+
11+
USER_FEEDBACK_BUCKET_NAME = os.environ.get("USER_FEEDBACK_BUCKET_NAME")
12+
13+
14+
def add_user_feedback(
15+
sessionId: str,
16+
key: str,
17+
feedback: str,
18+
prompt: str,
19+
completion: str,
20+
model: str,
21+
userId: str
22+
):
23+
feedbackId = str(uuid.uuid4())
24+
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
25+
26+
item = {
27+
"feedbackId": feedbackId,
28+
"sessionId": sessionId,
29+
"userId": userId,
30+
"key": key,
31+
"prompt": prompt,
32+
"completion": completion,
33+
"model": model,
34+
"feedback": feedback,
35+
"createdAt": timestamp
36+
}
37+
38+
response = s3_client.put_object(
39+
Bucket=USER_FEEDBACK_BUCKET_NAME,
40+
Key=feedbackId,
41+
Body=json.dumps(item),
42+
ContentType="application/json",
43+
StorageClass='STANDARD_IA',
44+
)
45+
print(response)
46+
47+
return {
48+
"feedback_id": feedbackId
49+
}
50+
51+

Diff for: lib/user-interface/react-app/package-lock.json

+57
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: lib/user-interface/react-app/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616
"@cloudscape-design/components": "^3.0.405",
1717
"@cloudscape-design/design-tokens": "^3.0.28",
1818
"@cloudscape-design/global-styles": "^1.0.13",
19+
"@fortawesome/fontawesome-svg-core": "^6.4.2",
20+
"@fortawesome/free-solid-svg-icons": "^6.4.2",
21+
"@fortawesome/react-fontawesome": "^0.2.0",
1922
"aws-amplify": "^5.3.12",
2023
"luxon": "^3.4.3",
2124
"react": "^18.2.0",
2225
"react-dom": "^18.2.0",
26+
"react-icons": "^4.12.0",
2327
"react-json-view-lite": "^0.9.8",
2428
"react-markdown": "^9.0.0",
2529
"remark-gfm": "^4.0.0",

Diff for: lib/user-interface/react-app/src/common/api-client/api-client.ts

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SessionsClient } from "./sessions-client";
99
import { SemanticSearchClient } from "./semantic-search-client";
1010
import { DocumentsClient } from "./documents-client";
1111
import { KendraClient } from "./kendra-client";
12+
import { UserFeedbackClient } from "./user-feedback-client";
1213

1314
export class ApiClient {
1415
private _healthClient: HealthClient | undefined;
@@ -21,6 +22,7 @@ export class ApiClient {
2122
private _semanticSearchClient: SemanticSearchClient | undefined;
2223
private _documentsClient: DocumentsClient | undefined;
2324
private _kendraClient: KendraClient | undefined;
25+
private _userFeedbackClient: UserFeedbackClient | undefined;
2426

2527
public get health() {
2628
if (!this._healthClient) {
@@ -102,5 +104,13 @@ export class ApiClient {
102104
return this._kendraClient;
103105
}
104106

107+
public get userFeedback() {
108+
if(!this._userFeedbackClient) {
109+
this._userFeedbackClient = new UserFeedbackClient(this._appConfig);
110+
}
111+
112+
return this._userFeedbackClient;
113+
}
114+
105115
constructor(protected _appConfig: AppConfig) {}
106116
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { GraphQLResult } from "@aws-amplify/api-graphql";
2+
import { API, GraphQLQuery } from "@aws-amplify/api";
3+
import { AddUserFeedbackMutation } from "../../API.ts";
4+
import {
5+
addUserFeedback
6+
} from "../../graphql/mutations.ts";
7+
import { FeedbackData } from "../../components/chatbot/types.ts";
8+
9+
export class UserFeedbackClient {
10+
11+
async addUserFeedback(params: {
12+
feedbackData: FeedbackData
13+
}
14+
): Promise<GraphQLResult<GraphQLQuery<AddUserFeedbackMutation>>> {
15+
const result = API.graphql<GraphQLQuery<AddUserFeedbackMutation>>({
16+
query: addUserFeedback,
17+
variables: {
18+
input: {
19+
sessionId: params.feedbackData.sessionId,
20+
key: params.feedbackData.key,
21+
feedback: params.feedbackData.feedback,
22+
prompt: params.feedbackData.prompt,
23+
completion: params.feedbackData.completion,
24+
model: params.feedbackData.model,
25+
},
26+
},
27+
});
28+
return result;
29+
}
30+
31+
}

0 commit comments

Comments
 (0)