Skip to content

Refactor the AI packages #4766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f4c18f8
initial work refactoring base ai package apis
IMax153 Apr 19, 2025
abae165
refactor openai integration to align with changes to base ai package
IMax153 Apr 19, 2025
f5b5484
refactor anthropic integration to align with changes to base ai package
IMax153 Apr 20, 2025
1920a40
fix eslint
IMax153 Apr 20, 2025
ee4bbb8
add missing doc tag
IMax153 Apr 20, 2025
91705f7
fix docgen
IMax153 Apr 21, 2025
f47e1f3
rename .provide to .use for AiModel and AiPlan
IMax153 Apr 22, 2025
5da3fd6
auto-resolve tool calls in language model methods
IMax153 Apr 23, 2025
b699b6f
fix potential footgun when merging toolkits
IMax153 Apr 23, 2025
1bfc1d6
move ai input type helpers to top level
IMax153 Apr 23, 2025
6c9d21d
move more type helpers to top-level of module
IMax153 Apr 23, 2025
38b265d
use Schema.tag
IMax153 Apr 23, 2025
2ac7f94
support a multi-step AiPlan.make
IMax153 Apr 23, 2025
e8a723a
set generateText as the parent span
mikearnaldi Apr 24, 2025
ea6d54d
make AiTool accepts only params
mikearnaldi Apr 24, 2025
eeb05c2
add maxSteps parameter to AiLanguageModel.generateText control sequen…
IMax153 Apr 24, 2025
210fc3c
fix AiToolkit.toLayer types
IMax153 Apr 24, 2025
03c24fe
fix AiToolkit.HandlersFrom utility
IMax153 Apr 24, 2025
0f41f60
carry types in AiToolkit instance
mikearnaldi Apr 24, 2025
4719f2d
update changeset
IMax153 Apr 24, 2025
3240ac9
add accessors to AiLanguageModel
mikearnaldi Apr 24, 2025
45e5cb5
allow passing an effect of a toolkit instead of a toolkit to generate
mikearnaldi Apr 24, 2025
3d53d18
carry tools in toolkit
mikearnaldi Apr 24, 2025
0ba61cf
fix type
mikearnaldi Apr 24, 2025
ffb8f49
update resolution logic
mikearnaldi Apr 24, 2025
b2a52aa
v2 of ai response
IMax153 Apr 24, 2025
7dfb274
wip
IMax153 Apr 29, 2025
1b6eeaf
rework the AiModel internals
IMax153 May 4, 2025
565988f
fix lockfile
IMax153 May 5, 2025
6303395
remove AiModels
IMax153 May 5, 2025
d147cad
fixup finish part provider metadata
IMax153 May 5, 2025
30c9ac1
disable lsp diagnostics in dtslint (#4824)
mattiamanzati May 6, 2025
8b2c14d
fix docgen
IMax153 May 6, 2025
1887544
support per-request requirements in tool call definitions
IMax153 May 6, 2025
f76c0f3
trick ts
mikearnaldi May 6, 2025
289cbcc
simplify tool choice
IMax153 May 6, 2025
3d3fdbe
support span transformers
IMax153 May 7, 2025
8e7edcf
move CurrentSpanTransformer to generic AiLanguageModel.make (#4831)
tim-smart May 7, 2025
0b2dda3
cleanup AiChat
IMax153 May 8, 2025
68b6d0b
fix AiChat and remove generateObject requirement for a toolCallId (#4…
tim-smart May 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/big-stingrays-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/ai-openai": minor
---

Refactor `@effect/ai-openai` to align with changes to `@effect/ai`
202 changes: 202 additions & 0 deletions .changeset/five-colts-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
---
"@effect/ai": minor
---

This release includes a complete refactor of the internals of the base `@effect/ai` library, with a focus on flexibility for the end user and incorporation of more information from model providers.

## Notable Changes

### `AiLanguageModel` and `AiEmbeddingModel`

The `Completions` service from `@effect/ai` has been renamed to `AiLanguageModel`, and the `Embeddings` service has similarly been renamed to `AiEmbeddingModel`. In addition, `Completions.create` and `Completions.toolkit` have been unified into `AiLanguageModel.generateText`. Similarly, `Completions.stream` and `Completions.toolkitStream` have been unified into `AiLanguageModel.streamText`.

### Structured Outputs

`Completions.structured` has been renamed to `AiLanguageModel.generateObject`, and this method now returns a specialized `AiResponse.WithStructuredOutput` type, which contains a `value` property with the result of the structured output call. This enhancement prevents the end user from having to unnecessarily unwrap an `Option`.

### `AiModel` and `AiPlan`

The `.provide` method on a built `AiModel` / `AiPlan` has been renamed to `.use` to improve clarity given that a user is _using_ the services provided by the model / plan to run a particular piece of code.

In addition, the `AiPlan.fromModel` constructor has been simplified into `AiPlan.make`, which allows you to create an initial `AiPlan` with multiple steps incorporated.

For example:

```ts
import { AiPlan } from "@effect/ai"
import { OpenAiLanguageModel } from "@effect/ai-openai"
import { AnthropicLanguageModel } from "@effect/ai-anthropic"
import { Effect } from "effect"

const main = Effect.gen(function*() {
const plan = yield* AiPlan.make({
model: OpenAiLanguageModel.model("gpt-4"),
attempts: 1
}, {
model: AnthropicLanguageModel.model("claude-3-7-sonnet-latest"),
attempts: 1
}, {
model: AnthropicLanguageModel.model("claude-3-5-sonnet-latest"),
attempts: 1
})

yield* plan.use(program)
})
```

### `AiInput` and `AiResponse`

The `AiInput` and `AiResponse` types have been refactored to allow inclusion of more information and metadata from model providers where possible, such as reasoning output and prompt cache token utilization.

In addition, for an `AiResponse` you can now access metadata that is specific to a given provider. For example, when using OpenAi to generate audio, you can check the input and output audio tokens used:

```ts
import { OpenAiLanguageModel } from "@effect/ai-openai"
import { Effect, Option } from "effect"

const getDadJoke = OpenAiLanguageModel.generateText({
prompt: "Generate a hilarious dad joke"
})

Effect.gen(function*() {
const model = yield* OpenAiLanguageModel.model("gpt-4o")
const response = yield* model.use(getDadJoke)
const metadata = response.getProviderMetadata(OpenAiLanguageModel.ProviderMetadata)
if (Option.isSome(metadata)) {
console.log(metadata.value)
}
})

```

### `AiTool` and `AiToolkit`

The `AiToolkit` has been completely refactored to simplify creating a collection of tools and using those tools in requests to model providers. A new `AiTool` data type has also been introduced to simplify defining tools for a toolkit. `AiToolkit.implement` has been renamed to `AiToolkit.toLayer` for clarity, and defining handlers is now very similar to the way handlers are defined in the `@effect/rpc` library.

A complete example of an `AiToolkit` implementation and usage can be found below:


```ts
import { AiLanguageModel, AiTool, AiToolkit } from "@effect/ai"
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"
import { NodeHttpClient, NodeRuntime } from "@effect/platform-node"
import { Array, Config, Console, Effect, Layer, Schema } from "effect"

// =============================================================================
// Domain Models
// =============================================================================

const DadJoke = Schema.Struct({
id: Schema.String,
joke: Schema.String
})

const SearchResponse = Schema.Struct({
current_page: Schema.Int,
limit: Schema.Int,
next_page: Schema.Int,
previous_page: Schema.Int,
search_term: Schema.String,
results: Schema.Array(DadJoke),
status: Schema.Int,
total_jokes: Schema.Int,
total_pages: Schema.Int
})

// =============================================================================
// Service Definitions
// =============================================================================

export class ICanHazDadJoke extends Effect.Service<ICanHazDadJoke>()("ICanHazDadJoke", {
dependencies: [FetchHttpClient.layer],
effect: Effect.gen(function*() {
const httpClient = (yield* HttpClient.HttpClient).pipe(
HttpClient.mapRequest(HttpClientRequest.prependUrl("https://icanhazdadjoke.com"))
)
const httpClientOk = HttpClient.filterStatusOk(httpClient)

const search = Effect.fn("ICanHazDadJoke.search")(
function(term: string) {
return httpClientOk.get("/search", {
acceptJson: true,
urlParams: { term }
}).pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(SearchResponse)),
Effect.orDie
)
}
)

return {
search
} as const
})
}) {}

// =============================================================================
// Toolkit Definition
// =============================================================================

export class DadJokeTools extends AiToolkit.make(
AiTool.make("GetDadJoke", {
description: "Fetch a dad joke based on a search term from the ICanHazDadJoke API",
success: DadJoke,
parameters: Schema.Struct({
searchTerm: Schema.String
})
})
) {}

// =============================================================================
// Toolkit Handlers
// =============================================================================

export const DadJokeToolHandlers = DadJokeTools.toLayer(
Effect.gen(function*() {
const icanhazdadjoke = yield* ICanHazDadJoke
return {
GetDadJoke: (params) =>
icanhazdadjoke.search(params.searchTerm).pipe(
Effect.flatMap((response) => Array.head(response.results)),
Effect.orDie
)
}
})
).pipe(Layer.provide(ICanHazDadJoke.Default))

// =============================================================================
// Toolkit Usage
// =============================================================================

const makeDadJoke = Effect.gen(function*() {
const languageModel = yield* AiLanguageModel.AiLanguageModel
const toolkit = yield* DadJokeTools

const response = yield* languageModel.generateText({
prompt: "Come up with a dad joke about pirates",
toolkit
})

return yield* languageModel.generateText({
prompt: response
})
})

const program = Effect.gen(function*() {
const model = yield* OpenAiLanguageModel.model("gpt-4o")
const result = yield* model.provide(makeDadJoke)
yield* Console.log(result.text)
})

const OpenAi = OpenAiClient.layerConfig({
apiKey: Config.redacted("OPENAI_API_KEY")
}).pipe(Layer.provide(NodeHttpClient.layerUndici))

program.pipe(
Effect.provide([OpenAi, DadJokeToolHandlers]),
Effect.tapErrorCause(Effect.logError),
NodeRuntime.runMain
)
```

5 changes: 5 additions & 0 deletions .changeset/sour-guests-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/ai-anthropic": minor
---

Refactor `@effect/ai-anthropic` to align with changes to `@effect/ai`
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@effect/build-utils": "^0.8.3",
"@effect/docgen": "https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@fd06738",
"@effect/eslint-plugin": "^0.3.2",
"@effect/language-service": "^0.1.0",
"@effect/language-service": "^0.9.1",
"@effect/vitest": "workspace:^",
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^3.1.0",
Expand Down
Loading