Skip to content

Commit 0552674

Browse files
IMax153KhraksMamtsovtim-smartvinassefranchethewilkybarkid
committed
Refactor AiModel and remove AiPlan (#4918)
Co-authored-by: Maxim Khramtsov <khraks.mamtsov@gmail.com> Co-authored-by: Tim <hello@timsmart.co> Co-authored-by: Vincent François <vincent.francois@inato.com> Co-authored-by: Chris Wilkinson <chris@prereview.org> Co-authored-by: Tylor Steinberger <tlsteinberger167@gmail.com> Co-authored-by: Jason Rudder <indxxxd@gmail.com> Co-authored-by: Sebastian Lorenz <fubhy@fubhy.com>
1 parent 5c806af commit 0552674

File tree

8 files changed

+286
-578
lines changed

8 files changed

+286
-578
lines changed

.changeset/silver-terms-doubt.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
"@effect/ai-anthropic": minor
3+
"@effect/ai-openai": minor
4+
"@effect/ai": minor
5+
---
6+
7+
Make `AiModel` a plain `Layer` and remove `AiPlan` in favor of `ExecutionPlan`
8+
9+
This release substantially simplifies and improves the ergonomics of using `AiModel` for various providers. With these changes, an `AiModel` now returns a plain `Layer` which can be used to provide services to a program that interacts with large language models.
10+
11+
**Before**
12+
13+
```ts
14+
import { AiLanguageModel } from "@effect/ai"
15+
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
16+
import { NodeHttpClient } from "@effect/platform-node"
17+
import { Config, Console, Effect, Layer } from "effect"
18+
19+
// Produces an `AiModel<AiLanguageModel, OpenAiClient>`
20+
const Gpt4o = OpenAiLanguageModel.model("gpt-4o")
21+
22+
// Generate a dad joke
23+
const getDadJoke = AiLanguageModel.generateText({
24+
prompt: "Tell me a dad joke"
25+
})
26+
27+
const program = Effect.gen(function*() {
28+
// Build the `AiModel` into a `Provider`
29+
const gpt4o = yield* Gpt4o
30+
// Use the built `AiModel` to run the program
31+
const response = yield* gpt4o.use(getDadJoke)
32+
// Log the response
33+
yield* Console.log(response.text)
34+
})
35+
36+
const OpenAi = OpenAiClient.layerConfig({
37+
apiKey: Config.redacted("OPENAI_API_KEY")
38+
}).pipe(Layer.provide(NodeHttpClient.layerUndici))
39+
40+
program.pipe(
41+
Effect.provide(OpenAi),
42+
Effect.runPromise
43+
)
44+
```
45+
46+
**After**
47+
48+
```ts
49+
import { AiLanguageModel } from "@effect/ai"
50+
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
51+
import { NodeHttpClient } from "@effect/platform-node"
52+
import { Config, Console, Effect, Layer } from "effect"
53+
54+
// Produces a `Layer<AiLanguageModel, never, OpenAiClient>`
55+
const Gpt4o = OpenAiLanguageModel.model("gpt-4o")
56+
57+
const program = Effect.gen(function*() {
58+
// Generate a dad joke
59+
const response = yield* AiLanguageModel.generateText({
60+
prompt: "Tell me a dad joke"
61+
})
62+
// Log the response
63+
yield* Console.log(response.text)
64+
).pipe(Effect.provide(Gpt4o))
65+
66+
const OpenAi = OpenAiClient.layerConfig({
67+
apiKey: Config.redacted("OPENAI_API_KEY")
68+
}).pipe(Layer.provide(NodeHttpClient.layerUndici))
69+
70+
program.pipe(
71+
Effect.provide(OpenAi),
72+
Effect.runPromise
73+
)
74+
```
75+
76+
In addition, `AiModel` can be `yield*`'ed to produce a layer with no requirements.
77+
78+
This shifts the requirements of building the layer into the calling effect, which is particularly useful for creating AI-powered services.
79+
80+
```ts
81+
import { AiLanguageModel } from "@effect/ai"
82+
import { OpenAiLanguageModel } from "@effect/ai-openai"
83+
import { Effect } from "effect"
84+
85+
class DadJokes extends Effect.Service<DadJokes>()("DadJokes", {
86+
effect: Effect.gen(function*() {
87+
// Yielding the model will return a layer with no requirements
88+
//
89+
// ┌─── Layer<AiLanguageModel>
90+
//
91+
const model = yield* OpenAiLanguageModel.model("gpt-4o")
92+
93+
const getDadJoke = AiLanguageModel.generateText({
94+
prompt: "Generate a dad joke"
95+
}).pipe(Effect.provide(model))
96+
97+
return { getDadJoke } as const
98+
})
99+
}) {}
100+
101+
// The requirements are lifted into the service constructor
102+
//
103+
// ┌─── Layer<DadJokes, never, OpenAiClient>
104+
//
105+
DadJokes.Default
106+
```

packages/ai/ai/src/AiModel.ts

Lines changed: 21 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
*/
44
import type * as Context from "effect/Context"
55
import * as Effect from "effect/Effect"
6-
import * as GlobalValue from "effect/GlobalValue"
7-
import * as Option from "effect/Option"
8-
import * as Predicate from "effect/Predicate"
9-
import type * as Scope from "effect/Scope"
10-
import type * as AiPlan from "./AiPlan.js"
11-
import * as InternalAiPlan from "./internal/aiPlan.js"
6+
import { CommitPrototype } from "effect/Effectable"
7+
import { identity } from "effect/Function"
8+
import * as Layer from "effect/Layer"
129

1310
/**
1411
* @since 1.0.0
@@ -22,88 +19,35 @@ export const TypeId: unique symbol = Symbol.for("@effect/ai/AiModel")
2219
*/
2320
export type TypeId = typeof TypeId
2421

25-
/**
26-
* @since 1.0.0
27-
* @category type ids
28-
*/
29-
export const PlanTypeId: unique symbol = Symbol.for("@effect/ai/Plan")
30-
31-
/**
32-
* @since 1.0.0
33-
* @category type ids
34-
*/
35-
export type PlanTypeId = typeof TypeId
36-
3722
/**
3823
* @since 1.0.0
3924
* @category models
4025
*/
41-
export interface AiModel<in out Provides, in out Requires> extends AiPlan.AiPlan<unknown, Provides, Requires> {
26+
export interface AiModel<in out Provides, in out Requires>
27+
extends Layer.Layer<Provides, never, Requires>, Effect.Effect<Layer.Layer<Provides>, never, Requires>
28+
{
4229
readonly [TypeId]: TypeId
43-
readonly buildContext: ContextBuilder<Provides, Requires>
4430
}
4531

46-
/**
47-
* @since 1.0.0
48-
* @category AiModel
49-
*/
50-
export type ContextBuilder<Provides, Requires> = Effect.Effect<
51-
Context.Context<Provides>,
52-
never,
53-
Requires | Scope.Scope
54-
>
55-
5632
const AiModelProto = {
57-
...InternalAiPlan.PlanPrototype,
58-
[TypeId]: TypeId
33+
...CommitPrototype,
34+
[TypeId]: TypeId,
35+
[Layer.LayerTypeId]: {
36+
_ROut: identity,
37+
_E: identity,
38+
_RIn: identity
39+
},
40+
commit(this: AiModel<any, any>) {
41+
return Effect.contextWith((context: Context.Context<never>) => {
42+
return Layer.provide(this, Layer.succeedContext(context))
43+
})
44+
}
5945
}
6046

61-
const contextCache = GlobalValue.globalValue(
62-
"@effect/ai/AiModel/CachedContexts",
63-
() => new Map<string, any>()
64-
)
65-
6647
/**
6748
* @since 1.0.0
6849
* @category constructors
6950
*/
70-
export const make = <Cached, PerRequest, CachedRequires, PerRequestRequires>(options: {
71-
/**
72-
* A unique key used to cache the `Context` built from the `cachedContext`
73-
* effect.
74-
*/
75-
readonly cacheKey: string
76-
/**
77-
* An effect used to build a `Context` that will be cached after creation
78-
* and used for all provider requests.
79-
*/
80-
readonly cachedContext: Effect.Effect<
81-
Context.Context<Cached>,
82-
never,
83-
CachedRequires | Scope.Scope
84-
>
85-
/**
86-
* A method that can be used to update the `Context` on a per-request basis
87-
* for all provider requests.
88-
*/
89-
readonly updateRequestContext: (context: Context.Context<Cached>) => Effect.Effect<
90-
Context.Context<PerRequest>,
91-
never,
92-
PerRequestRequires
93-
>
94-
}): AiModel<Cached | PerRequest, CachedRequires | PerRequestRequires> => {
95-
const self = Object.create(AiModelProto)
96-
self.buildContext = Effect.gen(function*() {
97-
let context = contextCache.get(options.cacheKey)
98-
if (Predicate.isUndefined(context)) {
99-
context = yield* options.cachedContext
100-
}
101-
return yield* options.updateRequestContext(context)
102-
})
103-
self.steps = [{
104-
model: self,
105-
check: Option.none(),
106-
schedule: Option.none()
107-
}]
108-
return self
109-
}
51+
export const make = <Provides, Requires>(
52+
layer: Layer.Layer<Provides, never, Requires>
53+
): AiModel<Provides, Requires> => Object.assign(Object.create(AiModelProto), layer)

0 commit comments

Comments
 (0)