UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

300 lines (299 loc) 11.8 kB
import { n as normalizeAccountId } from "./account-id-Df9e41E6.js"; import "./message-channel-constants-CgI32lqD.js"; import { t as resolveAccountEntry } from "./account-lookup-DL1YTqjF.js"; import { n as isSafeFenceBreak, r as parseFenceSpans, t as findFenceSpanAt } from "./fences-DSvtxlT8.js"; import { C as resolveChannelStreamingChunkMode } from "./streaming-_TVeMLGC.js"; import { t as chunkTextByBreakResolver } from "./text-chunking-DcQY-Iem.js"; //#region src/auto-reply/chunk.ts const DEFAULT_CHUNK_LIMIT = 4e3; const DEFAULT_CHUNK_MODE = "length"; function resolveChunkLimitForProvider(cfgSection, accountId) { if (!cfgSection) return; const normalizedAccountId = normalizeAccountId(accountId); const accounts = cfgSection.accounts; if (accounts && typeof accounts === "object") { const direct = resolveAccountEntry(accounts, normalizedAccountId); if (typeof direct?.textChunkLimit === "number") return direct.textChunkLimit; } return cfgSection.textChunkLimit; } function resolveTextChunkLimit(cfg, provider, accountId, opts) { const fallback = typeof opts?.fallbackLimit === "number" && opts.fallbackLimit > 0 ? opts.fallbackLimit : DEFAULT_CHUNK_LIMIT; const providerOverride = (() => { if (!provider || provider === "webchat") return; return resolveChunkLimitForProvider((cfg?.channels)?.[provider] ?? cfg?.[provider], accountId); })(); if (typeof providerOverride === "number" && providerOverride > 0) return providerOverride; return fallback; } function resolveChunkModeForProvider(cfgSection, accountId) { if (!cfgSection) return; const normalizedAccountId = normalizeAccountId(accountId); const accounts = cfgSection.accounts; if (accounts && typeof accounts === "object") { const directMode = resolveChannelStreamingChunkMode(resolveAccountEntry(accounts, normalizedAccountId)); if (directMode) return directMode; } return resolveChannelStreamingChunkMode(cfgSection) ?? cfgSection.chunkMode; } function resolveChunkMode(cfg, provider, accountId) { if (!provider || provider === "webchat") return DEFAULT_CHUNK_MODE; return resolveChunkModeForProvider((cfg?.channels)?.[provider] ?? cfg?.[provider], accountId) ?? DEFAULT_CHUNK_MODE; } /** * Split text on newlines, trimming line whitespace. * Blank lines are folded into the next non-empty line as leading "\n" prefixes. * Long lines can be split by length (default) or kept intact via splitLongLines:false. */ function chunkByNewline(text, maxLineLength, opts) { if (!text) return []; if (maxLineLength <= 0) return text.trim() ? [text] : []; const splitLongLines = opts?.splitLongLines !== false; const trimLines = opts?.trimLines !== false; const lines = splitByNewline(text, opts?.isSafeBreak); const chunks = []; let pendingBlankLines = 0; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) { pendingBlankLines += 1; continue; } const maxPrefix = Math.max(0, maxLineLength - 1); const cappedBlankLines = pendingBlankLines > 0 ? Math.min(pendingBlankLines, maxPrefix) : 0; const prefix = cappedBlankLines > 0 ? "\n".repeat(cappedBlankLines) : ""; pendingBlankLines = 0; const lineValue = trimLines ? trimmed : line; if (!splitLongLines || lineValue.length + prefix.length <= maxLineLength) { chunks.push(prefix + lineValue); continue; } const firstLimit = Math.max(1, maxLineLength - prefix.length); const first = lineValue.slice(0, firstLimit); chunks.push(prefix + first); const remaining = lineValue.slice(firstLimit); if (remaining) chunks.push(...chunkText(remaining, maxLineLength)); } if (pendingBlankLines > 0 && chunks.length > 0) chunks[chunks.length - 1] += "\n".repeat(pendingBlankLines); return chunks; } /** * Split text into chunks on paragraph boundaries (blank lines), preserving lists and * single-newline line wraps inside paragraphs. * * - Only breaks at paragraph separators ("\n\n" or more, allowing whitespace on blank lines) * - Packs multiple paragraphs into a single chunk up to `limit` * - Falls back to length-based splitting when a single paragraph exceeds `limit` * (unless `splitLongParagraphs` is disabled) */ function chunkByParagraph(text, limit, opts) { if (!text) return []; if (limit <= 0) return [text]; const splitLongParagraphs = opts?.splitLongParagraphs !== false; const normalized = text.replace(/\r\n?/g, "\n"); if (!/\n[\t ]*\n+/.test(normalized)) { if (normalized.length <= limit) return [normalized]; if (!splitLongParagraphs) return [normalized]; return chunkText(normalized, limit); } const spans = parseFenceSpans(normalized); const parts = []; const separators = []; const re = /\n[\t ]*\n+/g; let lastIndex = 0; for (const match of normalized.matchAll(re)) { const idx = match.index ?? 0; if (!isSafeFenceBreak(spans, idx)) continue; parts.push(normalized.slice(lastIndex, idx)); separators.push(match[0]); lastIndex = idx + match[0].length; } parts.push(normalized.slice(lastIndex)); const chunks = []; let currentChunk = ""; const pushParagraph = (paragraph, separatorBefore) => { if (!currentChunk) { if (paragraph.length <= limit) { currentChunk = paragraph; return; } if (!splitLongParagraphs) { chunks.push(paragraph); return; } chunks.push(...chunkText(paragraph, limit)); return; } const candidate = `${currentChunk}${separatorBefore ?? "\n\n"}${paragraph}`; if (candidate.length <= limit) { currentChunk = candidate; return; } chunks.push(currentChunk); currentChunk = ""; pushParagraph(paragraph); }; for (const [index, part] of parts.entries()) { const paragraph = part.replace(/\s+$/g, ""); if (!paragraph.trim()) continue; pushParagraph(paragraph, separators[index - 1]); } if (currentChunk) chunks.push(currentChunk); return chunks; } /** * Unified chunking function that dispatches based on mode. */ function chunkTextWithMode(text, limit, mode) { if (mode === "newline") return chunkByParagraph(text, limit); return chunkText(text, limit); } function chunkMarkdownTextWithMode(text, limit, mode) { if (mode === "newline") { const paragraphChunks = chunkByParagraph(text, limit, { splitLongParagraphs: false }); const out = []; for (const chunk of paragraphChunks.flatMap((paragraphChunk) => paragraphChunk.length > limit ? splitPackedFenceParagraphChunk(paragraphChunk) : paragraphChunk)) out.push(...chunkMarkdownText(chunk, limit)); return out; } return chunkMarkdownText(text, limit); } function splitByNewline(text, isSafeBreak = () => true) { const lines = []; let start = 0; for (let i = 0; i < text.length; i++) if (text[i] === "\n" && isSafeBreak(i)) { lines.push(text.slice(start, i)); start = i + 1; } lines.push(text.slice(start)); return lines; } function splitPackedFenceParagraphChunk(chunk) { const chunks = []; let start = 0; for (const span of parseFenceSpans(chunk)) { if (span.end <= start) continue; const separator = chunk.slice(span.end).match(/^\n[\t ]*\n+/)?.[0]; if (!separator) continue; if (!chunk.slice(span.end + separator.length).trim()) continue; chunks.push(chunk.slice(start, span.end)); start = span.end + separator.length; } if (chunks.length === 0) return [chunk]; const tail = chunk.slice(start); if (tail) chunks.push(tail); return chunks; } function resolveChunkEarlyReturn(text, limit) { if (!text) return []; if (limit <= 0) return [text]; if (text.length <= limit) return [text]; } function chunkText(text, limit) { const early = resolveChunkEarlyReturn(text, limit); if (early) return early; return chunkTextByBreakResolver(text, limit, (window) => { const { lastNewline, lastWhitespace } = scanParenAwareBreakpoints(window, 0, window.length); return lastNewline > 0 ? lastNewline : lastWhitespace; }); } function chunkMarkdownText(text, limit) { const early = resolveChunkEarlyReturn(text, limit); if (early) return early; const chunks = []; const spans = parseFenceSpans(text); let start = 0; let reopenFence; while (start < text.length) { const reopenPrefix = reopenFence ? `${reopenFence.openLine}\n` : ""; const contentLimit = Math.max(1, limit - reopenPrefix.length); if (text.length - start <= contentLimit) { const finalChunk = `${reopenPrefix}${text.slice(start)}`; if (finalChunk.length > 0) chunks.push(finalChunk); break; } const windowEnd = Math.min(text.length, start + contentLimit); const softBreak = pickSafeBreakIndex(text, start, windowEnd, spans); let breakIdx = softBreak > start ? softBreak : windowEnd; const initialFence = isSafeFenceBreak(spans, breakIdx) ? void 0 : findFenceSpanAt(spans, breakIdx); let fenceToSplit = initialFence; if (initialFence) { const closeLine = `${initialFence.indent}${initialFence.marker}`; const maxIdxIfNeedNewline = start + (contentLimit - (closeLine.length + 1)); if (maxIdxIfNeedNewline <= start) breakIdx = windowEnd; else { const minProgressIdx = Math.min(text.length, Math.max(start + 1, initialFence.start + initialFence.openLine.length + 2)); const maxIdxIfAlreadyNewline = start + (contentLimit - closeLine.length); let pickedNewline = false; let lastNewline = text.lastIndexOf("\n", Math.max(start, maxIdxIfAlreadyNewline - 1)); while (lastNewline >= start) { const candidateBreak = lastNewline + 1; if (candidateBreak < minProgressIdx) break; const candidateFence = findFenceSpanAt(spans, candidateBreak); if (candidateFence && candidateFence.start === initialFence.start) { breakIdx = candidateBreak; pickedNewline = true; break; } lastNewline = text.lastIndexOf("\n", lastNewline - 1); } if (!pickedNewline) if (minProgressIdx > maxIdxIfAlreadyNewline) breakIdx = windowEnd; else breakIdx = Math.max(minProgressIdx, maxIdxIfNeedNewline); } const fenceAtBreak = findFenceSpanAt(spans, breakIdx); fenceToSplit = fenceAtBreak && fenceAtBreak.start === initialFence.start ? fenceAtBreak : void 0; } const rawContent = text.slice(start, breakIdx); if (!rawContent) break; let rawChunk = `${reopenPrefix}${rawContent}`; const brokeOnSeparator = breakIdx < text.length && /\s/.test(text[breakIdx]); let nextStart = Math.min(text.length, breakIdx + (brokeOnSeparator ? 1 : 0)); if (fenceToSplit) { const closeLine = `${fenceToSplit.indent}${fenceToSplit.marker}`; rawChunk = rawChunk.endsWith("\n") ? `${rawChunk}${closeLine}` : `${rawChunk}\n${closeLine}`; reopenFence = fenceToSplit; } else { nextStart = skipLeadingNewlines(text, nextStart); reopenFence = void 0; } chunks.push(rawChunk); start = nextStart; } return chunks; } function skipLeadingNewlines(value, start = 0) { let i = start; while (i < value.length && value[i] === "\n") i++; return i; } function pickSafeBreakIndex(text, start, end, spans) { const { lastNewline, lastWhitespace } = scanParenAwareBreakpoints(text, start, end, (index) => isSafeFenceBreak(spans, index)); if (lastNewline > start) return lastNewline; if (lastWhitespace > start) return lastWhitespace; return -1; } function scanParenAwareBreakpoints(text, start, end, isAllowed = () => true) { let lastNewline = -1; let lastWhitespace = -1; let depth = 0; for (let i = start; i < end; i++) { if (!isAllowed(i)) continue; const char = text[i]; if (char === "(") { depth += 1; continue; } if (char === ")" && depth > 0) { depth -= 1; continue; } if (depth !== 0) continue; if (char === "\n") lastNewline = i; else if (/\s/.test(char)) lastWhitespace = i; } return { lastNewline, lastWhitespace }; } //#endregion export { chunkText as a, resolveTextChunkLimit as c, chunkMarkdownTextWithMode as i, chunkByParagraph as n, chunkTextWithMode as o, chunkMarkdownText as r, resolveChunkMode as s, chunkByNewline as t };