1import * as Cloudflare from "@/Cloudflare";
2import { Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Ref from "effect/Ref";
5import { Chat } from "effect/ai";
6import { Gateway } from "./Gateway.ts";
7
8// One DurableObject instance == one conversation thread. The thread's
9// history lives in the DO's `state.storage` via
10// `DurableObjectChatPersistence`, so it survives hibernation and DO
11// eviction. The Worker routes `/chat?id=<thread>` to the matching DO
12// instance by name.
13export default class ChatBackend extends Cloudflare.DurableObject<ChatBackend>()(
14 "ChatBackend",
15 Effect.gen(function* () {
16 // Init phase: bind the AI Gateway and build the LanguageModel layer.
17 const aiGateway = yield* Cloudflare.AI.QueryGateway(Gateway);
18 const languageModel = aiGateway.model({
19 model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
20 parameters: { temperature: 0.2, maxTokens: 512 },
21 });
22
23 return Effect.gen(function* () {
24 // Per-instance: a chat persistence service backed by this DO's
25 // storage. `Chat.makePersisted` needs `BackingPersistence`, which
26 // `DurableObjectChatPersistence` provides from `DurableObjectState`
27 // (in scope here).
28 const persistence = yield* Chat.makePersisted({
29 storeId: "alchemy.chat",
30 }).pipe(Effect.provide(Cloudflare.AI.DurableObjectChatPersistence));
31
32 return {
33 send: (threadId: string, prompt: string) =>
34 Effect.gen(function* () {
35 const chat = yield* persistence.getOrCreate(threadId);
36 const response = yield* chat.generateText({ prompt });
37 const history = yield* Ref.get(chat.history);
38 return {
39 text: response.text,
40 turns: history.content.length,
41 };
42 }).pipe(Effect.provide(languageModel), Effect.orDie),
43 };
44 });
45 }).pipe(Effect.provide(Layer.mergeAll(Cloudflare.AI.QueryGatewayBinding))),
46) {}