| 1 | /** |
| 2 | * @title Using LanguageModel for text, objects, and streams |
| 3 | * |
| 4 | * Configure a provider once, then use `LanguageModel` for plain text |
| 5 | * generation, schema-validated object generation, and streaming responses. |
| 6 | */ |
| 7 | import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic" |
| 8 | import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" |
| 9 | import { Config, Context, Effect, ExecutionPlan, Layer, Schema, Stream } from "effect" |
| 10 | import { AiError, LanguageModel, Model, type Response } from "effect/ai" |
| 11 | import { FetchHttpClient } from "effect/http" |
| 12 | import { LaunchPlan } from "./fixtures/domain/LaunchPlan.ts" |
| 13 | |
| 14 | // You can use Config to create ai clients |
| 15 | const AnthropicClientLayer = AnthropicClient.layerConfig({ |
| 16 | apiKey: Config.Redacted("ANTHROPIC_API_KEY") |
| 17 | }).pipe( |
| 18 | // Providers typically require an HttpClient, but you can choose which one to |
| 19 | // use. |
| 20 | Layer.provide(FetchHttpClient.layer) |
| 21 | ) |
| 22 | |
| 23 | const OpenAiClientLayer = OpenAiClient.layerConfig({ |
| 24 | apiKey: Config.Redacted("OPENAI_API_KEY") |
| 25 | }).pipe( |
| 26 | Layer.provide(FetchHttpClient.layer) |
| 27 | ) |
| 28 | |
| 29 | export class AiWriterError extends Schema.TaggedError<AiWriterError>()("AiWriterError", { |
| 30 | // AiErrorReason is a Schema, so we can include it directly in our custom |
| 31 | // error schema. |
| 32 | reason: AiError.AiErrorReason |
| 33 | }) { |
| 34 | static fromAiError(error: AiError.AiError) { |
| 35 | return new AiWriterError({ |
| 36 | reason: error.reason |
| 37 | }) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // You can use `ExecutionPlan` to define a strategy for trying multiple |
| 42 | // providers with different configurations. In this example, we try a cheaper |
| 43 | // OpenAI model first, then fall back to a more expensive Anthropic model if the |
| 44 | // first one fails. |
| 45 | const DraftPlan = ExecutionPlan.make( |
| 46 | { |
| 47 | provide: OpenAiLanguageModel.model("gpt-5.2"), |
| 48 | // Attempt to use the openai model up to 3 times before falling back to the |
| 49 | // anthropic model. |
| 50 | attempts: 3 |
| 51 | }, |
| 52 | { |
| 53 | provide: AnthropicLanguageModel.model("claude-opus-4-6"), |
| 54 | attempts: 2 |
| 55 | } |
| 56 | ) |
| 57 | |
| 58 | export class AiWriter extends Context.Service<AiWriter, { |
| 59 | draftAnnouncement(product: string): Effect.Effect<{ |
| 60 | readonly provider: string |
| 61 | readonly text: string |
| 62 | }, AiWriterError> |
| 63 | extractLaunchPlan(notes: string): Effect.Effect<LaunchPlan, AiWriterError> |
| 64 | streamReleaseHighlights(version: string): Stream.Stream<string, AiWriterError> |
| 65 | }>()("docs/AiWriter") { |
| 66 | static readonly layer = Layer.effect( |
| 67 | AiWriter, |
| 68 | Effect.gen(function*() { |
| 69 | // Calling `captureRequirements` on an `ExecutionPlan` will move the |
| 70 | // requirements of the plan (in this case the ai clients) into the Layer |
| 71 | // requirements. |
| 72 | const draftsModel = yield* DraftPlan.captureRequirements |
| 73 | |
| 74 | // Use a different model for the launch plan extraction |
| 75 | const launchPlanModel = yield* OpenAiLanguageModel.model("gpt-4.1").captureRequirements |
| 76 | |
| 77 | const draftAnnouncement = Effect.fn("AiWriter.draftAnnouncement")( |
| 78 | function*(product: string) { |
| 79 | const model = yield* LanguageModel.LanguageModel |
| 80 | const provider = yield* Model.ProviderName |
| 81 | const response = yield* model.generateText({ |
| 82 | prompt: `Write a short launch announcement for ${product}. ` + |
| 83 | "Keep it concise and include one concrete user benefit." |
| 84 | }) |
| 85 | |
| 86 | // `LanguageModel.generateText` exposes convenience fields so you can |
| 87 | // inspect usage and finish reason without parsing content parts. |
| 88 | yield* Effect.logInfo( |
| 89 | `${provider} finished with ${response.finishReason}. outputTokens=${response.usage.outputTokens.total}` |
| 90 | ) |
| 91 | |
| 92 | return { |
| 93 | provider, |
| 94 | text: response.text |
| 95 | } |
| 96 | }, |
| 97 | // To apply an `ExecutionPlan`, we use `Effect.withExecutionPlan` |
| 98 | Effect.withExecutionPlan(draftsModel), |
| 99 | // Map AiError into our custom error type |
| 100 | Effect.mapError((error) => AiWriterError.fromAiError(error)) |
| 101 | ) |
| 102 | |
| 103 | const extractLaunchPlan = Effect.fn("AiWriter.extractLaunchPlan")( |
| 104 | function*(notes: string) { |
| 105 | const model = yield* LanguageModel.LanguageModel |
| 106 | const response = yield* model.generateObject({ |
| 107 | objectName: "launch_plan", |
| 108 | prompt: |
| 109 | "Convert these notes into a launch plan object with audience, channels, launchDate, summary, and keyRisks:\n" + |
| 110 | notes, |
| 111 | // The generated object is validated and decoded through this schema. |
| 112 | schema: LaunchPlan |
| 113 | }) |
| 114 | |
| 115 | return response.value |
| 116 | }, |
| 117 | // The .model(...) apis return a Layer that can be used with |
| 118 | // Effect.provide |
| 119 | Effect.provide(launchPlanModel), |
| 120 | // Map AiError into our custom error type |
| 121 | Effect.mapError((error) => AiWriterError.fromAiError(error)) |
| 122 | ) |
| 123 | |
| 124 | const streamReleaseHighlights = (version: string) => |
| 125 | LanguageModel.streamText({ |
| 126 | prompt: `Write release highlights for version ${version} as a short bulleted list.` |
| 127 | }).pipe( |
| 128 | Stream.filter((part): part is Response.TextDeltaPart => part.type === "text-delta"), |
| 129 | Stream.map((part) => part.delta), |
| 130 | Stream.provide(launchPlanModel), |
| 131 | // Map AiError into our custom error type |
| 132 | Stream.mapError((error) => AiWriterError.fromAiError(error)) |
| 133 | ) |
| 134 | |
| 135 | return AiWriter.of({ |
| 136 | draftAnnouncement, |
| 137 | extractLaunchPlan, |
| 138 | streamReleaseHighlights |
| 139 | }) |
| 140 | }) |
| 141 | ).pipe( |
| 142 | // This Layer has requirements for both the OpenAI and Anthropic clients, |
| 143 | // since the ExecutionPlan includes models from both providers. |
| 144 | Layer.provide([OpenAiClientLayer, AnthropicClientLayer]) |
| 145 | ) |
| 146 | } |
| 147 | |
| 148 | // We can now use `AiWriter` like any other Effect service. |
| 149 | export const program: Effect.Effect< |
| 150 | void, |
| 151 | AiWriterError, |
| 152 | AiWriter |
| 153 | > = Effect.gen(function*() { |
| 154 | const writer = yield* AiWriter |
| 155 | yield* writer.draftAnnouncement("Effect Cloud") |
| 156 | }) |