@langgraph-js/sdk
Version:
The UI SDK for LangGraph - seamlessly integrate your AI agents with frontend interfaces
431 lines (430 loc) • 15.9 kB
JavaScript
import { atom } from "nanostores";
import { debounce } from "ts-debounce";
import { ToolRenderData } from "../tool/ToolUI.js";
import { createLangGraphServerClient } from "../client/LanggraphServer.js";
import { useArtifacts } from "../artifacts/index.js";
import { History } from "../History.js";
// ============ 工具函数 ============
export const formatTime = (date) => date.toLocaleTimeString();
export const formatFullTime = (date) => {
const pad = (n) => n.toString().padStart(2, "0");
const yyyy = date.getFullYear();
const mm = pad(date.getMonth() + 1);
const dd = pad(date.getDate());
const hh = pad(date.getHours());
const mi = pad(date.getMinutes());
const ss = pad(date.getSeconds());
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
};
export const formatTokens = (tokens) => tokens.toLocaleString("en");
export const getMessageContent = (content) => {
if (typeof content === "string")
return content;
if (Array.isArray(content)) {
return content
.map((item) => {
if (typeof item === "string")
return item;
if (item.type === "text")
return item.text;
if (item.type === "image_url")
return `[图片]`;
return JSON.stringify(item);
})
.join("");
}
return JSON.stringify(content);
};
export const getHistoryContent = (thread) => {
/** @ts-ignore */
const content = thread.title || thread.name || thread?.values?.messages?.[0]?.content;
if (content && Array.isArray(content)) {
return content.map((item) => (item.type === "text" ? item.text : undefined)).filter(Boolean);
}
return typeof content === "string" ? content : "";
};
// ============ Store 创建函数 ============
export const createChatStore = (initClientName, config, context = {}) => {
// ============ 状态原子 ============
// 会话管理
const history = atom(null);
const sessions = atom([]);
const client = atom(null);
const historyList = atom([]);
// UI 状态
const renderMessages = atom([]);
const userInput = atom("");
const inChatError = atom(null);
const currentAgent = atom(initClientName);
const currentChatId = atom(null);
const currentNodeName = atom("__start__");
const currentStatus = atom("idle");
// Interrupt 状态
const interruptData = atom(null);
const isInterrupted = atom(false);
// 工具和图表
const tools = atom([]);
const collapsedTools = atom([]);
const showHistory = atom(context.showHistory ?? false);
const showGraph = atom(context.showGraph ?? false);
const graphVisualize = atom(null);
// ============ 内部状态 ============
let cleanupCurrentClient = null;
// ============ 计算属性 ============
/** 基于 client.status 的 loading 状态 */
const loading = atom(false);
const updateLoadingFromClientStatus = () => {
const c = client.get();
if (c) {
// interrupted 状态也应该被视为 loading,因为用户需要处理中断
loading.set(c.status === "busy" || c.status === "interrupted");
}
};
// ============ UI 更新逻辑 ============
const updateUI = debounce((newClient) => {
if (client.get() !== newClient)
return;
const messages = newClient.renderMessage;
const lastMessage = messages[messages.length - 1];
currentNodeName.set(lastMessage?.node_name || lastMessage?.name || "__start__");
renderMessages.set(messages);
currentStatus.set(newClient.status);
}, 10);
// ============ 工具和图表辅助函数 ============
const refreshTools = async () => {
const c = client.get();
if (!c)
return;
c.tools.clearTools();
c.tools.bindTools(tools.get());
};
const refreshGraph = async () => {
if (showGraph.get()) {
graphVisualize.set((await client.get()?.graphVisualize()) || null);
}
};
// ============ 会话管理核心逻辑 ============
async function initClient() {
const historyManager = new History({
...config,
client: config.client ?? (await createLangGraphServerClient(config)),
});
await historyManager.init(currentAgent.get(), { fallbackToAvailableAssistants: context.fallbackToAvailableAssistants });
history.set(historyManager);
// 同步远程会话列表
// 根据配置决定初始化行为
if (context.autoRestoreLastSession) {
await refreshSessionList();
if (sessions.get().length > 0) {
const syncedSessions = sessions.get();
// 自动激活最近的历史会话
await activateSession(syncedSessions[0].sessionId);
}
}
else {
// 创建新会话
await createNewSession();
}
return historyManager;
}
async function refreshSessionList() {
const historyManager = history.get();
if (!historyManager)
return;
try {
const syncedSessions = await historyManager.syncFromRemote({ limit: 10 });
sessions.set(syncedSessions);
historyList.set(syncedSessions.filter((s) => s.thread).map((s) => s.thread));
}
catch (error) {
console.error("Failed to sync sessions:", error);
}
}
async function createNewSession() {
const historyManager = history.get();
if (!historyManager)
return;
try {
const session = await historyManager.createSession();
await refreshSessionList();
await activateSession(session.sessionId);
}
catch (error) {
console.error("Failed to create new session:", error);
inChatError.set(error.message);
}
}
// ============ 客户端事件监听器 ============
function setupClientListeners(newClient) {
const isActiveClient = () => client.get() === newClient;
const onStart = () => {
if (isActiveClient())
updateLoadingFromClientStatus();
};
const onThread = () => {
if (!isActiveClient())
return;
const thread = newClient.getCurrentThread();
currentChatId.set(thread?.thread_id || null);
currentNodeName.set("__start__");
const historyManager = history.get();
const activeSession = historyManager?.getActiveSession();
if (activeSession && thread) {
activeSession.thread = thread;
}
refreshSessionList();
};
const onDone = () => {
if (isActiveClient()) {
updateLoadingFromClientStatus();
updateUI(newClient);
}
};
const onError = (event) => {
if (isActiveClient()) {
updateLoadingFromClientStatus();
inChatError.set(event.data);
}
};
const onMessage = () => {
if (isActiveClient()) {
currentChatId.set(newClient.getCurrentThread()?.thread_id || null);
updateLoadingFromClientStatus();
updateUI(newClient);
}
};
const onValue = () => {
if (isActiveClient()) {
currentChatId.set(newClient.getCurrentThread()?.thread_id || null);
updateLoadingFromClientStatus();
updateUI(newClient);
}
};
const onInterrupted = (event) => {
if (!isActiveClient())
return;
const interruptInfo = newClient.interruptData;
if (interruptInfo) {
interruptData.set(interruptInfo);
isInterrupted.set(true);
updateLoadingFromClientStatus();
}
else {
interruptData.set(null);
isInterrupted.set(false);
}
updateUI(newClient);
client.set(client.get());
};
newClient.on("start", onStart);
newClient.on("thread", onThread);
newClient.on("done", onDone);
newClient.on("error", onError);
newClient.on("message", onMessage);
newClient.on("value", onValue);
newClient.on("interruptChange", onInterrupted);
return () => {
newClient.off("start", onStart);
newClient.off("thread", onThread);
newClient.off("done", onDone);
newClient.off("error", onError);
newClient.off("message", onMessage);
newClient.off("value", onValue);
newClient.off("interruptChange", onInterrupted);
};
}
// ============ 会话激活逻辑 ============
async function activateSession(sessionId, mustResetStream = false) {
const historyManager = history.get();
if (!historyManager)
return;
try {
if (cleanupCurrentClient) {
cleanupCurrentClient();
cleanupCurrentClient = null;
}
inChatError.set(null);
interruptData.set(null);
isInterrupted.set(false);
const session = await historyManager.activateSession(sessionId, mustResetStream);
const activeClient = session.client;
if (activeClient) {
cleanupCurrentClient = setupClientListeners(activeClient);
context.onInit?.(activeClient);
client.set(activeClient);
currentChatId.set(sessionId);
const messages = activeClient.renderMessage;
renderMessages.set(messages);
const lastMessage = messages[messages.length - 1];
currentNodeName.set(lastMessage?.node_name || lastMessage?.name || "__start__");
updateLoadingFromClientStatus();
if (showGraph.get())
refreshGraph();
refreshTools();
const currentThread = activeClient.getCurrentThread();
if (currentThread && (currentThread.status === "busy" || currentThread.status === "pending")) {
await activeClient.resetStream();
}
// 检查是否处于中断状态
if (currentThread?.status === "interrupted") {
const interruptInfo = activeClient.interruptData;
if (interruptInfo) {
interruptData.set(interruptInfo);
isInterrupted.set(true);
}
}
}
}
catch (error) {
console.error("Failed to activate session:", error);
inChatError.set(error.message);
}
}
// ============ 消息和交互逻辑 ============
async function sendMessage(message, extraData, withoutCheck = false, isResume = false) {
const c = client.get();
if ((!withoutCheck && !userInput.get().trim() && !message?.length) || !c)
return;
// 使用 client.status 判断是否正在加载
if (c.status === "busy")
return;
inChatError.set(null);
try {
loading.set(true);
await c.sendMessage(message || userInput.get(), extraData);
}
catch (e) {
const isThreadRunning = e.message.includes("422");
if (isThreadRunning) {
await c.resetStream();
}
else {
throw e;
}
}
finally {
userInput.set("");
updateLoadingFromClientStatus();
}
}
function stopGeneration() {
client.get()?.cancelRun();
}
function toggleToolCollapse(toolId) {
const prev = collapsedTools.get();
collapsedTools.set(prev.includes(toolId) ? prev.filter((id) => id !== toolId) : [...prev, toolId]);
}
function toggleHistoryVisible() {
showHistory.set(!showHistory.get());
if (showHistory.get()) {
refreshSessionList();
}
}
function addToHistory(thread) {
const prev = historyList.get();
historyList.set([thread, ...prev]);
}
function getToolUIRender(tool_name) {
const c = client.get();
if (!c)
return null;
const toolRender = c.tools.getTool(tool_name)?.render;
return toolRender ? (message) => toolRender(new ToolRenderData(message, c)) : null;
}
// ============ 返回 Store API ============
const artifactHook = useArtifacts(renderMessages, client);
return {
data: {
// 核心客户端
client,
history,
sessions,
// UI 状态
renderMessages,
userInput,
loading,
inChatError,
currentAgent,
currentChatId,
currentNodeName,
currentStatus,
// Interrupt 状态
interruptData,
isInterrupted,
// 工具和图表
tools,
collapsedTools,
showGraph,
graphVisualize,
// 历史记录
showHistory,
historyList,
...artifactHook.data,
},
mutations: {
// 初始化
initClient,
getClient: () => client.get(),
getHistory: () => history.get(),
// 会话管理
activateSession,
createNewSession,
refreshSessionList,
refreshHistoryList: refreshSessionList, // 向后兼容
// 消息操作
sendMessage,
stopGeneration,
setUserInput: (input) => userInput.set(input),
async revertChatTo(messageId, resend = false, sendOptions) {
await client.get()?.revertChatTo(messageId, sendOptions || {});
if (resend) {
return sendMessage([], sendOptions, true);
}
else {
updateUI(client.get());
}
},
// 工具操作
refreshTools,
setTools(new_tools) {
tools.set(new_tools);
refreshTools();
},
toggleToolCollapse,
getToolUIRender,
isFELocking: () => client.get()?.isFELocking(renderMessages.get()),
// UI 切换
toggleHistoryVisible,
toggleGraphVisible() {
showGraph.set(!showGraph.get());
if (showGraph.get())
refreshGraph();
},
refreshGraph,
// Agent 切换
setCurrentAgent(agent) {
currentAgent.set(agent);
return initClient();
},
resumeFromInterrupt(data) {
return sendMessage([], {
command: {
resume: data,
},
}, true);
},
// 历史记录(兼容旧 API)
addToHistory,
createNewChat: createNewSession,
toHistoryChat: (thread) => activateSession(thread.thread_id, true),
async deleteHistoryChat(thread) {
const historyManager = history.get();
if (historyManager) {
await historyManager.deleteSession(thread.thread_id);
await refreshSessionList();
}
},
...artifactHook.mutation,
},
};
};