Occupation collect was wiping year-month into an unscored note, and idle gap copy never joined the evidence turn. Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
/**
|
||
* Server-owned collect stems join the same assistant turn. Comparison is
|
||
* exact identity of the prompt string being attached, never a semantic
|
||
* “did this body already ask”.
|
||
*/
|
||
|
||
const SENTENCE_SPLIT = /(?<=[。!??\n])/;
|
||
const NARRATIVE_SENTENCE = /范围|记下|对照|\d{1,2}:\d{2}/;
|
||
|
||
export const EVIDENCE_VALUE_JUDGMENT_PHRASES = [
|
||
"很有帮助",
|
||
"很有价值",
|
||
"很有分量",
|
||
"特别有用",
|
||
] as const;
|
||
|
||
function spokenSentences(body: string): string[] {
|
||
return body.split(SENTENCE_SPLIT).map((part) => part.trim()).filter(Boolean);
|
||
}
|
||
|
||
export function trimEvidenceTurnBody(body: string, options?: { maxSentences?: number }): string {
|
||
const spoken = body.trim();
|
||
if (!spoken) return "";
|
||
const sentences = spokenSentences(spoken);
|
||
const maxSentences = options?.maxSentences;
|
||
let text: string;
|
||
if (typeof maxSentences === "number" && maxSentences > 0) {
|
||
text = sentences.length > maxSentences ? sentences.slice(0, maxSentences).join("") : spoken;
|
||
} else {
|
||
text = sentences.length > 2
|
||
? (/[。!??]$/.test(sentences[0] ?? "") ? sentences[0] ?? "" : `${sentences[0] ?? ""}。`)
|
||
: spoken;
|
||
}
|
||
for (const phrase of EVIDENCE_VALUE_JUDGMENT_PHRASES) {
|
||
text = text.replaceAll(phrase, "");
|
||
}
|
||
return text.replace(/[,、]{2,}/g, ",").replace(/[ \t]+/g, " ").trim();
|
||
}
|
||
|
||
export function trimSpokenTurnForInterview(body: string, terminalNote: boolean): string {
|
||
return terminalNote
|
||
? trimEvidenceTurnBody(body, { maxSentences: 3 })
|
||
: trimEvidenceTurnBody(body);
|
||
}
|
||
|
||
function isQuestionSentence(text: string, stem: string): boolean {
|
||
if (!text) return false;
|
||
if (stem && text === stem) return true;
|
||
const prefix = stem.slice(0, 12);
|
||
if (prefix && text.startsWith(prefix)) return true;
|
||
return /[??]$/.test(text);
|
||
}
|
||
|
||
export function stripVerbalWindowChange(body: string): string {
|
||
const stripped = body
|
||
.replace(/以你说的.{0,40}为准/g, "")
|
||
.replace(/[,、]{2,}/g, ",")
|
||
.replace(/[ \t]+/g, " ")
|
||
.replace(/[,、]+\s*(?=[。..!?!?;;]|$)/g, "")
|
||
.trim();
|
||
if (!stripped || /^[,、。..!?!?;;::\s]+$/.test(stripped)) return "";
|
||
return stripped;
|
||
}
|
||
|
||
export function stripQuestionSentences(body: string, stem: string): string {
|
||
const prompt = stem.trim();
|
||
const spoken = body.trim();
|
||
if (!spoken) return "";
|
||
const kept: string[] = [];
|
||
let dropContinuation = false;
|
||
for (const part of spoken.split(SENTENCE_SPLIT)) {
|
||
const text = part.trim();
|
||
if (!text) continue;
|
||
if (isQuestionSentence(text, prompt)) {
|
||
dropContinuation = true;
|
||
continue;
|
||
}
|
||
if (dropContinuation && !NARRATIVE_SENTENCE.test(text)) {
|
||
dropContinuation = false;
|
||
continue;
|
||
}
|
||
dropContinuation = false;
|
||
kept.push(part);
|
||
}
|
||
return kept.join("").trim();
|
||
}
|
||
|
||
export function composeCollectSpokenAssistantText(body: string, prompt: string): string {
|
||
const stem = prompt.trim();
|
||
const spoken = body.trim();
|
||
if (!stem) return spoken;
|
||
if (!spoken || spoken === stem) return stem;
|
||
const stripped = stripQuestionSentences(spoken, stem);
|
||
if (!stripped) return stem;
|
||
const suffix = `\n\n${stem}`;
|
||
if (stripped.includes(stem)) return stripped;
|
||
if (stripped.length >= suffix.length && stripped.slice(stripped.length - suffix.length) === suffix) {
|
||
return stripped;
|
||
}
|
||
return `${stripped}${suffix}`;
|
||
}
|
||
|
||
/** Idle-path gap copy joins the existing evidence recap instead of a second turn. */
|
||
export function composeIdleGapIntoSpoken(body: string, gap: string): string {
|
||
const spoken = body.trim();
|
||
const gapText = gap.trim();
|
||
if (!gapText) return spoken;
|
||
if (!/就能开始筛|现在记下的是/.test(gapText)) return spoken;
|
||
if (!spoken) return gapText;
|
||
if (spoken.includes(gapText) || spoken.includes("就能开始筛") || spoken.includes("现在记下的是")) {
|
||
return spoken;
|
||
}
|
||
const prefix = /[。!?!?]$/u.test(spoken) ? spoken : `${spoken}。`;
|
||
return `${prefix}${gapText}`;
|
||
}
|
||
|
||
export function detachCollectSpokenAssistantText(body: string, prompt: string): string {
|
||
const stem = prompt.trim();
|
||
const spoken = body.trim();
|
||
if (!stem || !spoken || spoken === stem) return spoken;
|
||
const suffix = `\n\n${stem}`;
|
||
if (spoken.length >= suffix.length && spoken.slice(spoken.length - suffix.length) === suffix) {
|
||
return spoken.slice(0, spoken.length - suffix.length).trim();
|
||
}
|
||
return spoken;
|
||
}
|