Repository navigation
Expand file tree
/
Copy path20_tools.ts
More file actions
226 lines (201 loc) · 8.95 KB
/
Copy path20_tools.ts
File metadata and controls
226 lines (201 loc) · 8.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
* @title Defining and using AI tools
*
* Define tools with schemas, group them into toolkits, implement handlers,
* and pass them to `LanguageModel.generateText`.
*/
import { OpenAiClient, OpenAiLanguageModel, OpenAiTool } from "@effect/ai-openai"
import { Config, Context, Effect, Layer, Schema } from "effect"
import { AiError, LanguageModel, Tool, Toolkit } from "effect/ai"
import { FetchHttpClient } from "effect/http"
// ---------------------------------------------------------------------------
// 1. Defining tools
// ---------------------------------------------------------------------------
const ProductId = Schema.String.pipe(Schema.brand("ProductId")).annotate({
description: "A unique identifier for a product, e.g. 'p-123'"
})
class Product extends Schema.Class<Product>("acme/domain/Product")({
id: ProductId,
name: Schema.String,
price: Schema.Finite
}) {}
// Each tool has a name, an optional description, a parameters schema that the
// model fills in, and a success schema for the handler result. The description
// is shown to the model to help it decide when to call the tool.
const SearchProducts = Tool.make("SearchProducts", {
description: "Search the product catalog by keyword",
parameters: Schema.Struct({
query: Schema.String.annotate({
// Add a description to individual parameters for even better model
// guidance.
description: "The search query, e.g. 'wireless headphones'"
}),
maxResults: Schema.Natural.pipe(Schema.withDecodingDefault(Effect.succeed(10))).annotate({
description: "The maximum number of results to return"
})
}),
success: Schema.Array(Product),
// The strategy used for handling errors returned from tool call handler
// execution.
//
// If set to `"error"` (the default), errors that occur during tool call handler
// execution will be returned in the error channel of the calling effect.
//
// If set to `"return"`, errors that occur during tool call handler execution
// will be captured and returned as part of the tool call result.
failureMode: "error"
})
const GetInventory = Tool.make("GetInventory", {
description: "Check current stock level for a product",
parameters: Schema.Struct({
productId: ProductId
}),
success: Schema.Struct({
productId: ProductId,
available: Schema.Natural
})
})
// ---------------------------------------------------------------------------
// 2. Grouping tools into a Toolkit
// ---------------------------------------------------------------------------
// `Toolkit.make` accepts any number of tools and produces a typed toolkit that
// knows the names and schemas of every tool it contains.
const ProductToolkit = Toolkit.make(SearchProducts, GetInventory)
// ---------------------------------------------------------------------------
// 3. Implementing handlers via toLayer
// ---------------------------------------------------------------------------
// `toLayer` returns a `Layer` that satisfies the handler requirements for every
// tool in the toolkit. Each handler receives the decoded parameters and returns
// an Effect producing the success type.
const ProductToolkitLayer = ProductToolkit.toLayer(Effect.gen(function*() {
yield* Effect.log("Initializing ProductToolkitLive")
// Here you could access other services or resources needed to implement the
// handlers, e.g. a database client or external API client.
//
// const client = yield* SomeDatabaseClient
return ProductToolkit.of({
SearchProducts: Effect.fn("ProductToolkit.SearchProducts")(function*({ query, maxResults }) {
return [
new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }),
new Product({ id: ProductId.make("p-2"), name: `${query} gadget`, price: 29.99 })
].slice(0, maxResults)
}),
GetInventory: Effect.fn("ProductToolkit.GetInventory")(function*({ productId }) {
return { productId, available: 42 }
})
})
}))
// ---------------------------------------------------------------------------
// 4. Using tools with LanguageModel
// ---------------------------------------------------------------------------
// Provider setup (same pattern as the language-model example).
const OpenAiClientLayer = OpenAiClient.layerConfig({
apiKey: Config.Redacted("OPENAI_API_KEY")
}).pipe(Layer.provide(FetchHttpClient.layer))
export class ProductAssistantError extends Schema.TaggedError<ProductAssistantError>()(
"ProductAssistantError",
{ reason: AiError.AiErrorReason }
) {}
// Wrap tool-enabled generation in a service
export class ProductAssistant extends Context.Service<ProductAssistant, {
answer(question: string): Effect.Effect<{
readonly text: string
readonly toolCallCount: number
}, ProductAssistantError>
}>()("docs/ProductAssistant") {
static readonly layer = Layer.effect(
ProductAssistant,
Effect.gen(function*() {
// Access the toolkit's handlers by yielding the toolkit definition.
const toolkit = yield* ProductToolkit
// Choose a model to use
const model = yield* OpenAiLanguageModel.model("gpt-5.2").captureRequirements
const answer = Effect.fn("ProductAssistant.answer")(
function*(question: string) {
// Pass the toolkit to `generateText`. The model can call any tool in
// the toolkit; the framework resolves parameters, invokes handlers,
// and feeds results back automatically.
const response = yield* LanguageModel.generateText({
prompt: question,
toolkit,
// You can set `toolChoice` to "required" to force the model to call
// a tool before responding with text.
//
// By default it is set to "auto"
toolChoice: "required"
})
// -------------------------------------------------------------------
// 5. Inspecting tool calls and results
// -------------------------------------------------------------------
// `response.toolCalls` lists every tool the model invoked, each with
// the tool name, a unique id, and the decoded parameters.
for (const call of response.toolCalls) {
yield* Effect.log(`Tool call: ${call.name} id=${call.id}`)
}
// `response.toolResults` lists the resolved results, each with the
// tool name, id, decoded result, and an `isFailure` flag.
for (const result of response.toolResults) {
yield* Effect.log(
`Tool result: ${result.name} id=${result.id} isFailure=${result.isFailure}`
)
}
return {
text: response.text,
toolCallCount: response.toolCalls.length
}
},
// Provide the chosen model to use
Effect.provide(model),
(_) => _,
// Map AI errors into our domain error type
Effect.catchTag(
"AiError",
(error) =>
Effect.fail(
new ProductAssistantError({
reason: error.reason
})
),
// For unexpected errors, die with the original error
(e) => Effect.die(e)
)
)
return ProductAssistant.of({ answer })
})
).pipe(
// The toolkit handler layer must be provided so the framework can invoke
// the tool handlers when the model makes tool calls.
Layer.provide(ProductToolkitLayer),
// Also provide the openai client required by OpenAiLanguageModel.model
Layer.provide(OpenAiClientLayer)
)
}
// ---------------------------------------------------------------------------
// 6. Provider-defined tools
// ---------------------------------------------------------------------------
// Some providers offer built-in tools (web search, code interpreter, etc.)
// that run server-side. Use `Tool.providerDefined` or the pre-built
// definitions from provider packages.
// OpenAI's web search tool is pre-defined in `@effect/ai-openai`. Calling it
// produces a tool instance that can be merged into any toolkit.
const webSearch = OpenAiTool.WebSearch({
search_context_size: "medium"
})
// Combine user-defined and provider-defined tools in a single toolkit.
const AssistantToolkit = Toolkit.make(SearchProducts, GetInventory, webSearch)
// Only user-defined tools that require handlers appear in `toLayer`. The
// provider-defined `WebSearch` is executed server-side by the provider.
export const AssistantToolkitLayer = AssistantToolkit.toLayer(Effect.gen(function*() {
yield* Effect.log("Initializing AssistantToolkitLive")
return AssistantToolkit.of({
SearchProducts: Effect.fn("AssistantToolkit.SearchProducts")(function*({ query, maxResults }) {
return [
new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }),
new Product({ id: ProductId.make("p-2"), name: `${query} gadget`, price: 29.99 })
].slice(0, maxResults)
}),
GetInventory: Effect.fn("AssistantToolkit.GetInventory")(function*({ productId }) {
return { productId, available: 42 }
})
})
}))