fix: preserve rectification conversation context
This commit is contained in:
@@ -986,8 +986,33 @@ async function createProductionService(
|
||||
const profileClient = authenticated.context as ProfileClient;
|
||||
const engine = createJyotishBirthTimeJourneyEngine();
|
||||
const store = createSupabaseConversationalRectificationStore(admin);
|
||||
const loadConversationMessages = async (userId: string, caseId: string) => {
|
||||
const { data: ownedCase, error: caseError } = await admin
|
||||
.from("birth_time_rectification_cases")
|
||||
.select("id")
|
||||
.eq("id", caseId)
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
if (caseError || !ownedCase) return [];
|
||||
const [{ data: turns, error: turnsError }, { data: evidence, error: evidenceError }] = await Promise.all([
|
||||
admin
|
||||
.from("birth_time_rectification_turns")
|
||||
.select("id,turn_version,narrative")
|
||||
.eq("case_id", caseId)
|
||||
.order("turn_version", { ascending: true }),
|
||||
admin
|
||||
.from("birth_time_rectification_event_evidence")
|
||||
.select("source_turn_id,raw_text,created_at,id")
|
||||
.eq("case_id", caseId)
|
||||
.order("created_at", { ascending: true })
|
||||
.order("id", { ascending: true }),
|
||||
]);
|
||||
if (turnsError || evidenceError) return [];
|
||||
return conversationMessagesFromStoredTurns(turns, evidence);
|
||||
};
|
||||
const service = createConversationalRectificationService({
|
||||
store,
|
||||
loadConversationMessages,
|
||||
billing: createSupabaseConversationalRectificationBilling(admin),
|
||||
get rectificationPriceCredits() { return priceCredits(); },
|
||||
allowNewCaseCreation: conversationalRectificationCreationPolicyFromEnvironment(
|
||||
@@ -1073,33 +1098,7 @@ async function createProductionService(
|
||||
narrativeGenerator,
|
||||
asOfDate: () => new Date().toISOString().slice(0, 10),
|
||||
});
|
||||
return {
|
||||
...service,
|
||||
async loadConversationMessages(userId, caseId) {
|
||||
const { data: ownedCase, error: caseError } = await admin
|
||||
.from("birth_time_rectification_cases")
|
||||
.select("id")
|
||||
.eq("id", caseId)
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
if (caseError || !ownedCase) return [];
|
||||
const [{ data: turns, error: turnsError }, { data: evidence, error: evidenceError }] = await Promise.all([
|
||||
admin
|
||||
.from("birth_time_rectification_turns")
|
||||
.select("id,turn_version,narrative")
|
||||
.eq("case_id", caseId)
|
||||
.order("turn_version", { ascending: true }),
|
||||
admin
|
||||
.from("birth_time_rectification_event_evidence")
|
||||
.select("source_turn_id,raw_text,created_at,id")
|
||||
.eq("case_id", caseId)
|
||||
.order("created_at", { ascending: true })
|
||||
.order("id", { ascending: true }),
|
||||
]);
|
||||
if (turnsError || evidenceError) return [];
|
||||
return conversationMessagesFromStoredTurns(turns, evidence);
|
||||
},
|
||||
};
|
||||
return { ...service, loadConversationMessages };
|
||||
}
|
||||
|
||||
function stableRequestId(request: Request): string {
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
|
||||
export type RectificationNarrativePhase = "first" | "intermediate" | "final";
|
||||
|
||||
export type RectificationConversationMessage = Readonly<{
|
||||
role: "assistant" | "user";
|
||||
text: string;
|
||||
}>;
|
||||
|
||||
export type RectificationNarrativeContext = Readonly<{
|
||||
recentConversation?: ReadonlyArray<RectificationConversationMessage>;
|
||||
latestUserText?: string;
|
||||
latestEvidence?: ReadonlyArray<{
|
||||
id?: string;
|
||||
@@ -353,6 +359,7 @@ function grounding(packet: RectificationTechnicalPacket, phase: RectificationNar
|
||||
|
||||
function narrativeConversationContext(context: RectificationNarrativeContext) {
|
||||
return {
|
||||
recentConversation: context.recentConversation?.slice(-40),
|
||||
latestUserText: context.latestUserText,
|
||||
latestEvidence: context.latestEvidence?.map(({ id, dateLabel, summary }) => ({
|
||||
id,
|
||||
@@ -510,6 +517,7 @@ function promptFor(
|
||||
task: "write_grounded_rectification_narrative",
|
||||
phase,
|
||||
conversationContext: narrativeConversationContext(context),
|
||||
continuity: "recentConversation 是同一会话的真实连续问答。必须直接理解用户对上一条问题的确认、否认、补充或纠正,不得把“是的/对/来年/那次”等回复当成脱离上下文的新事件,也不得重复询问已经确认的信息。",
|
||||
packet: grounding(packet, phase),
|
||||
outputContract: {
|
||||
returnOnlyNarrativeAndEvidenceRequest: true,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "./persistence-contracts.ts";
|
||||
import {
|
||||
generateRectificationNarrative,
|
||||
type RectificationConversationMessage,
|
||||
type RectificationNarrativeGenerator,
|
||||
type RectificationNarrativeResult,
|
||||
} from "./narrative-agent.ts";
|
||||
@@ -84,6 +85,10 @@ export type ConversationalRectificationServicePorts = Readonly<{
|
||||
buildTechnicalPacket(
|
||||
input: ConversationalRectificationPacketBuildInput,
|
||||
): Promise<ComputedConversationalRectificationPacket>;
|
||||
loadConversationMessages?(
|
||||
userId: string,
|
||||
caseId: string,
|
||||
): Promise<ReadonlyArray<RectificationConversationMessage>>;
|
||||
narrativeGenerator: RectificationNarrativeGenerator;
|
||||
asOfDate(): string;
|
||||
}>;
|
||||
@@ -126,6 +131,8 @@ const genericUncertaintyPattern = /(?:不知道|不确定)/;
|
||||
const contextualRelativeMonthPattern = /(?:来年|次年|第二年|翌年|同年|当年|那年)\s*(\d{1,2})\s*月份?/;
|
||||
const contextualBareMonthDayPattern = /^\s*(\d{1,2})\s*月\s*(\d{1,2})\s*(?:日|号)\s*[。.]?\s*$/;
|
||||
const contextualBareDayPattern = /^\s*(\d{1,2})\s*(?:日|号)\s*[。.]?\s*$/;
|
||||
const affirmativeAnswerPattern = /^\s*(?:是(?:的)?|对(?:的)?|没错|正确|确认|嗯+|没问题)\s*[。.!!]?\s*$/;
|
||||
const proposedDatePattern = /((?:19|20)\d{2})\s*年\s*(1[0-2]|0?[1-9])\s*月(?:\s*(3[01]|[12]\d|0?[1-9])\s*(?:日|号))?/g;
|
||||
|
||||
export function evidencePredatesBirthDate(
|
||||
evidence: Pick<LifeEventEvidence, "dateValue" | "datePrecision">,
|
||||
@@ -318,6 +325,7 @@ function evidenceRecap(evidence: ReadonlyArray<LifeEventEvidenceInput>) {
|
||||
}
|
||||
|
||||
function narrativeConversationContext(input: Readonly<{
|
||||
recentConversation?: ReadonlyArray<RectificationConversationMessage>;
|
||||
latestUserText: string;
|
||||
allEvidence: ReadonlyArray<LifeEventEvidenceInput>;
|
||||
newEvidence: ReadonlyArray<LifeEventEvidenceInput>;
|
||||
@@ -325,6 +333,7 @@ function narrativeConversationContext(input: Readonly<{
|
||||
const activeEvidence = effectiveLifeEventEvidence(input.allEvidence);
|
||||
const activeIds = new Set(activeEvidence.map((item) => item.id));
|
||||
return {
|
||||
recentConversation: input.recentConversation,
|
||||
latestUserText: input.latestUserText.trim().slice(0, 4_000),
|
||||
latestEvidence: evidenceRecap(input.newEvidence).map((item) => ({
|
||||
id: item.id,
|
||||
@@ -542,8 +551,8 @@ function nonScoringTurn(input: {
|
||||
&& latestIncomplete.eventSummary === "事件内容待补充"
|
||||
? { kind: "event_detail" as const, evidenceId: latestIncomplete.id }
|
||||
: null;
|
||||
const authoredRequest = authoredNarrative?.output.evidenceRequest;
|
||||
const priorRequest = input.current.latestTurn.evidenceRequest;
|
||||
const authoredRequest = authoredNarrative?.output.evidenceRequest;
|
||||
const priorRequest = input.current.latestTurn.evidenceRequest;
|
||||
const evidenceRequest = status === "confirming" && priorRequest === null
|
||||
? null
|
||||
: authoredRequest
|
||||
@@ -551,7 +560,7 @@ function nonScoringTurn(input: {
|
||||
domains: authoredRequest.domains,
|
||||
datePrecision: authoredRequest.datePrecision,
|
||||
freeTextAllowed: true as const,
|
||||
followUp: clarificationFollowUp ?? authoredRequest.followUp,
|
||||
followUp: authoredRequest.followUp,
|
||||
}
|
||||
: priorRequest
|
||||
? {
|
||||
@@ -786,6 +795,14 @@ export function createConversationalRectificationService(
|
||||
if (followUp?.kind !== "event_date" && followUp?.kind !== "event_detail") {
|
||||
return command.answer;
|
||||
}
|
||||
if (followUp.kind === "event_date" && affirmativeAnswerPattern.test(command.answer)) {
|
||||
const dates = [...current.latestTurn.narrative.matchAll(proposedDatePattern)];
|
||||
const proposed = dates.at(-1);
|
||||
if (proposed) {
|
||||
const [, year, month, day] = proposed;
|
||||
return `${year}年${Number(month)}月${day ? `${Number(day)}日` : ""}`;
|
||||
}
|
||||
}
|
||||
const activeEvidence = effectiveLifeEventEvidence(current.eventEvidence);
|
||||
const target = followUp.evidenceId
|
||||
? activeEvidence.find((item) => item.id === followUp.evidenceId)
|
||||
@@ -818,6 +835,33 @@ export function createConversationalRectificationService(
|
||||
return command.answer.replace(match[0], `${sameYear ? anchorYear : anchorYear + 1}年${month}月`);
|
||||
}
|
||||
|
||||
async function conversationContext(input: Readonly<{
|
||||
userId: string;
|
||||
current: LoadedConversationalRectificationCase;
|
||||
latestUserText: string;
|
||||
allEvidence: ReadonlyArray<LifeEventEvidenceInput>;
|
||||
newEvidence: ReadonlyArray<LifeEventEvidenceInput>;
|
||||
}>) {
|
||||
let recentConversation: ReadonlyArray<RectificationConversationMessage> = [{
|
||||
role: "assistant",
|
||||
text: input.current.latestTurn.narrative,
|
||||
}];
|
||||
if (ports.loadConversationMessages) {
|
||||
try {
|
||||
const loaded = await ports.loadConversationMessages(input.userId, input.current.caseId);
|
||||
if (loaded.length > 0) recentConversation = loaded;
|
||||
} catch {
|
||||
// Conversation history improves continuity but must not make a turn unavailable.
|
||||
}
|
||||
}
|
||||
return narrativeConversationContext({
|
||||
recentConversation: [...recentConversation, { role: "user", text: input.latestUserText }],
|
||||
latestUserText: input.latestUserText,
|
||||
allEvidence: input.allEvidence,
|
||||
newEvidence: input.newEvidence,
|
||||
});
|
||||
}
|
||||
|
||||
async function extractedEvidence(
|
||||
command: CommandOf<"answer">,
|
||||
current: LoadedConversationalRectificationCase,
|
||||
@@ -825,6 +869,9 @@ export function createConversationalRectificationService(
|
||||
let extracted: readonly LifeEventEvidence[];
|
||||
try {
|
||||
const answerForExtraction = contextualizedAnswer(command, current);
|
||||
if (affirmativeAnswerPattern.test(command.answer) && answerForExtraction === command.answer) {
|
||||
return [];
|
||||
}
|
||||
extracted = extractLifeEventEvidence({
|
||||
rawText: answerForExtraction,
|
||||
sourceTurnId: command.actionId,
|
||||
@@ -1226,7 +1273,9 @@ export function createConversationalRectificationService(
|
||||
phase: "intermediate",
|
||||
packet: gatedPacket,
|
||||
generator: ports.narrativeGenerator,
|
||||
context: narrativeConversationContext({
|
||||
context: await conversationContext({
|
||||
userId,
|
||||
current,
|
||||
latestUserText: command.answer,
|
||||
allEvidence: [...current.eventEvidence, ...evidence],
|
||||
newEvidence: evidence,
|
||||
@@ -1267,7 +1316,9 @@ export function createConversationalRectificationService(
|
||||
phase,
|
||||
packet: gatedPacket,
|
||||
generator: ports.narrativeGenerator,
|
||||
context: narrativeConversationContext({
|
||||
context: await conversationContext({
|
||||
userId,
|
||||
current,
|
||||
latestUserText: command.answer,
|
||||
allEvidence: [...current.eventEvidence, ...evidence],
|
||||
newEvidence: evidence,
|
||||
@@ -1325,7 +1376,9 @@ export function createConversationalRectificationService(
|
||||
phase: "intermediate",
|
||||
packet: gatedPacket,
|
||||
generator: ports.narrativeGenerator,
|
||||
context: narrativeConversationContext({
|
||||
context: await conversationContext({
|
||||
userId,
|
||||
current,
|
||||
latestUserText: command.answer,
|
||||
allEvidence: [...current.eventEvidence, ...evidence],
|
||||
newEvidence: evidence,
|
||||
@@ -1381,7 +1434,9 @@ export function createConversationalRectificationService(
|
||||
phase,
|
||||
packet: gatedPacket,
|
||||
generator: ports.narrativeGenerator,
|
||||
context: narrativeConversationContext({
|
||||
context: await conversationContext({
|
||||
userId,
|
||||
current,
|
||||
latestUserText: command.answer,
|
||||
allEvidence: [...current.eventEvidence, ...evidence],
|
||||
newEvidence: evidence,
|
||||
@@ -1460,7 +1515,9 @@ export function createConversationalRectificationService(
|
||||
phase,
|
||||
packet: gatedPacket,
|
||||
generator: ports.narrativeGenerator,
|
||||
context: latestEvidence ? narrativeConversationContext({
|
||||
context: latestEvidence ? await conversationContext({
|
||||
userId,
|
||||
current,
|
||||
latestUserText: latestEvidence.rawText,
|
||||
allEvidence: current.eventEvidence,
|
||||
newEvidence: [latestEvidence],
|
||||
|
||||
Reference in New Issue
Block a user