Skip to content

Commit

Permalink
add changeset
Browse files Browse the repository at this point in the history
  • Loading branch information
IMax153 committed Jan 30, 2025
1 parent f27c3e6 commit 4d71f07
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 41 deletions.
41 changes: 41 additions & 0 deletions .changeset/wise-jeans-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
"@effect/ai-openai": patch
"@effect/ai": patch
---

Support creation of embeddings from the AI integration packages.

For example, the following program will create an OpenAI `Embeddings` service
that will aggregate all embedding requests received within a `500` millisecond
window into a single batch.

```ts
import { Embeddings } from "@effect/ai"
import { OpenAiClient, OpenAiEmbeddings } from "@effect/ai-openai"
import { NodeHttpClient } from "@effect/platform-node"
import { Config, Effect, Layer } from "effect"

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

// Create an embeddings service for the `text-embedding-3-large` model
const TextEmbeddingsLarge = OpenAiEmbeddings.layerDataLoader({
model: "text-embedding-3-large",
window: "500 millis",
maxBatchSize: 2048
}).pipe(Layer.provide(OpenAi))

// Use the generic `Embeddings` service interface in your program
const program = Effect.gen(function*() {
const embeddings = yield* Embeddings.Embeddings
const result = yield* embeddings.embed("The input to embed")
})

// Provide the specific implementation to use
program.pipe(
Effect.provide(TextEmbeddingsLarge),
Effect.runPromise
)
```
2 changes: 1 addition & 1 deletion packages/ai/ai/src/Completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export declare namespace Completions {
* @since 1.0.0
* @category models
*/
interface StructuredSchema<A, I, R> extends Schema.Schema<A, I, R> {
export interface StructuredSchema<A, I, R> extends Schema.Schema<A, I, R> {
readonly _tag?: string
readonly identifier: string
}
Expand Down
133 changes: 93 additions & 40 deletions packages/ai/openai/src/OpenAiEmbeddings.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/**
* @since 1.0.0
*/
import { AiError } from "@effect/ai/AiError"
import * as Embeddings from "@effect/ai/Embeddings"
import * as Context from "effect/Context"
import type * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import type { Simplify } from "effect/Types"
import type * as Generated from "./Generated.js"
import { OpenAiClient } from "./OpenAiClient.js"
import { AiError } from "@effect/ai/AiError"

/**
* @since 1.0.0
Expand All @@ -34,50 +35,87 @@ export class OpenAiEmbeddingsConfig extends Context.Tag("@effect/ai-openai/OpenA
)
}

const makeRequest = (
client: OpenAiClient.Service,
input: ReadonlyArray<string>,
parentConfig: typeof OpenAiEmbeddingsConfig.Service | undefined,
options: {
readonly model: string
readonly maxBatchSize?: number
readonly cache?: {
readonly capacity: number
readonly timeToLive: Duration.DurationInput
}
}
) =>
Effect.context<never>().pipe(
Effect.flatMap((context) => {
const localConfig = context.unsafeMap.get(OpenAiEmbeddingsConfig.key)
return client.client.createEmbedding({
input,
model: options.model,
...parentConfig,
...localConfig
})
}),
Effect.map((response) =>
response.data.map(({ embedding, index }) => ({
embeddings: embedding as Array<number>,
index
}))
),
Effect.mapError((cause) => {
const common = {
module: "OpenAiEmbeddings",
method: "embed",
cause
}
if (cause._tag === "ParseError") {
return new AiError({
description: "Malformed input detected in request",
...common
})
}
return new AiError({
description: "An error occurred with the OpenAI API",
...common
})
})
)

const make = (options: {
readonly model: string
readonly maxBatchSize?: number
readonly cache?: {
readonly capacity: number
readonly timeToLive: Duration.DurationInput
}
}) =>
Effect.gen(function*() {
const client = yield* OpenAiClient
const config = yield* OpenAiEmbeddingsConfig.getOrUndefined

function makeRequest(input: ReadonlyArray<string>) {
return Effect.flatMap(Effect.context<never>(), (context) =>
client.client.createEmbedding({
input,
model: options.model,
...config,
...context.unsafeMap.get(OpenAiEmbeddingsConfig.key)
}))
}

const parentConfig = yield* OpenAiEmbeddingsConfig.getOrUndefined
return yield* Embeddings.make({
cache: options.cache,
maxBatchSize: options.maxBatchSize ?? 2048,
embedMany(input) {
return makeRequest(client, input, parentConfig, options)
}
})
})

const makeDataLoader = (options: {
readonly model: string
readonly window: Duration.DurationInput
readonly maxBatchSize?: number
}) =>
Effect.gen(function*() {
const client = yield* OpenAiClient
const parentConfig = yield* OpenAiEmbeddingsConfig.getOrUndefined
return yield* Embeddings.makeDataLoader({
window: options.window,
maxBatchSize: options.maxBatchSize ?? 2048,
embedMany(input) {
return makeRequest(input).pipe(
Effect.map((response) =>
response.data.map(({ embedding, index }) => ({
embeddings: embedding as Array<number>,
index
}))
),
Effect.mapError((cause) => {
const common = {
module: "OpenAiEmbeddings",
method: "embed",
cause
}
if (cause._tag === "ParseError") {
return new AiError({
description: "Malformed input detected in request",
...common
})
}
return new AiError({
description: "An error occurred with the OpenAI API",
...common
})
})
)
return makeRequest(client, input, parentConfig, options)
}
})
})
Expand All @@ -88,5 +126,20 @@ const make = (options: {
*/
export const layer = (options: {
readonly model: string
}): Layer.Layer<Embeddings.Embeddings, never, OpenAiClient> =>
Layer.effect(Embeddings.Embeddings, make(options))
readonly maxBatchSize?: number
readonly cache?: {
readonly capacity: number
readonly timeToLive: Duration.DurationInput
}
}): Layer.Layer<Embeddings.Embeddings, never, OpenAiClient> => Layer.effect(Embeddings.Embeddings, make(options))

/**
* @since 1.0.0
* @category layers
*/
export const layerDataLoader = (options: {
readonly model: string
readonly window: Duration.DurationInput
readonly maxBatchSize?: number
}): Layer.Layer<Embeddings.Embeddings, never, OpenAiClient> =>
Layer.scoped(Embeddings.Embeddings, makeDataLoader(options))

0 comments on commit 4d71f07

Please sign in to comment.