Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add backend-ai package #1813

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-fireants-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-ai': patch
---

Introduce backend-ai package. Implement basic conversation handler.
3 changes: 3 additions & 0 deletions .eslint_dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"amazoncognito",
"amplifyconfiguration",
"ampx",
"anthropic",
"apns",
"apollo",
"appleid",
"appsync",
"argv",
Expand All @@ -20,6 +22,7 @@
"changeset",
"changesets",
"chown",
"claude",
"cloudformation",
"codebase",
"codegen",
Expand Down
598 changes: 496 additions & 102 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions packages/backend-ai/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Be very careful editing this file. It is crafted to work around [this issue](https://github.com/npm/npm/issues/4479)

# First ignore everything
**/*

# Then add back in transpiled js and ts declaration files
!lib/**/*.js
!lib/**/*.d.ts

# Then ignore test js and ts declaration files
*.test.js
*.test.d.ts

# This leaves us with including only js and ts declaration files of functional code
82 changes: 82 additions & 0 deletions packages/backend-ai/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
## API Report File for "@aws-amplify/backend-ai"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts

import { Construct } from 'constructs';
import { FunctionResources } from '@aws-amplify/plugin-types';
import { ResourceProvider } from '@aws-amplify/plugin-types';

declare namespace constructs {
export {
ConversationHandlerFunction,
ConversationHandlerFunctionProps
}
}

declare namespace conversation {
export {
constructs,
runtime
}
}
export { conversation }

// @public
class ConversationHandlerFunction extends Construct implements ResourceProvider<FunctionResources> {
constructor(scope: Construct, id: string, props: ConversationHandlerFunctionProps);
// (undocumented)
resources: FunctionResources;
}

// @public (undocumented)
type ConversationHandlerFunctionProps = {
entry?: string;
models: Array<{
modelId: string;
region: string;
}>;
};

// @public (undocumented)
type ConversationMessage = {
role: 'user' | 'assistant';
content: Array<ConversationMessageContentBlock>;
};

// @public (undocumented)
type ConversationMessageContentBlock = {
text: string;
};

// @public (undocumented)
type ConversationTurnEvent = {
conversationId: string;
currentMessageId: string;
responseMutationName: string;
responseMutationInputTypeName: string;
graphqlApiEndpoint: string;
modelConfiguration: {
modelId: string;
systemPrompt: string;
};
request: {
headers: {
authorization: string;
};
};
messages: Array<ConversationMessage>;
};

declare namespace runtime {
export {
ConversationMessage,
ConversationMessageContentBlock,
ConversationTurnEvent
}
}

// (No @packageDocumentation comment for this package)

```
4 changes: 4 additions & 0 deletions packages/backend-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Description

This package contains components for interacting with AI models, simplifying the process
of incorporating AI functionality into Amplify projects.
3 changes: 3 additions & 0 deletions packages/backend-ai/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../api-extractor.base.json"
}
35 changes: 35 additions & 0 deletions packages/backend-ai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@aws-amplify/backend-ai",
"version": "0.0.1",
"type": "commonjs",
"publishConfig": {
"access": "public"
},
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./conversation/constructs": {
"types": "./lib/conversation/constructs/index.d.ts",
"default": "./lib/conversation/constructs/index.js"
},
"./conversation/runtime": {
"types": "./lib/conversation/runtime/index.d.ts",
"default": "./lib/conversation/runtime/index.js"
}
},
"types": "lib/index.d.ts",
"scripts": {
"update:api": "api-extractor run --local"
},
"license": "Apache-2.0",
"dependencies": {
"@aws-amplify/plugin-types": "^1.0.1",
"@aws-sdk/client-bedrock-runtime": "^3.622.0"
},
"peerDependencies": {
"aws-cdk-lib": "^2.132.0",
"constructs": "^10.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { FunctionResources, ResourceProvider } from '@aws-amplify/plugin-types';
import { Duration } from 'aws-cdk-lib';
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { CfnFunction, Runtime as LambdaRuntime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import {
CustomDataIdentifier,
DataProtectionPolicy,
LogGroup,
RetentionDays,
} from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
import path from 'path';

const resourcesRoot = path.normalize(path.join(__dirname, '..', 'runtime'));
const defaultHandlerFilePath = path.join(resourcesRoot, 'default_handler.js');

export type ConversationHandlerFunctionProps = {
entry?: string;
models: Array<{
modelId: string;
region: string;
}>;
};

/**
* TODO docs.
*/
export class ConversationHandlerFunction
extends Construct
implements ResourceProvider<FunctionResources>
{
resources: FunctionResources;

/**
* TODO docs.
*/
constructor(
scope: Construct,
id: string,
private readonly props: ConversationHandlerFunctionProps
) {
super(scope, id);

const conversationHandler = new NodejsFunction(
this,
`conversationHandlerFunction`,
{
runtime: LambdaRuntime.NODEJS_18_X,
timeout: Duration.seconds(60),
entry: this.props.entry ?? defaultHandlerFilePath,
handler: 'handler',
bundling: {
bundleAwsSDK: true,
},
logGroup: new LogGroup(this, 'conversationHandlerFunctionLogGroup', {
retention: RetentionDays.INFINITE,
dataProtectionPolicy: new DataProtectionPolicy({
identifiers: [
new CustomDataIdentifier(
'idToken',
'ey[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*'
),
],
}),
}),
}
);

if (this.props.models && this.props.models.length > 0) {
const resources = this.props.models.map(
(model) =>
`arn:aws:bedrock:${model.region}::foundation-model/${model.modelId}`
);
conversationHandler.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['bedrock:InvokeModel'],
resources,
})
);
}

this.resources = {
lambda: conversationHandler,
cfnResources: {
cfnFunction: conversationHandler.node.findChild(
'Resource'
) as CfnFunction,
},
};
}
}
6 changes: 6 additions & 0 deletions packages/backend-ai/src/conversation/constructs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {
ConversationHandlerFunction,
ConversationHandlerFunctionProps,
} from './conversation_handler_construct.js';

export { ConversationHandlerFunction, ConversationHandlerFunctionProps };
4 changes: 4 additions & 0 deletions packages/backend-ai/src/conversation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as constructs from './constructs';
import * as runtime from './runtime';

export { constructs, runtime };
5 changes: 5 additions & 0 deletions packages/backend-ai/src/conversation/runtime/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-console": "off"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
BedrockRuntimeClient,
ContentBlock,
ConverseCommand,
ConverseCommandInput,
Message,
} from '@aws-sdk/client-bedrock-runtime';
import { ConversationTurnEvent } from './types.js';

/**
* TODO docs
*/
export class BedrockConverseAdapter {
private readonly bedrockClient: BedrockRuntimeClient;

/**
* TODO docs
*/
constructor(private readonly event: ConversationTurnEvent) {
// TODO. Region selection may happen at event scope, so
// we should make all these lambda components request scoped.
this.bedrockClient = new BedrockRuntimeClient();
}

askBedrock = async (): Promise<ContentBlock[]> => {
const { modelId, systemPrompt } = this.event.modelConfiguration;

const messages: Array<Message> = this.event.messages;

const converseCommandInput: ConverseCommandInput = {
modelId,
messages,
system: [{ text: systemPrompt }],
inferenceConfig: {
maxTokens: 2000,
temperature: 0,
},
};
const bedrockResponse = await this.bedrockClient.send(
new ConverseCommand(converseCommandInput)
);
if (bedrockResponse.output?.message) {
messages.push(bedrockResponse.output?.message);
}

const assistantResponse = bedrockResponse.output?.message?.content;
if (!assistantResponse) {
throw new Error('No response from bedrock');
}

return assistantResponse;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ConversationTurnResponseSender } from './conversation_turn_response_sender.js';
import { ConversationTurnEvent } from './types.js';
import { BedrockConverseAdapter } from './bedrock_converse_adapter.js';

/**
* TODO docs.
*/
export class ConversationTurnExecutor {
/**
* TODO docs
*/
constructor(private readonly event: ConversationTurnEvent) {}

execute = async (): Promise<void> => {
console.log(
`Handling conversation turn event, currentMessageId=${this.event.currentMessageId}, conversationId=${this.event.conversationId}`
);

const assistantResponse = await new BedrockConverseAdapter(
this.event
).askBedrock();

await new ConversationTurnResponseSender(this.event).respond(
assistantResponse
);

console.log(
`Conversation turn event handled successfully, currentMessageId=${this.event.currentMessageId}, conversationId=${this.event.conversationId}`
);
};
}
Loading
Loading