fix(agent): strip thinking tags from text content
This commit is contained in:
@@ -358,4 +358,84 @@ File contents here`,
|
|||||||
const result = extractAssistantText(msg);
|
const result = extractAssistantText(msg);
|
||||||
expect(result).toBe("Here's what I found:\nDone checking.");
|
expect(result).toBe("Here's what I found:\nDone checking.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips thinking tags from text content", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "<think>El usuario quiere retomar una tarea...</think>Aquí está tu respuesta.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("Aquí está tu respuesta.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips thinking tags without closing tag", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "<think>Pensando sobre el problema...",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips thinking tags with various formats", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Before<thinking>internal reasoning</thinking>After",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("BeforeAfter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips antthinking tags", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "<antthinking>Some reasoning</antthinking>The actual answer.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("The actual answer.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles nested or multiple thinking blocks", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Start<think>first thought</think>Middle<think>second thought</think>End",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("StartMiddleEnd");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,44 @@ function stripDowngradedToolCallText(text: string): string {
|
|||||||
return cleaned.trim();
|
return cleaned.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip thinking tags and their content from text.
|
||||||
|
* This is a safety net for cases where the model outputs <think> tags
|
||||||
|
* that slip through other filtering mechanisms.
|
||||||
|
*/
|
||||||
|
function stripThinkingTagsFromText(text: string): string {
|
||||||
|
if (!text) return text;
|
||||||
|
// Quick check to avoid regex overhead when no tags present.
|
||||||
|
if (!/(?:think(?:ing)?|thought|antthinking)/i.test(text)) return text;
|
||||||
|
|
||||||
|
const tagRe = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
|
||||||
|
let result = "";
|
||||||
|
let lastIndex = 0;
|
||||||
|
let inThinking = false;
|
||||||
|
|
||||||
|
for (const match of text.matchAll(tagRe)) {
|
||||||
|
const idx = match.index ?? 0;
|
||||||
|
const isClose = match[1] === "/";
|
||||||
|
|
||||||
|
if (!inThinking && !isClose) {
|
||||||
|
// Opening tag - save text before it.
|
||||||
|
result += text.slice(lastIndex, idx);
|
||||||
|
inThinking = true;
|
||||||
|
} else if (inThinking && isClose) {
|
||||||
|
// Closing tag - skip content inside.
|
||||||
|
inThinking = false;
|
||||||
|
}
|
||||||
|
lastIndex = idx + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append remaining text if we're not inside thinking.
|
||||||
|
if (!inThinking) {
|
||||||
|
result += text.slice(lastIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function extractAssistantText(msg: AssistantMessage): string {
|
export function extractAssistantText(msg: AssistantMessage): string {
|
||||||
const isTextBlock = (block: unknown): block is { type: "text"; text: string } => {
|
const isTextBlock = (block: unknown): block is { type: "text"; text: string } => {
|
||||||
if (!block || typeof block !== "object") return false;
|
if (!block || typeof block !== "object") return false;
|
||||||
@@ -58,7 +96,9 @@ export function extractAssistantText(msg: AssistantMessage): string {
|
|||||||
? msg.content
|
? msg.content
|
||||||
.filter(isTextBlock)
|
.filter(isTextBlock)
|
||||||
.map((c) =>
|
.map((c) =>
|
||||||
stripDowngradedToolCallText(stripMinimaxToolCallXml(c.text)).trim(),
|
stripThinkingTagsFromText(
|
||||||
|
stripDowngradedToolCallText(stripMinimaxToolCallXml(c.text)),
|
||||||
|
).trim(),
|
||||||
)
|
)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
Reference in New Issue
Block a user