aws-genai-llm-chatbot
329 строк · 11.8 Кб
1import * as path from "path";
2import * as cdk from "aws-cdk-lib";
3import { SageMakerModelEndpoint, SystemConfig } from "../shared/types";
4import { Construct } from "constructs";
5import { RagEngines } from "../rag-engines";
6import * as cognito from "aws-cdk-lib/aws-cognito";
7import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
8import * as ec2 from "aws-cdk-lib/aws-ec2";
9import * as iam from "aws-cdk-lib/aws-iam";
10import * as lambda from "aws-cdk-lib/aws-lambda";
11import * as logs from "aws-cdk-lib/aws-logs";
12import * as ssm from "aws-cdk-lib/aws-ssm";
13import { Shared } from "../shared";
14import * as appsync from "aws-cdk-lib/aws-appsync";
15import { parse } from "graphql";
16import { readFileSync } from "fs";
17import * as s3 from "aws-cdk-lib/aws-s3";
18
19export interface ApiResolversProps {
20readonly shared: Shared;
21readonly config: SystemConfig;
22readonly ragEngines?: RagEngines;
23readonly userPool: cognito.UserPool;
24readonly sessionsTable: dynamodb.Table;
25readonly byUserIdIndex: string;
26readonly userFeedbackBucket: s3.Bucket;
27readonly modelsParameter: ssm.StringParameter;
28readonly models: SageMakerModelEndpoint[];
29readonly api: appsync.GraphqlApi;
30}
31
32export class ApiResolvers extends Construct {
33constructor(scope: Construct, id: string, props: ApiResolversProps) {
34super(scope, id);
35
36const apiSecurityGroup = new ec2.SecurityGroup(this, "ApiSecurityGroup", {
37vpc: props.shared.vpc,
38});
39
40const appSyncLambdaResolver = new lambda.Function(
41this,
42"GraphQLApiHandler",
43{
44code: props.shared.sharedCode.bundleWithLambdaAsset(
45path.join(__dirname, "./functions/api-handler")
46),
47handler: "index.handler",
48runtime: props.shared.pythonRuntime,
49architecture: props.shared.lambdaArchitecture,
50timeout: cdk.Duration.minutes(10),
51memorySize: 512,
52tracing: lambda.Tracing.ACTIVE,
53logRetention: logs.RetentionDays.ONE_WEEK,
54layers: [props.shared.powerToolsLayer, props.shared.commonLayer],
55vpc: props.shared.vpc,
56securityGroups: [apiSecurityGroup],
57vpcSubnets: props.shared.vpc.privateSubnets as ec2.SubnetSelection,
58environment: {
59...props.shared.defaultEnvironmentVariables,
60CONFIG_PARAMETER_NAME: props.shared.configParameter.parameterName,
61MODELS_PARAMETER_NAME: props.modelsParameter.parameterName,
62X_ORIGIN_VERIFY_SECRET_ARN:
63props.shared.xOriginVerifySecret.secretArn,
64API_KEYS_SECRETS_ARN: props.shared.apiKeysSecret.secretArn,
65SESSIONS_TABLE_NAME: props.sessionsTable.tableName,
66SESSIONS_BY_USER_ID_INDEX_NAME: props.byUserIdIndex,
67USER_FEEDBACK_BUCKET_NAME: props.userFeedbackBucket?.bucketName ?? "",
68UPLOAD_BUCKET_NAME: props.ragEngines?.uploadBucket?.bucketName ?? "",
69PROCESSING_BUCKET_NAME:
70props.ragEngines?.processingBucket?.bucketName ?? "",
71AURORA_DB_SECRET_ID: props.ragEngines?.auroraPgVector?.database
72?.secret?.secretArn as string,
73WORKSPACES_TABLE_NAME:
74props.ragEngines?.workspacesTable.tableName ?? "",
75WORKSPACES_BY_OBJECT_TYPE_INDEX_NAME:
76props.ragEngines?.workspacesByObjectTypeIndexName ?? "",
77DOCUMENTS_TABLE_NAME:
78props.ragEngines?.documentsTable.tableName ?? "",
79DOCUMENTS_BY_COMPOUND_KEY_INDEX_NAME:
80props.ragEngines?.documentsByCompountKeyIndexName ?? "",
81DOCUMENTS_BY_STATUS_INDEX:
82props.ragEngines?.documentsByStatusIndexName ?? "",
83SAGEMAKER_RAG_MODELS_ENDPOINT:
84props.ragEngines?.sageMakerRagModels?.model.endpoint
85?.attrEndpointName ?? "",
86DELETE_WORKSPACE_WORKFLOW_ARN:
87props.ragEngines?.deleteWorkspaceWorkflow?.stateMachineArn ?? "",
88CREATE_AURORA_WORKSPACE_WORKFLOW_ARN:
89props.ragEngines?.auroraPgVector?.createAuroraWorkspaceWorkflow
90?.stateMachineArn ?? "",
91CREATE_OPEN_SEARCH_WORKSPACE_WORKFLOW_ARN:
92props.ragEngines?.openSearchVector
93?.createOpenSearchWorkspaceWorkflow?.stateMachineArn ?? "",
94CREATE_KENDRA_WORKSPACE_WORKFLOW_ARN:
95props.ragEngines?.kendraRetrieval?.createKendraWorkspaceWorkflow
96?.stateMachineArn ?? "",
97FILE_IMPORT_WORKFLOW_ARN:
98props.ragEngines?.fileImportWorkflow?.stateMachineArn ?? "",
99WEBSITE_CRAWLING_WORKFLOW_ARN:
100props.ragEngines?.websiteCrawlingWorkflow?.stateMachineArn ?? "",
101OPEN_SEARCH_COLLECTION_ENDPOINT:
102props.ragEngines?.openSearchVector?.openSearchCollectionEndpoint ??
103"",
104DEFAULT_KENDRA_INDEX_ID:
105props.ragEngines?.kendraRetrieval?.kendraIndex?.attrId ?? "",
106DEFAULT_KENDRA_INDEX_NAME:
107props.ragEngines?.kendraRetrieval?.kendraIndex?.name ?? "",
108DEFAULT_KENDRA_S3_DATA_SOURCE_ID:
109props.ragEngines?.kendraRetrieval?.kendraS3DataSource?.attrId ?? "",
110DEFAULT_KENDRA_S3_DATA_SOURCE_BUCKET_NAME:
111props.ragEngines?.kendraRetrieval?.kendraS3DataSourceBucket
112?.bucketName ?? "",
113RSS_FEED_INGESTOR_FUNCTION:
114props.ragEngines?.dataImport.rssIngestorFunction?.functionArn ?? "",
115},
116}
117);
118
119function addPermissions(apiHandler: lambda.Function) {
120if (props.ragEngines?.workspacesTable) {
121props.ragEngines.workspacesTable.grantReadWriteData(apiHandler);
122}
123
124if (props.ragEngines?.documentsTable) {
125props.ragEngines.documentsTable.grantReadWriteData(apiHandler);
126props.ragEngines?.dataImport.rssIngestorFunction?.grantInvoke(
127apiHandler
128);
129}
130
131if (props.ragEngines?.auroraPgVector) {
132props.ragEngines.auroraPgVector.database.secret?.grantRead(apiHandler);
133props.ragEngines.auroraPgVector.database.connections.allowDefaultPortFrom(
134apiHandler
135);
136
137props.ragEngines.auroraPgVector.createAuroraWorkspaceWorkflow.grantStartExecution(
138apiHandler
139);
140}
141
142if (props.ragEngines?.openSearchVector) {
143apiHandler.addToRolePolicy(
144new iam.PolicyStatement({
145actions: ["aoss:APIAccessAll"],
146resources: [
147props.ragEngines?.openSearchVector.openSearchCollection.attrArn,
148],
149})
150);
151
152props.ragEngines.openSearchVector.createOpenSearchWorkspaceWorkflow.grantStartExecution(
153apiHandler
154);
155}
156
157if (props.ragEngines?.kendraRetrieval) {
158props.ragEngines.kendraRetrieval.createKendraWorkspaceWorkflow.grantStartExecution(
159apiHandler
160);
161
162props.ragEngines?.kendraRetrieval?.kendraS3DataSourceBucket?.grantReadWrite(
163apiHandler
164);
165
166if (props.ragEngines.kendraRetrieval.kendraIndex) {
167apiHandler.addToRolePolicy(
168new iam.PolicyStatement({
169actions: [
170"kendra:Retrieve",
171"kendra:Query",
172"kendra:BatchDeleteDocument",
173"kendra:BatchPutDocument",
174"kendra:StartDataSourceSyncJob",
175"kendra:DescribeDataSourceSyncJob",
176"kendra:StopDataSourceSyncJob",
177"kendra:ListDataSourceSyncJobs",
178"kendra:ListDataSources",
179"kendra:DescribeIndex",
180],
181resources: [
182props.ragEngines.kendraRetrieval.kendraIndex.attrArn,
183`${props.ragEngines.kendraRetrieval.kendraIndex.attrArn}/*`,
184],
185})
186);
187}
188
189for (const item of props.config.rag.engines.kendra.external ?? []) {
190if (item.roleArn) {
191apiHandler.addToRolePolicy(
192new iam.PolicyStatement({
193actions: ["sts:AssumeRole"],
194resources: [item.roleArn],
195})
196);
197} else {
198apiHandler.addToRolePolicy(
199new iam.PolicyStatement({
200actions: ["kendra:Retrieve", "kendra:Query"],
201resources: [
202`arn:${cdk.Aws.PARTITION}:kendra:${
203item.region ?? cdk.Aws.REGION
204}:${cdk.Aws.ACCOUNT_ID}:index/${item.kendraId}`,
205],
206})
207);
208}
209}
210}
211
212if (props.ragEngines?.fileImportWorkflow) {
213props.ragEngines.fileImportWorkflow.grantStartExecution(apiHandler);
214}
215
216if (props.ragEngines?.websiteCrawlingWorkflow) {
217props.ragEngines.websiteCrawlingWorkflow.grantStartExecution(
218apiHandler
219);
220}
221
222if (props.ragEngines?.deleteWorkspaceWorkflow) {
223props.ragEngines.deleteWorkspaceWorkflow.grantStartExecution(
224apiHandler
225);
226}
227
228if (props.ragEngines?.sageMakerRagModels) {
229apiHandler.addToRolePolicy(
230new iam.PolicyStatement({
231actions: ["sagemaker:InvokeEndpoint"],
232resources: [props.ragEngines.sageMakerRagModels.model.endpoint.ref],
233})
234);
235}
236
237for (const model of props.models) {
238apiHandler.addToRolePolicy(
239new iam.PolicyStatement({
240actions: ["sagemaker:InvokeEndpoint"],
241resources: [model.endpoint.ref],
242})
243);
244}
245
246apiHandler.addToRolePolicy(
247new iam.PolicyStatement({
248actions: [
249"comprehend:DetectDominantLanguage",
250"comprehend:DetectSentiment",
251],
252resources: ["*"],
253})
254);
255
256props.shared.xOriginVerifySecret.grantRead(apiHandler);
257props.shared.apiKeysSecret.grantRead(apiHandler);
258props.shared.configParameter.grantRead(apiHandler);
259props.modelsParameter.grantRead(apiHandler);
260props.sessionsTable.grantReadWriteData(apiHandler);
261props.userFeedbackBucket.grantReadWrite(apiHandler);
262props.ragEngines?.uploadBucket.grantReadWrite(apiHandler);
263props.ragEngines?.processingBucket.grantReadWrite(apiHandler);
264
265if (props.config.bedrock?.enabled) {
266apiHandler.addToRolePolicy(
267new iam.PolicyStatement({
268actions: [
269"bedrock:ListFoundationModels",
270"bedrock:ListCustomModels",
271"bedrock:InvokeModel",
272"bedrock:InvokeModelWithResponseStream",
273],
274resources: ["*"],
275})
276);
277
278if (props.config.bedrock?.roleArn) {
279apiHandler.addToRolePolicy(
280new iam.PolicyStatement({
281actions: ["sts:AssumeRole"],
282resources: [props.config.bedrock.roleArn],
283})
284);
285}
286}
287}
288
289addPermissions(appSyncLambdaResolver);
290
291props.ragEngines?.openSearchVector?.addToAccessPolicy(
292"graphql-api",
293[appSyncLambdaResolver.role?.roleArn],
294["aoss:DescribeIndex", "aoss:ReadDocument", "aoss:WriteDocument"]
295);
296
297const functionDataSource = props.api.addLambdaDataSource(
298"proxyResolverFunction",
299appSyncLambdaResolver
300);
301
302const schema = parse(
303readFileSync("lib/chatbot-api/schema/schema.graphql", "utf8")
304);
305
306function addResolvers(operationType: string) {
307const fieldNames = (
308schema.definitions
309.filter((x) => x.kind == "ObjectTypeDefinition")
310.filter((y: any) => y.name.value == operationType)[0] as any
311).fields.map((z: any) => z.name.value);
312
313for (const fieldName of fieldNames) {
314// These resolvers are added by the Realtime API
315if (fieldName == "sendQuery" || fieldName == "publishResponse") {
316continue;
317}
318props.api.createResolver(`${fieldName}-resolver`, {
319typeName: operationType,
320fieldName: fieldName,
321dataSource: functionDataSource,
322});
323}
324}
325
326addResolvers("Query");
327addResolvers("Mutation");
328}
329}
330