148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
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)))
|
|
})
|