Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/collect-prompt.ts
T
Jesse_ChenandCursor 1fa994ea63
Independent Staging Quality Gate / validate (push) Successful in 13m26s
Independent Staging Quality Gate / publish (push) Successful in 9m33s
fix(rectification): keep dated occupation answers scoreable (BUG-649/650)
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>
2026-09-11 11:49:52 +08:00

127 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}