import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Option, Predicate, Ref, Schema } from "effect" import { TestClock } from "effect/testing" import { Chat, IdGenerator, Prompt } from "effect/unstable/ai" import { Persistence } from "effect/unstable/persistence" import * as TestUtils from "./utils.ts" const withConstantIdGenerator = (id: string) => Effect.provideService(IdGenerator.IdGenerator, { generateId: () => Effect.succeed(id) }) const PersistenceLayer = Layer.provideMerge( Chat.layerPersisted({ storeId: "chat" }), Persistence.layerMemory ) describe("Chat", () => { it("should persist chat history to the backing persistence store", () => Effect.gen(function*() { const storeId = "chat" const chatId = "1" const backing = yield* Persistence.BackingPersistence const persistence = yield* Chat.Persistence const store = yield* backing.make(storeId) const chat = yield* persistence.getOrCreate(chatId) yield* chat.generateText({ prompt: "test user message" }).pipe( TestUtils.withLanguageModel({ generateText: [{ type: "text", text: "test assistant message" }] }) ) const chatHistory = yield* Ref.get(chat.history) const encodedHistory = yield* store.get(chatId) const storedHistory = Predicate.isNotUndefined(encodedHistory) ? yield* Schema.decodeUnknownEffect(Prompt.Prompt)(encodedHistory) : undefined const options = { [Chat.Persistence.key]: { messageId: "msg_abc123" } } const expectedHistory = Prompt.make([ { role: "user", content: [{ type: "text", text: "test user message" }], options }, { role: "assistant", content: [{ type: "text", text: "test assistant message" }], options } ]) assert.deepStrictEqual(chatHistory, expectedHistory) assert.deepStrictEqual(chatHistory, storedHistory) }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) it("should respect the specified time to live", () => Effect.gen(function*() { const storeId = "chat" const chatId = "1" const backing = yield* Persistence.BackingPersistence const persistence = yield* Chat.Persistence const store = yield* backing.make(storeId) const chat = yield* persistence.getOrCreate(chatId, { timeToLive: "30 days" }) yield* chat.generateText({ prompt: "test user message" }).pipe( TestUtils.withLanguageModel({ generateText: [{ type: "text", text: "test assistant message" }] }) ) const encodedHistory = yield* store.get(chatId) const storedHistory = Predicate.isNotUndefined(encodedHistory) ? yield* Schema.decodeUnknownEffect(Prompt.Prompt)(encodedHistory) : undefined const options = { [Chat.Persistence.key]: { messageId: "msg_abc123" } } const expectedHistory = Prompt.make([ { role: "user", content: [{ type: "text", text: "test user message" }], options }, { role: "assistant", content: [{ type: "text", text: "test assistant message" }], options } ]) assert.deepStrictEqual(storedHistory, expectedHistory) // Simulate chat expiration yield* TestClock.adjust("30 days") const afterExpiration = yield* store.get(chatId) assert.deepStrictEqual(afterExpiration, Option.none()) }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) it("should prefer the message identifier of the most recent assistant message", () => Effect.gen(function*() { const storeId = "chat" const chatId = "2" const backing = yield* Persistence.BackingPersistence const persistence = yield* Chat.Persistence const store = yield* backing.make(storeId) const chat = yield* persistence.getOrCreate(chatId) const options = { [Chat.Persistence.key]: { messageId: "msg_123abc" } } const history = Prompt.make([ { role: "user", content: "first user message", options }, { role: "assistant", content: "first assistant message", options } ]) yield* Ref.set(chat.history, history) yield* chat.save yield* chat.generateText({ prompt: "second user message" }).pipe( TestUtils.withLanguageModel({ generateText: [{ type: "text", text: "second assistant message" }] }) ) const encodedHistory = yield* store.get(chatId) const storedHistory = Predicate.isNotUndefined(encodedHistory) ? yield* Schema.decodeUnknownEffect(Prompt.Prompt)(encodedHistory) : undefined const expectedHistory = Prompt.concat(history, [ { role: "user", content: "second user message", options }, { role: "assistant", content: "second assistant message", options } ]) assert.deepStrictEqual(storedHistory, expectedHistory) }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) it("should raise an error when retrieving a chat that does not exist", () => Effect.gen(function*() { const persistence = yield* Chat.Persistence const result = yield* Effect.flip(persistence.get("chat-321")) assert.instanceOf(result, Chat.ChatNotFoundError) assert.strictEqual(result.chatId, "chat-321") }).pipe(Effect.provide(PersistenceLayer))) })