mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 18:44:57 +00:00
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 7458444144
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
168 lines
4.4 KiB
TypeScript
168 lines
4.4 KiB
TypeScript
import { formatReasoningMessage } from "../agents/pi-embedded-utils.js";
|
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
|
import { stripReasoningTagsFromText } from "../shared/text/reasoning-tags.js";
|
|
|
|
const REASONING_MESSAGE_PREFIX = "Reasoning:\n";
|
|
const REASONING_TAG_PREFIXES = [
|
|
"<think",
|
|
"<thinking",
|
|
"<thought",
|
|
"<antthinking",
|
|
"</think",
|
|
"</thinking",
|
|
"</thought",
|
|
"</antthinking",
|
|
];
|
|
const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
|
|
|
|
interface CodeRegion {
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
function findCodeRegions(text: string): CodeRegion[] {
|
|
const regions: CodeRegion[] = [];
|
|
|
|
const fencedRe = /(^|\n)(```|~~~)[^\n]*\n[\s\S]*?(?:\n\2(?:\n|$)|$)/g;
|
|
for (const match of text.matchAll(fencedRe)) {
|
|
const start = (match.index ?? 0) + match[1].length;
|
|
regions.push({ start, end: start + match[0].length - match[1].length });
|
|
}
|
|
|
|
const inlineRe = /`+[^`]+`+/g;
|
|
for (const match of text.matchAll(inlineRe)) {
|
|
const start = match.index ?? 0;
|
|
const end = start + match[0].length;
|
|
const insideFenced = regions.some((r) => start >= r.start && end <= r.end);
|
|
if (!insideFenced) {
|
|
regions.push({ start, end });
|
|
}
|
|
}
|
|
|
|
regions.sort((a, b) => a.start - b.start);
|
|
return regions;
|
|
}
|
|
|
|
function isInsideCode(pos: number, regions: CodeRegion[]): boolean {
|
|
return regions.some((r) => pos >= r.start && pos < r.end);
|
|
}
|
|
|
|
function extractThinkingFromTaggedStreamOutsideCode(text: string): string {
|
|
if (!text) {
|
|
return "";
|
|
}
|
|
const codeRegions = findCodeRegions(text);
|
|
let result = "";
|
|
let lastIndex = 0;
|
|
let inThinking = false;
|
|
THINKING_TAG_RE.lastIndex = 0;
|
|
for (const match of text.matchAll(THINKING_TAG_RE)) {
|
|
const idx = match.index ?? 0;
|
|
if (isInsideCode(idx, codeRegions)) {
|
|
continue;
|
|
}
|
|
if (inThinking) {
|
|
result += text.slice(lastIndex, idx);
|
|
}
|
|
const isClose = match[1] === "/";
|
|
inThinking = !isClose;
|
|
lastIndex = idx + match[0].length;
|
|
}
|
|
if (inThinking) {
|
|
result += text.slice(lastIndex);
|
|
}
|
|
return result.trim();
|
|
}
|
|
|
|
function isPartialReasoningTagPrefix(text: string): boolean {
|
|
const trimmed = text.trimStart().toLowerCase();
|
|
if (!trimmed.startsWith("<")) {
|
|
return false;
|
|
}
|
|
if (trimmed.includes(">")) {
|
|
return false;
|
|
}
|
|
return REASONING_TAG_PREFIXES.some((prefix) => prefix.startsWith(trimmed));
|
|
}
|
|
|
|
export type TelegramReasoningSplit = {
|
|
reasoningText?: string;
|
|
answerText?: string;
|
|
};
|
|
|
|
export function splitTelegramReasoningText(text?: string): TelegramReasoningSplit {
|
|
if (typeof text !== "string") {
|
|
return {};
|
|
}
|
|
|
|
const trimmed = text.trim();
|
|
if (isPartialReasoningTagPrefix(trimmed)) {
|
|
return {};
|
|
}
|
|
if (
|
|
trimmed.startsWith(REASONING_MESSAGE_PREFIX) &&
|
|
trimmed.length > REASONING_MESSAGE_PREFIX.length
|
|
) {
|
|
return { reasoningText: trimmed };
|
|
}
|
|
|
|
const taggedReasoning = extractThinkingFromTaggedStreamOutsideCode(text);
|
|
const strippedAnswer = stripReasoningTagsFromText(text, { mode: "strict", trim: "both" });
|
|
|
|
if (!taggedReasoning && strippedAnswer === text) {
|
|
return { answerText: text };
|
|
}
|
|
|
|
const reasoningText = taggedReasoning ? formatReasoningMessage(taggedReasoning) : undefined;
|
|
const answerText = strippedAnswer || undefined;
|
|
return { reasoningText, answerText };
|
|
}
|
|
|
|
export type BufferedFinalAnswer = {
|
|
payload: ReplyPayload;
|
|
text: string;
|
|
};
|
|
|
|
export function createTelegramReasoningStepState() {
|
|
let reasoningStatus: "none" | "hinted" | "delivered" = "none";
|
|
let bufferedFinalAnswer: BufferedFinalAnswer | undefined;
|
|
|
|
const noteReasoningHint = () => {
|
|
if (reasoningStatus === "none") {
|
|
reasoningStatus = "hinted";
|
|
}
|
|
};
|
|
|
|
const noteReasoningDelivered = () => {
|
|
reasoningStatus = "delivered";
|
|
};
|
|
|
|
const shouldBufferFinalAnswer = () => {
|
|
return reasoningStatus === "hinted" && !bufferedFinalAnswer;
|
|
};
|
|
|
|
const bufferFinalAnswer = (value: BufferedFinalAnswer) => {
|
|
bufferedFinalAnswer = value;
|
|
};
|
|
|
|
const takeBufferedFinalAnswer = (): BufferedFinalAnswer | undefined => {
|
|
const value = bufferedFinalAnswer;
|
|
bufferedFinalAnswer = undefined;
|
|
return value;
|
|
};
|
|
|
|
const resetForNextStep = () => {
|
|
reasoningStatus = "none";
|
|
bufferedFinalAnswer = undefined;
|
|
};
|
|
|
|
return {
|
|
noteReasoningHint,
|
|
noteReasoningDelivered,
|
|
shouldBufferFinalAnswer,
|
|
bufferFinalAnswer,
|
|
takeBufferedFinalAnswer,
|
|
resetForNextStep,
|
|
};
|
|
}
|