fix: stabilize rectification staging release
This commit is contained in:
@@ -292,6 +292,18 @@ const allowedKindsByDomain: Readonly<Record<RectificationEvidenceDomain, readonl
|
||||
other: ["other"],
|
||||
};
|
||||
|
||||
const familyRelatedPeople = new Set<ModelAssistedEventExtraction["relatedPerson"]>([
|
||||
"father",
|
||||
"mother",
|
||||
"grandparent",
|
||||
"sibling",
|
||||
]);
|
||||
const explicitFamilySubjectMarkers = [
|
||||
"父亲", "爸爸", "老爸", "母亲", "妈妈", "老妈",
|
||||
"爷爷", "奶奶", "外公", "外婆", "祖父", "祖母", "外祖父", "外祖母",
|
||||
"兄弟", "姐妹", "家里老人", "家中老人",
|
||||
] as const;
|
||||
|
||||
export function validatedModelAssistedEvidence(input: Readonly<{
|
||||
rawText: string;
|
||||
sourceTurnId: string;
|
||||
@@ -305,7 +317,14 @@ export function validatedModelAssistedEvidence(input: Readonly<{
|
||||
const date = parseDeclaredDateText(dateText.normalize("NFKC"), input.asOfDate);
|
||||
if (!date || dateIsFuture(date, input.asOfDate)) return null;
|
||||
if (!allowedKindsByDomain[input.extraction.domain]?.includes(input.extraction.eventKind)) return null;
|
||||
if (input.extraction.subject === "partner" && input.extraction.domain !== "relationship") return null;
|
||||
const { subject, relatedPerson, domain } = input.extraction;
|
||||
if (subject === "self" && relatedPerson !== null) return null;
|
||||
if ((subject === "family") !== (domain === "family")) return null;
|
||||
if (familyRelatedPeople.has(relatedPerson) && subject !== "family") return null;
|
||||
if (relatedPerson === "partner" && (subject !== "partner" || domain !== "relationship")) return null;
|
||||
if (subject === "partner" && (domain !== "relationship" || relatedPerson !== "partner")) return null;
|
||||
if (explicitFamilySubjectMarkers.some((marker) => sourceSpan.includes(marker))
|
||||
&& (subject !== "family" || domain !== "family")) return null;
|
||||
const summary = eventSummary(sourceSpan);
|
||||
if (summary === missingEventSummary) return null;
|
||||
const familyContext = input.extraction.subject === "family" || input.extraction.domain === "family";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { CandidateSnapshot, EvidenceDomain, LifeEventRevision, RectificationV4Turn } from "../rectification-v4/contracts.ts";
|
||||
import { chronologicalEvents } from "../rectification-v4/evidence-ledger.ts";
|
||||
import type { TargetDisposition } from "../rectification-v4/extraction.ts";
|
||||
import type { DiagnosticsSummary, QuestionOpportunity, SemanticQuestionOpportunity } from "./contracts.ts";
|
||||
|
||||
@@ -181,12 +182,12 @@ export function buildQuestionOpportunities(input: Readonly<{
|
||||
}
|
||||
|
||||
const scoreableCount = input.events.filter((event) => event.scoreability === "scoreable").length;
|
||||
const latestEvent = chronologicalEvents(input.events).at(-1);
|
||||
for (const [domain, policy] of Object.entries(domainPolicy) as [Exclude<EvidenceDomain, "family" | "other">, (typeof domainPolicy)[Exclude<EvidenceDomain, "family" | "other">]][]) {
|
||||
if (refusedDomains.has(domain)) continue;
|
||||
const covered = scoreableDomains.has(domain);
|
||||
const themeBonus = policy.keywords.test(latestContext) ? .12 : 0;
|
||||
const alreadyAsked = input.turns.some((turn) => turn.questionDomain === domain && !turn.questionTargetEventId);
|
||||
const latestEvent = input.events.at(-1);
|
||||
const latestAnchor = latestEvent ? anchorFor(latestEvent) : null;
|
||||
const prompt = latestAnchor
|
||||
? `承接“${latestAnchor}”,请再说一件时间相对明确的经历:${policy.fallbackPrompt}`
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createTool } from "@mastra/core/tools";
|
||||
import { z } from "zod";
|
||||
import { defaultLanguageModel, resolveLanguageModel } from "@/mastra/model";
|
||||
import type { CandidateSnapshot, LifeEventRevision, PendingEvidence, RectificationV4Case, RectificationV4Turn } from "../rectification-v4/contracts.ts";
|
||||
import { chronologicalEvents } from "../rectification-v4/evidence-ledger.ts";
|
||||
import type { TargetDisposition } from "../rectification-v4/extraction.ts";
|
||||
import { deterministicDecision } from "./fallback-policy.ts";
|
||||
import { recordRectificationAgentTelemetry } from "./telemetry.ts";
|
||||
@@ -54,7 +55,7 @@ export function buildReasonerState(input: Readonly<{
|
||||
candidateRangeChanged: input.candidateRangeChanged ?? false,
|
||||
latestAnswer: input.recentTurns?.at(-1)?.answer ?? "",
|
||||
recentTurns: (input.recentTurns ?? []).slice(-6).map((turn) => ({ question: turn.question, answer: turn.answer })),
|
||||
recentEvents: (input.recentEvents ?? []).slice(-5).map((event) => ({ summary: event.summary, date: event.dateRange.label, domain: event.domain, subject: event.subject })),
|
||||
recentEvents: chronologicalEvents(input.recentEvents ?? []).slice(-5).map((event) => ({ summary: event.summary, date: event.dateRange.label, domain: event.domain, subject: event.subject })),
|
||||
currentTarget: input.currentTarget ? { summary: input.currentTarget.summary, date: input.currentTarget.dateRange.label, domain: input.currentTarget.domain } : null,
|
||||
targetDisposition: input.targetDisposition ?? "not_applicable",
|
||||
pendingEvidence: {
|
||||
|
||||
@@ -11,7 +11,8 @@ const bannedAcknowledgement = /(?:这个信息很有用|它不是单纯的|而
|
||||
const overinterpretedAcknowledgement = /(?:职业方向正式落地|人生意义|意味着你|说明你(?:已经|开始|正式)|标志着你)/;
|
||||
const internalTerms = /(?:opportunityId|snapshotId|eventId|targetEventId|requestedFields|fallbackPrompt|tool\s*call|tool_call|score|评分|模型名|opportunity|snapshot|D\d{1,2}|KP\b|Vimshottari)/i;
|
||||
const multiQuestionMoves = /(?:另外|还有|同时再说|并且告诉我|顺便|再告诉我)/;
|
||||
const birthMinute = /(?:出生|生时|几点).{0,12}(?:[01]\d|2[0-3]):[0-5]\d|(?:[01]\d|2[0-3]):[0-5]\d.{0,12}(?:出生|生时)/;
|
||||
const exactClockMinute = /(?:[01]?\d|2[0-3])[::][0-5]\d|(?:[零〇一二两三四五六七八九十]{1,3}|(?:[01]?\d|2[0-3]))点(?:[零〇一二两三四五六七八九十]{1,3}|[0-5]?\d)分/;
|
||||
const exactMinuteClaim = /(?:唯一|准确|精确|确切|确认|确定|代表).{0,12}(?:出生|生时)?(?:时间|时刻|分钟)|(?:出生|生时)(?:时间|时刻|分钟)?.{0,12}(?:唯一|准确|精确|确切|确认|确定|代表|就是)/;
|
||||
|
||||
function agentFor(modelId: string | null): { id: string; agent: Agent } | null {
|
||||
const selected = (modelId ? resolveLanguageModel(modelId) : null) ?? defaultLanguageModel();
|
||||
@@ -33,6 +34,13 @@ function normalized(value: string): string {
|
||||
return value.normalize("NFKC").replace(/[“”"'\s,,。.!!??::;;]/g, "");
|
||||
}
|
||||
|
||||
function visibleTextSafetyIssues(value: string): string[] {
|
||||
const issues: string[] = [];
|
||||
if (internalTerms.test(value)) issues.push("internal_information_exposed");
|
||||
if (exactClockMinute.test(value) || exactMinuteClaim.test(value)) issues.push("birth_minute_injected");
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function validateQuestionRealization(question: unknown, opportunity: QuestionOpportunity): Readonly<{ valid: boolean; issues: readonly string[] }> {
|
||||
if (typeof question !== "string") return { valid: false, issues: ["question_missing"] };
|
||||
const value = question.trim();
|
||||
@@ -41,9 +49,8 @@ export function validateQuestionRealization(question: unknown, opportunity: Ques
|
||||
if ((value.match(/[??]/g) ?? []).length > 1) issues.push("multiple_question_marks");
|
||||
if ((value.match(/[。.!!??]/g) ?? []).length > 2) issues.push("too_many_sentences");
|
||||
if (/\n\s*(?:[-*•]|\d+[.)、])/.test(value)) issues.push("question_list_forbidden");
|
||||
if (internalTerms.test(value)) issues.push("internal_information_exposed");
|
||||
issues.push(...visibleTextSafetyIssues(value));
|
||||
if (multiQuestionMoves.test(value)) issues.push("multiple_question_instruction");
|
||||
if (birthMinute.test(value)) issues.push("birth_minute_injected");
|
||||
if (opportunity.targetEventId) {
|
||||
const questionText = normalized(value);
|
||||
if (!opportunity.anchors.some((anchor) => questionText.includes(normalized(anchor)))) issues.push("target_anchor_missing");
|
||||
@@ -111,9 +118,9 @@ export function realizePublicMessage(value: unknown, input: Parameters<typeof de
|
||||
const parsed = publicMessageSchema.parse(value);
|
||||
const opportunity = input.validated.selectedOpportunity;
|
||||
const fallback = deterministic(input);
|
||||
const acknowledgement = bannedAcknowledgement.test(parsed.acknowledgement)
|
||||
const acknowledgement = visibleTextSafetyIssues(parsed.acknowledgement).length > 0
|
||||
|| bannedAcknowledgement.test(parsed.acknowledgement)
|
||||
|| overinterpretedAcknowledgement.test(parsed.acknowledgement)
|
||||
|| internalTerms.test(parsed.acknowledgement)
|
||||
|| (parsed.acknowledgement.match(/[。.!!??]/g) ?? []).length > 2
|
||||
|| (input.acceptedEvents.at(-1) && !normalized(parsed.acknowledgement).includes(normalized(input.acceptedEvents.at(-1)!.summary)))
|
||||
? fallback.acknowledgement
|
||||
@@ -124,7 +131,7 @@ export function realizePublicMessage(value: unknown, input: Parameters<typeof de
|
||||
return {
|
||||
acknowledgement,
|
||||
candidateUpdate: fallback.candidateUpdate,
|
||||
limitation: fallback.limitation ?? (parsed.limitation && !internalTerms.test(parsed.limitation) ? parsed.limitation : null),
|
||||
limitation: fallback.limitation ?? (parsed.limitation && visibleTextSafetyIssues(parsed.limitation).length === 0 ? parsed.limitation : null),
|
||||
question,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ export function latestEventRevisions(revisions: readonly LifeEventRevision[]): r
|
||||
return [...latest.values()].sort((left, right) => left.eventId.localeCompare(right.eventId));
|
||||
}
|
||||
|
||||
export function chronologicalEvents(events: readonly LifeEventRevision[]): readonly LifeEventRevision[] {
|
||||
return [...events].sort((left, right) => left.createdAt.localeCompare(right.createdAt)
|
||||
|| left.eventId.localeCompare(right.eventId)
|
||||
|| left.revision - right.revision);
|
||||
}
|
||||
|
||||
export function appendEventRevision(
|
||||
revisions: readonly LifeEventRevision[],
|
||||
input: NewEventRevision,
|
||||
|
||||
Reference in New Issue
Block a user