Skip to content

Commit ff3feb8

Browse files
authored
update conversation > tools page with more information / examples (#8113)
1 parent e6af6e0 commit ff3feb8

File tree

1 file changed

+141
-77
lines changed
  • src/pages/[platform]/ai/conversation/tools

1 file changed

+141
-77
lines changed

src/pages/[platform]/ai/conversation/tools/index.mdx

+141-77
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,41 @@ export function getStaticProps(context) {
2929
}
3030

3131

32+
Tools allow LLMs to query information to respond with current and relevant information. They are invoked only if the LLM requests to use one based on the user's message and the tool's description.
3233

33-
Tools allow LLMs to take action or query information so it can respond with up to date information. There are a few different ways to define LLM tools in the Amplify AI kit.
34+
There are a few different ways to define LLM tools in the Amplify AI kit.
3435

3536
1. Model tools
3637
2. Query tools
3738
3. Lambda tools
3839

39-
The easiest way you can define tools for the LLM to use is with data models and custom queries in your data schema. When you define tools in your data schema, Amplify will take care of all of the heavy lifting required to properly implement such as:
40+
The easiest way to define tools for your conversation route is with `a.ai.dataTool()` for data models and custom queries in your data schema. When you define a tool for a conversation route, Amplify takes care of the heavy lifting:
4041

41-
* **Describing the tools to the LLM:** because each tool is a custom query or data model that is defined in the schema, Amplify knows the input shape needed for that tool
42-
* **Invoking the tool with the right parameters:** after the LLM responds it wants to call a tool, the code that initially called the LLM needs to then run that code.
43-
* **Maintaining the caller identity and authorization:** we don't want users to have access to more data through the LLM than they normally would, so when the LLM wants to invoke a tool we will call it with the user's identity. For example, if the LLM wanted to invoke a query to list Todos, it would only return the todos of the user and not everyone's todos.
42+
* **Describing the tools to the LLM:** Each tool definition is an Amplify model query or custom query that is defined in the schema. Amplify knows the input parameters needed for that tool and describes them to the LLM.
43+
* **Invoking the tool with the right parameters:** After the LLM requests to use a tool with necessary input parameters, the conversation handler Lambda function invokes the tool, returns the result to the LLM, and continues the conversation.
44+
* **Maintaining the caller identity and authorization:** Through tools, the LLM can only access data that the application user has access to. When the LLM requests to invoke a tool, we will call it with the user's identity. For example, if the LLM wanted to invoke a query to list Todos, it would only return the todos that user has access to.
4445

4546
## Model tools
4647

47-
You can give the LLM access to your data models by referencing them in an `a.ai.dataTool()` with a reference to a model in your data schema.
48+
You can give the LLM access to your data models by referencing them in an `a.ai.dataTool()` with a reference to a model in your data schema. This requires that the model uses at least one of the following authorization strategies:
49+
50+
**[Per user data access](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/per-user-per-owner-data-access/)**
51+
- `owner()`
52+
- `ownerDefinedIn()`
53+
- `ownersDefinedIn()`
54+
55+
**[Any signed-in user data access](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/signed-in-user-data-access/)**
56+
- `authenticated()`
57+
58+
**[Per user group data access](https://docs.amplify.aws/react/build-a-backend/data/customize-authz/user-group-based-data-access/)**
59+
- `group()`
60+
- `groupsDefinedIn()`
61+
- `groups()`
62+
- `groupsDefinedIn()`
63+
64+
```ts title="amplify/data/resource.ts"
65+
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
4866

49-
```ts
5067
const schema = a.schema({
5168
Post: a.model({
5269
title: a.string(),
@@ -59,9 +76,14 @@ const schema = a.schema({
5976
systemPrompt: 'Hello, world!',
6077
tools: [
6178
a.ai.dataTool({
79+
// The name of the tool as it will be referenced in the message to the LLM
6280
name: 'PostQuery',
81+
// The description of the tool provided to the LLM.
82+
// Use this to help the LLM understand when to use the tool.
6383
description: 'Searches for Post records',
84+
// A reference to the `a.model()` that the tool will use
6485
model: a.ref('Post'),
86+
// The operation to perform on the model
6587
modelOperation: 'list',
6688
}),
6789
],
@@ -71,26 +93,29 @@ const schema = a.schema({
7193

7294
This will let the LLM list and filter `Post` records. Because the data schema has all the information about the shape of a `Post` record, the data tool will provide that information to the LLM so you don't have to. Also, the Amplify AI kit handles authorizing the tool use requests based on the caller's identity. This means if you have an owner-based model, the LLM will only be able to query the user's records.
7395

74-
*The only supported model operation currently is 'list'*
96+
<Callout type="info">
97+
98+
The only supported model operation is `'list'`.
99+
100+
</Callout>
75101

76102
## Query tools
77103

78-
You can also give the LLM access to custom queries. You can define a custom query with a [Function](/[platform]/build-a-backend/functions/set-up-function/) handler and then reference that custom query as a tool.
104+
You can also give the LLM access to custom queries defined in your data schema. To do so, define a custom query with a [function or custom handler](https://docs.amplify.aws/react/build-a-backend/data/custom-business-logic/) and then reference that custom query as a tool. This requires that the custom query uses the `allow.authenticated()` authorization strategy.
79105

80106
```ts title="amplify/data/resource.ts"
81-
// highlight-start
82107
import { type ClientSchema, a, defineData, defineFunction } from "@aws-amplify/backend";
83-
// highlight-end
84108

85-
// highlight-start
86109
export const getWeather = defineFunction({
87110
name: 'getWeather',
88-
entry: 'getWeather.ts'
111+
entry: './getWeather.ts',
112+
environment: {
113+
API_ENDPOINT: 'MY_API_ENDPOINT',
114+
API_KEY: secret('MY_API_KEY'),
115+
},
89116
});
90-
// highlight-end
91117

92118
const schema = a.schema({
93-
// highlight-start
94119
getWeather: a.query()
95120
.arguments({ city: a.string() })
96121
.returns(a.customType({
@@ -99,68 +124,73 @@ const schema = a.schema({
99124
}))
100125
.handler(a.handler.function(getWeather))
101126
.authorization((allow) => allow.authenticated()),
102-
// highlight-end
103127

104128
chat: a.conversation({
105129
aiModel: a.ai.model('Claude 3 Haiku'),
106130
systemPrompt: 'You are a helpful assistant',
107-
// highlight-start
108131
tools: [
109132
a.ai.dataTool({
110-
name: 'getWeather',
133+
// The name of the tool as it will be referenced in the LLM prompt
134+
name: 'get_weather',
135+
// The description of the tool provided to the LLM.
136+
// Use this to help the LLM understand when to use the tool.
111137
description: 'Gets the weather for a given city',
138+
// A reference to the `a.query()` that the tool will invoke.
112139
query: a.ref('getWeather'),
113140
}),
114141
]
115-
// highlight-end
116-
}),
142+
})
143+
.authorization((allow) => allow.owner()),
117144
});
118145
```
119146

120-
Because the definition of the query itself has the shape of the inputs and outputs (arguments and returns), the Amplify data tool can automatically tell the LLM exactly how to call the custom query.
121-
122-
<Callout>
147+
The Amplify data tool takes care of specifying the necessary input parameters to the LLM based on the query definition.
123148

124-
The description of the tool is very important to help the LLM know when to use that tool. The more descriptive you are about what the tool does, the better.
125-
126-
</Callout>
127-
128-
Here is an example Lambda function handler for our `getWeather` query:
149+
Below is an illustrative example of a Lambda function handler for the `getWeather` query.
129150

130151
```ts title="amplify/data/getWeather.ts"
152+
import { env } from "$amplify/env/getWeather";
131153
import type { Schema } from "./resource";
132154

133155
export const handler: Schema["getWeather"]["functionHandler"] = async (
134156
event
135157
) => {
136-
// This returns a mock value, but you can connect to any API, database, or other service
137-
return {
138-
value: 42,
139-
unit: 'C'
140-
};
158+
const { city } = event.arguments;
159+
if (!city) {
160+
throw new Error('City is required');
161+
}
162+
163+
const url = `${env.API_ENDPOINT}?city=${encodeURIComponent(city)}`;
164+
const request = new Request(url, {
165+
headers: {
166+
Authorization: `Bearer ${env.API_KEY}`
167+
}
168+
});
169+
170+
const response = await fetch(request);
171+
const weather = await response.json();
172+
return weather;
141173
}
142174
```
143175

144176
Lastly, you will need to update your **`amplify/backend.ts`** file to include the newly defined `getWeather` function.
145177

146178
```ts title="amplify/backend.ts"
147-
// highlight-start
148-
import { getWeather } from "./data/resource";
149-
// highlight-end
179+
import { defineBackend } from '@aws-amplify/backend';
180+
import { auth } from './auth/resource';
181+
import { data, getWeather } from './data/resource';
150182

151183
const backend = defineBackend({
152184
auth,
153185
data,
154-
// highlight-start
155186
getWeather
156-
// highlight-end
157187
});
158188
```
159189

160190

161-
## Connect to any AWS Service
191+
### Connect to any AWS Service
162192

163-
You can connect to any AWS service by defining a custom query and calling that service in the function handler. Then you will need to provide the Lambda with the proper permissions to call the AWS service.
193+
You can connect to any AWS service by defining a custom query and calling that service in the function handler. To properly authorize the custom query function to call the AWS service, you will need to provide the Lambda with the proper permissions.
164194

165195
```ts title="amplify/backend.ts"
166196
import { defineBackend } from "@aws-amplify/backend";
@@ -185,25 +215,23 @@ backend.getWeather.resources.lambda.addToRolePolicy(
185215
)
186216
```
187217

188-
189-
190218
## Custom Lambda Tools
191219

192-
Conversation routes can also have completely custom tools defined in a Lambda handler.
220+
You can also define a tool that executes in the conversation handler AWS Lambda function. This is useful if you want to define a tool that is not related to your data schema or that does simple tasks within the Lambda function runtime.
193221

194-
### Install the backend-ai package
222+
First install the `@aws-amplify/backend-ai` package.
195223

196-
```bash
224+
```bash title="Terminal"
197225
npm install @aws-amplify/backend-ai
198226
```
199227

200-
### Create a custom conversation handler function
228+
Define a custom conversation handler function in your data schema and reference the function in the `handler` property of the `a.conversation()` definition.
201229

202230
```ts title="amplify/data/resource.ts"
203231
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
204232
import { defineConversationHandlerFunction } from '@aws-amplify/backend-ai/conversation';
205233

206-
const chatHandler = defineConversationHandlerFunction({
234+
export const chatHandler = defineConversationHandlerFunction({
207235
entry: './chatHandler.ts',
208236
name: 'customChatHandler',
209237
models: [
@@ -217,52 +245,88 @@ const schema = a.schema({
217245
systemPrompt: "You are a helpful assistant",
218246
handler: chatHandler,
219247
})
248+
.authorization((allow) => allow.owner()),
220249
})
221250
```
222251

223-
### Implement the custom handler
252+
Define the executable tool(s) and handler. Below is an illustrative example of a custom conversation handler function that defines a `calculator` tool.
224253

225254
```ts title="amplify/data/chatHandler.ts"
226255
import {
227256
ConversationTurnEvent,
228-
handleConversationTurnEvent,
229-
} from '@aws-amplify/ai-constructs/conversation/runtime';
230-
import { createExecutableTool } from '@aws-amplify/backend-ai/conversation/runtime';
231-
232-
const thermometer = createExecutableTool(
233-
'thermometer',
234-
'Returns current temperature in a city',
235-
{
236-
json: {
237-
type: 'object',
238-
'properties': {
239-
'city': {
240-
'type': 'string',
241-
'description': 'The city name'
242-
}
257+
createExecutableTool,
258+
handleConversationTurnEvent
259+
} from '@aws-amplify/backend-ai/conversation/runtime';
260+
261+
const jsonSchema = {
262+
json: {
263+
type: 'object',
264+
properties: {
265+
'operator': {
266+
'type': 'string',
267+
'enum': ['+', '-', '*', '/'],
268+
'description': 'The arithmetic operator to use'
243269
},
244-
required: ['city']
245-
}
246-
},
270+
'operands': {
271+
'type': 'array',
272+
'items': {
273+
'type': 'number'
274+
},
275+
'minItems': 2,
276+
'maxItems': 2,
277+
'description': 'Two numbers to perform the operation on'
278+
}
279+
},
280+
required: ['operator', 'operands']
281+
}
282+
} as const;
283+
// declare as const to allow the input type to be derived from the JSON schema in the tool handler definition.
284+
285+
const calculator = createExecutableTool(
286+
'calculator',
287+
'Returns the result of a simple calculation',
288+
jsonSchema,
289+
// input type is derived from the JSON schema
247290
(input) => {
248-
if (input.city === 'Seattle') {
249-
return Promise.resolve({
250-
text: '75F',
251-
});
291+
const [a, b] = input.operands;
292+
switch (input.operator) {
293+
case '+': return Promise.resolve({ text: (a + b).toString() });
294+
case '-': return Promise.resolve({ text: (a - b).toString() });
295+
case '*': return Promise.resolve({ text: (a * b).toString() });
296+
case '/':
297+
if (b === 0) throw new Error('Division by zero');
298+
return Promise.resolve({ text: (a / b).toString() });
299+
default:
300+
throw new Error('Invalid operator');
252301
}
253-
return Promise.resolve({
254-
text: 'unknown'
255-
})
256302
},
257303
);
258304

259-
/**
260-
* Handler with simple tool.
261-
*/
262305
export const handler = async (event: ConversationTurnEvent) => {
263306
await handleConversationTurnEvent(event, {
264-
tools: [thermometer],
307+
tools: [calculator],
265308
});
266309
};
267310
```
268311

312+
Note that we throw an error in the `calculator` tool example above if the input is invalid. This error is surfaced to the LLM by the conversation handler function. Depending on the error message, the LLM may try to use the tool again with different input or completing its response with test for the user.
313+
314+
Lastly, update your backend definition to include the newly defined `chatHandler` function.
315+
316+
```ts title="amplify/backend.ts"
317+
import { defineBackend } from '@aws-amplify/backend';
318+
import { auth } from './auth/resource';
319+
import { data, chatHandler } from './data/resource';
320+
321+
defineBackend({
322+
auth,
323+
data,
324+
chatHandler,
325+
});
326+
```
327+
328+
### Best Practices
329+
330+
- Validate and sanitize any input from the LLM before using it in your application, e.g. don't use it directly in a database query or use `eval()` to execute it.
331+
- Handle errors gracefully and provide meaningful error messages.
332+
- Log and monitor tool usage to detect potential misuse or issues.

0 commit comments

Comments
 (0)