| 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 | */ |
| 7 | import { OpenAiClient, OpenAiLanguageModel, OpenAiTool } from "@effect/ai-openai" |
| 8 | import { Config, Context, Effect, Layer, Schema } from "effect" |
| 9 | import { AiError, LanguageModel, Tool, Toolkit } from "effect/ai" |
| 10 | import { FetchHttpClient } from "effect/http" |
| 11 | |
| 12 | // --------------------------------------------------------------------------- |
| 13 | // 1. Defining tools |
| 14 | // --------------------------------------------------------------------------- |
| 15 | |
| 16 | const ProductId = Schema.String.pipe(Schema.brand("ProductId")).annotate({ |
| 17 | description: "A unique identifier for a product, e.g. 'p-123'" |
| 18 | }) |
| 19 | |
| 20 | class 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. |
| 29 | const 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 | |
| 53 | const 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. |
| 70 | const 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. |
| 79 | const 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). |
| 103 | const OpenAiClientLayer = OpenAiClient.layerConfig({ |
| 104 | apiKey: Config.Redacted("OPENAI_API_KEY") |
| 105 | }).pipe(Layer.provide(FetchHttpClient.layer)) |
| 106 | |
| 107 | export class ProductAssistantError extends Schema.TaggedError<ProductAssistantError>()( |
| 108 | "ProductAssistantError", |
| 109 | { reason: AiError.AiErrorReason } |
| 110 | ) {} |
| 111 | |
| 112 | // Wrap tool-enabled generation in a service |
| 113 | export 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. |
| 204 | const webSearch = OpenAiTool.WebSearch({ |
| 205 | search_context_size: "medium" |
| 206 | }) |
| 207 | |
| 208 | // Combine user-defined and provider-defined tools in a single toolkit. |
| 209 | const 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. |
| 213 | export 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 | })) |