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