EdgeAIG's picture
download
raw
17.6 kB
/**
* Stateful conversation sessions on top of a language model.
*
* A `Chat` keeps `Prompt` history in a `Ref` and reuses it for text generation,
* streaming, and structured output. Each generation call combines the current
* history with the caller's new prompt, invokes the active language model, and
* appends the response parts back into history. Constructors create fresh
* sessions, seed sessions from prompts, restore exported history, or connect a
* chat to persistence.
*
* @since 4.0.0
*/
import * as Channel from "../../Channel.js";
import * as Chunk from "../../Chunk.js";
import * as Context from "../../Context.js";
import * as Duration from "../../Duration.js";
import * as Effect from "../../Effect.js";
import * as Layer from "../../Layer.js";
import * as Option from "../../Option.js";
import * as Predicate from "../../Predicate.js";
import * as Ref from "../../Ref.js";
import * as Schema from "../../Schema.js";
import * as Semaphore from "../../Semaphore.js";
import * as Stream from "../../Stream.js";
import { BackingPersistence } from "../persistence/Persistence.js";
import * as AiError from "./AiError.js";
import * as IdGenerator from "./IdGenerator.js";
import * as LanguageModel from "./LanguageModel.js";
import * as Prompt from "./Prompt.js";
/**
* Service tag for stateful AI conversation sessions.
*
* **When to use**
*
* Use to access or provide conversational AI sessions through the Effect
* context.
*
* **Details**
*
* This tag provides access to chat functionality throughout your application,
* enabling persistent conversational AI interactions with full context
* management.
*
* **Example** (Using the Chat service)
*
* ```ts
* import { Effect } from "effect"
* import { Chat } from "effect/unstable/ai"
*
* const program = Effect.gen(function*() {
* const chat = yield* Chat.empty
* const response = yield* chat.generateText({
* prompt: "Explain quantum computing in simple terms"
* })
* return response.content
* })
* ```
*
* @category services
* @since 4.0.0
*/
export class Chat extends /*#__PURE__*/Context.Service()("effect/ai/Chat") {}
const decodeHistory = /*#__PURE__*/Schema.decodeUnknownEffect(Prompt.Prompt);
const encodeHistory = /*#__PURE__*/Schema.encodeUnknownEffect(Prompt.Prompt);
const decodeHistoryJson = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.fromJsonString(Prompt.Prompt));
const encodeHistoryJson = /*#__PURE__*/Schema.encodeUnknownEffect(/*#__PURE__*/Schema.fromJsonString(Prompt.Prompt));
// =============================================================================
// Constructors
// =============================================================================
const makeUnsafe = history => {
const semaphore = Semaphore.makeUnsafe(1);
return Chat.of({
history,
export: Ref.get(history).pipe(Effect.flatMap(encodeHistory), Effect.catchTag("SchemaError", error => Effect.fail(AiError.make({
module: "Chat",
method: "export",
reason: AiError.InvalidOutputError.fromSchemaError(error)
}))), Effect.withSpan("Chat.export")),
exportJson: Ref.get(history).pipe(Effect.flatMap(encodeHistoryJson), Effect.catchTag("SchemaError", error => Effect.fail(AiError.make({
module: "Chat",
method: "exportJson",
reason: AiError.InvalidOutputError.fromSchemaError(error)
}))), Effect.withSpan("Chat.exportJson")),
generateText: Effect.fnUntraced(function* (options) {
const newPrompt = Prompt.make(options.prompt);
const oldPrompt = yield* Ref.get(history);
const prompt = Prompt.concat(oldPrompt, newPrompt);
const response = yield* LanguageModel.generateText({
...options,
prompt
});
const newHistory = Prompt.concat(prompt, Prompt.fromResponseParts(response.content));
yield* Ref.set(history, newHistory);
return response;
}, semaphore.withPermits(1), effect => Effect.withSpan(effect, "Chat.generateText", {
captureStackTrace: false
})),
streamText: Effect.fnUntraced(function* (options) {
let parts = Chunk.empty();
return Stream.fromChannel(Channel.acquireUseRelease(semaphore.take(1).pipe(Effect.flatMap(() => Ref.get(history)), Effect.map(history => Prompt.concat(history, Prompt.make(options.prompt)))), prompt => LanguageModel.streamText({
...options,
prompt
}).pipe(Stream.mapArray(chunk => {
parts = Chunk.appendAll(parts, Chunk.fromArrayUnsafe(chunk));
return chunk;
}), Stream.toChannel), prompt => Effect.andThen(Ref.set(history, Prompt.concat(prompt, Prompt.fromResponseParts(Array.from(parts)))), semaphore.release(1)))).pipe(Stream.withSpan("Chat.streamText", {
captureStackTrace: false
}));
}, Stream.unwrap),
generateObject: Effect.fnUntraced(function* (options) {
const newPrompt = Prompt.make(options.prompt);
const oldPrompt = yield* Ref.get(history);
const prompt = Prompt.concat(oldPrompt, newPrompt);
const response = yield* LanguageModel.generateObject({
...options,
prompt
});
const newHistory = Prompt.concat(prompt, Prompt.fromResponseParts(response.content));
yield* Ref.set(history, newHistory);
return response;
}, semaphore.withPermits(1), (effect, options) => Effect.withSpan(effect, "Chat.generateObject", {
attributes: {
objectName: LanguageModel.getObjectName(options.objectName, options.schema)
},
captureStackTrace: false
}))
});
};
/**
* Creates a new Chat service with empty conversation history.
*
* **When to use**
*
* Use when you need to start a fresh chat session without initial context or
* system prompts.
*
* **Example** (Creating an empty chat)
*
* ```ts
* import { Effect } from "effect"
* import { Chat } from "effect/unstable/ai"
*
* const freshChat = Effect.gen(function*() {
* const chat = yield* Chat.empty
*
* const response = yield* chat.generateText({
* prompt: "Hello! Can you introduce yourself?"
* })
*
* console.log(response.content)
*
* return chat
* })
* ```
*
* @category constructors
* @since 4.0.0
*/
export const empty = /*#__PURE__*/Effect.sync(() => makeUnsafe(Ref.makeUnsafe(Prompt.empty)));
/**
* Creates a new Chat service from an initial prompt.
*
* **Details**
*
* This is the primary constructor for creating chat instances. It initializes
* a new conversation with the provided prompt as the starting context.
*
* **Example** (Creating a chat from a system prompt)
*
* ```ts
* import { Effect } from "effect"
* import { Chat } from "effect/unstable/ai"
*
* const chatWithSystemPrompt = Effect.gen(function*() {
* const chat = yield* Chat.fromPrompt([{
* role: "system",
* content: "You are a helpful assistant specialized in mathematics."
* }])
*
* const response = yield* chat.generateText({
* prompt: "What is 2+2?"
* })
*
* return response.content
* })
* ```
*
* **Example** (Restoring chat history from a prompt)
*
* ```ts
* import { Effect } from "effect"
* import { Chat } from "effect/unstable/ai"
*
* // Initialize with conversation history
* const existingChat = Effect.gen(function*() {
* const chat = yield* Chat.fromPrompt([
* {
* role: "user",
* content: [{ type: "text", text: "What's the weather like?" }]
* },
* {
* role: "assistant",
* content: [{ type: "text", text: "I don't have access to weather data." }]
* },
* {
* role: "user",
* content: [{ type: "text", text: "Can you help me with coding?" }]
* }
* ])
*
* const response = yield* chat.generateText({
* prompt: "I need help with TypeScript"
* })
*
* return response
* })
* ```
*
* @category constructors
* @since 4.0.0
*/
export const fromPrompt = prompt => Effect.sync(() => makeUnsafe(Ref.makeUnsafe(Prompt.make(prompt))));
/**
* Creates a Chat service from previously exported chat data.
*
* **Details**
*
* Restores a chat session from structured data that was previously exported
* using the `export` method. Useful for persisting and restoring conversation
* state.
*
* **Example** (Restoring chat data)
*
* ```ts
* import { Effect, Ref } from "effect"
* import { Chat } from "effect/unstable/ai"
*
* const restoreChat = Effect.gen(function*() {
* const originalChat = yield* Chat.fromPrompt([
* {
* role: "user",
* content: "Which library are we using?"
* },
* {
* role: "assistant",
* content: "The project uses Effect."
* }
* ])
*
* const exported = yield* originalChat.export
* const restoredChat = yield* Chat.fromExport(exported)
* const restoredHistory = yield* Ref.get(restoredChat.history)
*
* console.log(restoredHistory.content.map((message) => message.role))
* // ["user", "assistant"]
*
* const restoredResponse = restoredHistory.content[1]
* if (restoredResponse?.role === "assistant") {
* const restoredText = restoredResponse.content[0]
* if (restoredText?.type === "text") {
* console.log(restoredText.text)
* // "The project uses Effect."
* }
* }
* })
* ```
*
* @category constructors
* @since 4.0.0
*/
export const fromExport = data => Effect.flatMap(decodeHistory(data), fromPrompt);
/**
* Creates a Chat service from previously exported JSON chat data.
*
* **Details**
*
* Restores a chat session from JSON string that was previously exported
* using the `exportJson` method. This is the most convenient way to
* persist and restore chat sessions to/from storage systems.
*
* **Example** (Restoring chat history from JSON)
*
* ```ts
* import { Effect } from "effect"
* import { Chat } from "effect/unstable/ai"
*
* const restoreFromJson = Effect.gen(function*() {
* // Load JSON from localStorage or file system
* const jsonData = localStorage.getItem("my-chat-backup")
* if (!jsonData) return yield* Chat.empty
*
* const restoredChat = yield* Chat.fromJson(jsonData)
*
* // Chat history is now restored
* const response = yield* restoredChat.generateText({
* prompt: "What were we talking about?"
* })
*
* return response
* }).pipe(
* Effect.catchTag("SchemaError", (error) => {
* console.log("Invalid JSON format:", error.message)
* return Chat.empty // Fallback to empty chat
* })
* )
* ```
*
* @category constructors
* @since 4.0.0
*/
export const fromJson = data => Effect.flatMap(decodeHistoryJson(data), fromPrompt);
// =============================================================================
// Chat Persistence
// =============================================================================
/**
* Represents an error that occurs when attempting to retrieve a persisted `Chat` that
* does not exist in the backing persistence store.
*
* **When to use**
*
* Use to represent a missing persisted conversation when lookup by id cannot
* find stored history.
*
* @category errors
* @since 4.0.0
*/
export class ChatNotFoundError extends /*#__PURE__*/Schema.ErrorClass("effect/ai/Chat/ChatNotFoundError")({
_tag: /*#__PURE__*/Schema.tag("ChatNotFoundError"),
chatId: Schema.String
}) {}
/**
* Service tag for persistence-backed AI conversation storage.
*
* **When to use**
*
* Use to provide the storage operations needed by persisted conversation
* sessions.
*
* @category services
* @since 4.0.0
*/
// @effect-diagnostics effect/leakingRequirements:off
export class Persistence extends /*#__PURE__*/Context.Service()("effect/ai/Chat/Persisted") {}
/**
* Creates a new chat persistence service.
*
* **When to use**
*
* Use when you need programmatic persisted chat creation and retrieval backed
* by the current `BackingPersistence`.
*
* **Details**
*
* The provided store identifier will be used to indicate which "store" the
* backing persistence should load chats from.
*
* @see {@link layerPersisted} for the `Layer`-based constructor
*
* @category constructors
* @since 4.0.0
*/
export const makePersisted = /*#__PURE__*/Effect.fnUntraced(function* (options) {
const persistence = yield* BackingPersistence;
const store = yield* persistence.make(options.storeId);
const toPersisted = Effect.fnUntraced(function* (chatId, chat, ttl) {
const idGenerator = yield* Effect.serviceOption(IdGenerator.IdGenerator).pipe(Effect.map(Option.getOrElse(() => IdGenerator.defaultIdGenerator)));
const saveChat = Effect.fnUntraced(function* (prevHistory) {
// Get the current chat history
const history = yield* Ref.get(chat.history);
// Get the most recent message stored in the previous chat history
const lastMessage = prevHistory.content[prevHistory.content.length - 1];
// Determine the correct message identifier to use:
let messageId = undefined;
// If the most recent message in the chat history is an assistant message,
// use the message identifer stored in that message
if (Predicate.isNotUndefined(lastMessage) && lastMessage.role === "assistant") {
messageId = lastMessage.options[Persistence.key]?.messageId;
}
// If the chat history is empty or a message identifier did not exist on
// the most recent message in the chat history, generate a new identifier
if (Predicate.isUndefined(messageId)) {
messageId = yield* idGenerator.generateId();
}
// Mutate the new messages to add the generated message identifier
for (let i = prevHistory.content.length; i < history.content.length; i++) {
const message = history.content[i];
message.options[Persistence.key] = {
messageId
};
}
// Save the mutated history back to the ref
yield* Ref.set(chat.history, history);
// Export the chat history
const exported = yield* Effect.orDie(chat.export);
const timeToLive = Predicate.isNotUndefined(ttl) ? Option.getOrUndefined(Duration.fromInput(ttl)) : undefined;
// Save the chat to the backing store
yield* store.set(chatId, exported, timeToLive);
});
const persisted = {
...chat,
id: chatId,
save: Effect.flatMap(Ref.get(chat.history), saveChat),
generateText: Effect.fnUntraced(function* (options) {
const history = yield* Ref.get(chat.history);
return yield* chat.generateText(options).pipe(Effect.ensuring(Effect.orDie(saveChat(history))));
}),
generateObject: Effect.fnUntraced(function* (options) {
const history = yield* Ref.get(chat.history);
return yield* chat.generateObject(options).pipe(Effect.ensuring(Effect.orDie(saveChat(history))));
}),
streamText: Effect.fnUntraced(function* (options) {
const history = yield* Ref.get(chat.history);
const stream = chat.streamText(options).pipe(Stream.ensuring(Effect.orDie(saveChat(history))));
return stream;
}, Stream.unwrap)
};
return persisted;
});
const createChat = Effect.fnUntraced(function* (chatId, ttl) {
// Create an empty chat
const chat = yield* empty;
// Export the chat history
const history = yield* Effect.orDie(chat.export);
// Save the history for the newly created chat
const timeToLive = Predicate.isNotUndefined(ttl) ? Option.getOrUndefined(Duration.fromInput(ttl)) : undefined;
yield* store.set(chatId, history, timeToLive);
// Convert the chat to a persisted chat
return yield* toPersisted(chatId, chat, ttl);
});
const getChat = Effect.fnUntraced(function* (chatId, ttl) {
// Create an empty chat
const chat = yield* empty;
// Attempt to retrieve the previous history from the store
const previousHistory = yield* store.get(chatId);
// If the previous history was not found, raise an error
if (Predicate.isUndefined(previousHistory)) {
return yield* new ChatNotFoundError({
chatId
});
}
// Decode the encoded previous history
const history = yield* decodeHistory(previousHistory);
// Hydrate the chat history
yield* Ref.set(chat.history, history);
// Convert the chat to a persisted chat
return yield* toPersisted(chatId, chat, ttl);
}, Effect.catchTag("SchemaError", Effect.die));
const get = Effect.fnUntraced(function* (chatId, options) {
return yield* getChat(chatId, options?.timeToLive);
}, effect => Effect.withSpan(effect, "PersistedChat.get", {
captureStackTrace: false
}));
const getOrCreate = Effect.fnUntraced(function* (chatId, options) {
return yield* getChat(chatId, options?.timeToLive).pipe(Effect.catchTag("ChatNotFoundError", () => createChat(chatId, options?.timeToLive)));
}, effect => Effect.withSpan(effect, "PersistedChat.getOrCreate", {
captureStackTrace: false
}));
return Persistence.of({
get,
getOrCreate
});
});
/**
* Creates a `Layer` for a new chat persistence service.
*
* **When to use**
*
* Use to provide `Chat.Persistence` from a configured `BackingPersistence` when
* your application needs persisted chat sessions backed by a named store.
*
* **Details**
*
* The provided store identifier will be used to indicate which "store" the
* backing persistence should load chats from.
*
* @see {@link makePersisted} for the effect constructor when building the service directly instead of providing it as a layer
*
* @category constructors
* @since 4.0.0
*/
export const layerPersisted = options => Layer.effect(Persistence)(makePersisted(options));
//# sourceMappingURL=Chat.js.map

Xet Storage Details

Size:
17.6 kB
·
Xet hash:
63ba5f0503a8432a6750a4e9bc1274bcdbec85e3ea005acd1e2565926ceb4120

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.