Skip to content

Commit

Permalink
Add AWSLambda tool (#727)
Browse files Browse the repository at this point in the history
* Add AWSLambda tool

* Update yarn.lock

* Update yarn.lock

* updated package.json

* Remove from index, add entrypoint, subclass dynamictool

* Update lambda_agent.md

---------

Co-authored-by: Nuno Campos <nuno@boringbits.io>
  • Loading branch information
jasondotparse and nfcampos authored Apr 14, 2023
1 parent f913496 commit 91d90ad
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/docs/modules/agents/tools/integrations/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import DocCardList from "@theme/DocCardList";

LangChain provides the following tools you can use out of the box:

- `AWSLambda` - A wrapper around the AWS Lambda API, invoked via the Amazon Web Services Node.js SDK. Useful for invoking serverless functions with any behavior which you need to provide to an Agent.
- `BingSerpAPI` - A wrapper around the Bing Search API. Useful for when you need to answer questions about current events. Input should be a search query.
- `Calculator` - Useful for getting the result of a math expression. The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.
- `IFTTTWebHook` - A wrapper around the IFTTT Webhook API. Useful for triggering IFTTT actions.
Expand Down
42 changes: 42 additions & 0 deletions docs/docs/modules/agents/tools/lambda_agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
sidebar_label: Agent with AWS Lambda
hide_table_of_contents: true
---

# Agent with AWS Lambda Integration

Full docs here: https://docs.aws.amazon.com/lambda/index.html

**AWS Lambda** is a serverless computing service provided by Amazon Web Services (AWS), designed to allow developers to build and run applications and services without the need for provisioning or managing servers. This serverless architecture enables you to focus on writing and deploying code, while AWS automatically takes care of scaling, patching, and managing the infrastructure required to run your applications.

By including a AWSLambda in the list of tools provided to an Agent, you can grant your Agent the ability to invoke code running in your AWS Cloud for whatever purposes you need.

When an Agent uses the AWSLambda tool, it will provide an argument of type `string` which will in turn be passed into the Lambda function via the `event` parameter.

This quick start will demonstrate how an Agent could use a Lambda function to send an email via [Amazon Simple Email Service](https://aws.amazon.com/ses/). The lambda code which sends the email is not provided, but if you'd like to learn how this could be done, see [here](https://repost.aws/knowledge-center/lambda-send-email-ses). Keep in mind this is an intentionally simple example; Lambda can used to execute code for a near infinite number of other purposes (including executing more Langchains)!

```typescript
import { OpenAI } from "langchain";
import { SerpAPI, AWSLambda } from "langchain/tools";
import { initializeAgentExecutor } from "langchain/agents";
const model = new OpenAI({ temperature: 0 });
const emailSenderTool = new AWSLambda(
"email-sender", // name: tell the Agent what this tool is called
"Sends an email with the specified content to testing123@gmail.com", // description: tell the Agent precisely what the tool does
{
region: "us-east-1", // the region in which the function is deployed in the AWS cloud
accessKeyId: "abc123", // the access key for a user which has the IAM permissions necessary to invoke the function
secretAccessKey: "xyz456", // the secret access key for a user which has the IAM permissions necessary to invoke the function
functionName: "SendEmailViaSES", // the function name as seen in the AWS Lambda console
}
);
const tools = [emailSenderTool, new SerpAPI("api_key_goes_here")];
const executor = await initializeAgentExecutor(
tools,
model,
"zero-shot-react-description"
);
const input = `Find out the capital of Croatia. Once you have it, email the answer to testing123@gmail.com.`;
const result = await executor.call({ input });
console.log(result);
```
3 changes: 3 additions & 0 deletions langchain/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ base_language.d.ts
tools.cjs
tools.js
tools.d.ts
tools/aws_lambda.cjs
tools/aws_lambda.js
tools/aws_lambda.d.ts
tools/calculator.cjs
tools/calculator.js
tools/calculator.d.ts
Expand Down
13 changes: 13 additions & 0 deletions langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"tools.cjs",
"tools.js",
"tools.d.ts",
"tools/aws_lambda.cjs",
"tools/aws_lambda.js",
"tools/aws_lambda.d.ts",
"tools/calculator.cjs",
"tools/calculator.js",
"tools/calculator.d.ts",
Expand Down Expand Up @@ -255,6 +258,7 @@
"author": "LangChain",
"license": "MIT",
"devDependencies": {
"@aws-sdk/client-lambda": "^3.310.0",
"@aws-sdk/client-s3": "^3.310.0",
"@faker-js/faker": "^7.6.0",
"@getmetal/metal-sdk": "^1.0.12",
Expand Down Expand Up @@ -305,6 +309,7 @@
"typescript": "^4.9.5"
},
"peerDependencies": {
"@aws-sdk/client-lambda": "^3.310.0",
"@aws-sdk/client-s3": "^3.310.0",
"@getmetal/metal-sdk": "*",
"@huggingface/inference": "^1.5.1",
Expand All @@ -330,6 +335,9 @@
"typeorm": "^0.3.12"
},
"peerDependenciesMeta": {
"@aws-sdk/client-lambda": {
"optional": true
},
"@aws-sdk/client-s3": {
"optional": true
},
Expand Down Expand Up @@ -460,6 +468,11 @@
"import": "./tools.js",
"require": "./tools.cjs"
},
"./tools/aws_lambda": {
"types": "./tools/aws_lambda.d.ts",
"import": "./tools/aws_lambda.js",
"require": "./tools/aws_lambda.cjs"
},
"./tools/calculator": {
"types": "./tools/calculator.d.ts",
"import": "./tools/calculator.js",
Expand Down
2 changes: 2 additions & 0 deletions langchain/scripts/create-entrypoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const entrypoints = {
base_language: "base_language/index",
// tools
tools: "tools/index",
"tools/aws_lambda": "tools/aws_lambda",
"tools/calculator": "tools/calculator",
"tools/webbrowser": "tools/webbrowser",
// chains
Expand Down Expand Up @@ -118,6 +119,7 @@ const deprecatedNodeOnly = [
// Therefore they are no tested in the generated test-exports-* packages.
const requiresOptionalDependency = [
"agents/load",
"tools/aws_lambda",
"tools/calculator",
"tools/webbrowser",
"chains/load",
Expand Down
38 changes: 38 additions & 0 deletions langchain/src/agents/tests/aws_lambda.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, jest, expect } from "@jest/globals";
import LambdaClient from "@aws-sdk/client-lambda";

import { AWSLambda } from "../../tools/aws_lambda.js";

// eslint-disable-next-line tree-shaking/no-side-effects-in-initialization
jest.mock("@aws-sdk/client-lambda", () => ({
LambdaClient: jest.fn().mockImplementation(() => ({
send: jest.fn().mockImplementation(() =>
Promise.resolve({
Payload: new TextEncoder().encode(
JSON.stringify({ body: "email sent." })
),
})
),
})),
InvokeCommand: jest.fn().mockImplementation(() => ({})),
}));

test("AWSLambda invokes the correct lambda function and returns the response.body contents", async () => {
if (!LambdaClient) {
// this is to avoid a linting error. S3Client is mocked above.
}

const lambda = new AWSLambda({
name: "email-sender",
description:
"Sends an email with the specified content to holtkam2@gmail.com",
region: "us-east-1",
accessKeyId: "abc123",
secretAccessKey: "xyz456/1T+PzUZ2fd",
functionName: "testFunction1",
});

const result = await lambda.call("Hello world! This is an email.");

expect(result).toBe("email sent.");
});
82 changes: 82 additions & 0 deletions langchain/src/tools/aws_lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { DynamicTool, DynamicToolParams } from "./dynamic.js";

interface LambdaConfig {
region: string;
accessKeyId: string;
secretAccessKey: string;
functionName: string;
}

class AWSLambda extends DynamicTool {
private lambdaConfig: LambdaConfig;

constructor({
name,
description,
...rest
}: LambdaConfig & Omit<DynamicToolParams, "func">) {
super({
name,
description,
func: async (input: string) => this._func(input),
});

this.lambdaConfig = rest;
}

async _func(input: string): Promise<string> {
const { Client, Invoker } = await LambdaImports();

const lambdaClient = new Client({
region: this.lambdaConfig.region,
credentials: {
accessKeyId: this.lambdaConfig.accessKeyId,
secretAccessKey: this.lambdaConfig.secretAccessKey,
},
});

return new Promise((resolve) => {
const payloadUint8Array = new TextEncoder().encode(JSON.stringify(input));

const command = new Invoker({
FunctionName: this.lambdaConfig.functionName,
InvocationType: "RequestResponse",
Payload: payloadUint8Array,
});

lambdaClient
.send(command)
.then((response) => {
const responseData = JSON.parse(
new TextDecoder().decode(response.Payload)
);

resolve(responseData.body ? responseData.body : "request completed.");
})
.catch((error: Error) => {
console.error("Error invoking Lambda function:", error);
resolve("failed to complete request");
});
});
}
}

async function LambdaImports() {
try {
const { LambdaClient, InvokeCommand } = await import(
"@aws-sdk/client-lambda"
);

return {
Client: LambdaClient as typeof LambdaClient,
Invoker: InvokeCommand as typeof InvokeCommand,
};
} catch (e) {
console.error(e);
throw new Error(
"Failed to load @aws-sdk/client-lambda'. Please install it eg. `yarn add @aws-sdk/client-lambda`."
);
}
}

export { AWSLambda };
14 changes: 8 additions & 6 deletions langchain/src/tools/dynamic.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Tool } from "./base.js";

export interface DynamicToolParams {
name: string;
description: string;
func: (arg1: string) => Promise<string>;
returnDirect?: boolean;
}

export class DynamicTool extends Tool {
name: string;

description: string;

func: (arg1: string) => Promise<string>;

constructor(fields: {
name: string;
description: string;
func: (arg1: string) => Promise<string>;
returnDirect?: boolean;
}) {
constructor(fields: DynamicToolParams) {
super();
this.name = fields.name;
this.description = fields.description;
Expand Down
1 change: 1 addition & 0 deletions langchain/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"src/agents/load.ts",
"src/base_language/index.ts",
"src/tools/index.ts",
"src/tools/aws_lambda.ts",
"src/tools/calculator.ts",
"src/tools/webbrowser.ts",
"src/chains/index.ts",
Expand Down
51 changes: 51 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,53 @@ __metadata:
languageName: node
linkType: hard

"@aws-sdk/client-lambda@npm:^3.310.0":
version: 3.310.0
resolution: "@aws-sdk/client-lambda@npm:3.310.0"
dependencies:
"@aws-crypto/sha256-browser": 3.0.0
"@aws-crypto/sha256-js": 3.0.0
"@aws-sdk/client-sts": 3.310.0
"@aws-sdk/config-resolver": 3.310.0
"@aws-sdk/credential-provider-node": 3.310.0
"@aws-sdk/eventstream-serde-browser": 3.310.0
"@aws-sdk/eventstream-serde-config-resolver": 3.310.0
"@aws-sdk/eventstream-serde-node": 3.310.0
"@aws-sdk/fetch-http-handler": 3.310.0
"@aws-sdk/hash-node": 3.310.0
"@aws-sdk/invalid-dependency": 3.310.0
"@aws-sdk/middleware-content-length": 3.310.0
"@aws-sdk/middleware-endpoint": 3.310.0
"@aws-sdk/middleware-host-header": 3.310.0
"@aws-sdk/middleware-logger": 3.310.0
"@aws-sdk/middleware-recursion-detection": 3.310.0
"@aws-sdk/middleware-retry": 3.310.0
"@aws-sdk/middleware-serde": 3.310.0
"@aws-sdk/middleware-signing": 3.310.0
"@aws-sdk/middleware-stack": 3.310.0
"@aws-sdk/middleware-user-agent": 3.310.0
"@aws-sdk/node-config-provider": 3.310.0
"@aws-sdk/node-http-handler": 3.310.0
"@aws-sdk/protocol-http": 3.310.0
"@aws-sdk/smithy-client": 3.310.0
"@aws-sdk/types": 3.310.0
"@aws-sdk/url-parser": 3.310.0
"@aws-sdk/util-base64": 3.310.0
"@aws-sdk/util-body-length-browser": 3.310.0
"@aws-sdk/util-body-length-node": 3.310.0
"@aws-sdk/util-defaults-mode-browser": 3.310.0
"@aws-sdk/util-defaults-mode-node": 3.310.0
"@aws-sdk/util-endpoints": 3.310.0
"@aws-sdk/util-retry": 3.310.0
"@aws-sdk/util-user-agent-browser": 3.310.0
"@aws-sdk/util-user-agent-node": 3.310.0
"@aws-sdk/util-utf8": 3.310.0
"@aws-sdk/util-waiter": 3.310.0
tslib: ^2.5.0
checksum: e0416e32f25727f4591a1bbca534c3ccd2be6c98d0b8dd4b5954dd417428a38d74da152d34ac62cbf5ba3eb191bb97d83ad756cd04b7078ba8d64b3c13774621
languageName: node
linkType: hard

"@aws-sdk/client-s3@npm:^3.310.0":
version: 3.310.0
resolution: "@aws-sdk/client-s3@npm:3.310.0"
Expand Down Expand Up @@ -17023,6 +17070,7 @@ __metadata:
resolution: "langchain@workspace:langchain"
dependencies:
"@anthropic-ai/sdk": ^0.4.3
"@aws-sdk/client-lambda": ^3.310.0
"@aws-sdk/client-s3": ^3.310.0
"@dqbd/tiktoken": ^1.0.4
"@faker-js/faker": ^7.6.0
Expand Down Expand Up @@ -17086,6 +17134,7 @@ __metadata:
yaml: ^2.2.1
zod: ^3.21.4
peerDependencies:
"@aws-sdk/client-lambda": ^3.310.0
"@aws-sdk/client-s3": ^3.310.0
"@getmetal/metal-sdk": "*"
"@huggingface/inference": ^1.5.1
Expand All @@ -17110,6 +17159,8 @@ __metadata:
srt-parser-2: ^1.2.2
typeorm: ^0.3.12
peerDependenciesMeta:
"@aws-sdk/client-lambda":
optional: true
"@aws-sdk/client-s3":
optional: true
"@getmetal/metal-sdk":
Expand Down

1 comment on commit 91d90ad

@vercel
Copy link

@vercel vercel bot commented on 91d90ad Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.