feat: advance reasoning after comparability is resolved
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { describeGraph } from "./builder.js";
|
||||
import {
|
||||
buildReasoningState,
|
||||
COMPARABILITY_REASONING_NODE_ID,
|
||||
formulateQuestion,
|
||||
formulateTieResolutionQuestion,
|
||||
} from "./question-formulator.js";
|
||||
@@ -426,7 +428,67 @@ function buildChangesApplied(proposal, affectedNodeIds) {
|
||||
};
|
||||
}
|
||||
|
||||
export function applyValidatedProposal({ situationGraph, proposal }) {
|
||||
function isComparabilityQuestion(question) {
|
||||
const text = String(question || "").toLowerCase();
|
||||
return (
|
||||
text.includes("same basis") ||
|
||||
text.includes("same scale") ||
|
||||
text.includes("same period")
|
||||
);
|
||||
}
|
||||
|
||||
function answerConfirmsComparability(answer) {
|
||||
const text = String(answer || "").toLowerCase();
|
||||
return (
|
||||
/\byes\b/.test(text) &&
|
||||
(text.includes("same accounting period") ||
|
||||
text.includes("same management accounts") ||
|
||||
text.includes("same basis") ||
|
||||
text.includes("same scale") ||
|
||||
text.includes("both figures cover the same"))
|
||||
);
|
||||
}
|
||||
|
||||
function deriveReasoningStateOverride({
|
||||
graph,
|
||||
previousQuestion,
|
||||
answer,
|
||||
resolvedUnknownNodeIds,
|
||||
}) {
|
||||
const previousReasoningState = buildReasoningState(graph);
|
||||
const previousComparabilityStatus =
|
||||
previousReasoningState.comparabilityStatus ?? null;
|
||||
|
||||
if (
|
||||
previousComparabilityStatus === "uncertain" &&
|
||||
isComparabilityQuestion(previousQuestion) &&
|
||||
answerConfirmsComparability(answer)
|
||||
) {
|
||||
return {
|
||||
reasoningStateOverride: {
|
||||
comparabilityStatus: "confirmed",
|
||||
comparabilityReason:
|
||||
"Comparability was confirmed by the user answer covering the same period and source basis.",
|
||||
comparabilityEvidence: resolvedUnknownNodeIds,
|
||||
},
|
||||
resolvedReasoningNodeIds: [COMPARABILITY_REASONING_NODE_ID],
|
||||
previousReasoningState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
reasoningStateOverride: {},
|
||||
resolvedReasoningNodeIds: [],
|
||||
previousReasoningState,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyValidatedProposal({
|
||||
situationGraph,
|
||||
proposal,
|
||||
previousQuestion = null,
|
||||
answer = null,
|
||||
}) {
|
||||
const graphValidation = situationGraphSchema.safeParse(situationGraph);
|
||||
const proposalValidation = graphUpdateSchema.safeParse(proposal);
|
||||
|
||||
@@ -574,6 +636,12 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
|
||||
const proposalSnapshot = cloneJsonSafe(validatedProposal);
|
||||
const previousActiveUnknownNodeId = graphSnapshot.activeUnknownNodeId ?? null;
|
||||
const affectedNodeIds = buildAffectedNodeIds(graphSnapshot, proposalSnapshot);
|
||||
const reasoningResolution = deriveReasoningStateOverride({
|
||||
graph: graphSnapshot,
|
||||
previousQuestion,
|
||||
answer,
|
||||
resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds,
|
||||
});
|
||||
|
||||
const applied = applyGraphUpdate(graphSnapshot, proposalSnapshot);
|
||||
if (!applied.success) {
|
||||
@@ -590,6 +658,11 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
|
||||
edges: applied.edges,
|
||||
resolvedNodeIds: applied.resolvedNodeIds,
|
||||
};
|
||||
const nextReasoningState = buildReasoningState(
|
||||
updatedSituationGraph,
|
||||
reasoningResolution.reasoningStateOverride,
|
||||
);
|
||||
updatedSituationGraph.reasoningState = nextReasoningState;
|
||||
|
||||
const activeUnknownWasResolved =
|
||||
previousActiveUnknownNodeId != null &&
|
||||
@@ -676,7 +749,21 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
|
||||
strategy: formulatedQuestion?.strategy,
|
||||
investigationStrategy: formulatedQuestion?.investigationStrategy,
|
||||
}
|
||||
: null;
|
||||
: (() => {
|
||||
const relationshipFallback = formulateTieResolutionQuestion({
|
||||
graph: updatedSituationGraph,
|
||||
});
|
||||
return relationshipFallback?.question
|
||||
? {
|
||||
nodeId: null,
|
||||
question: relationshipFallback.question,
|
||||
reason: relationshipFallback.reason,
|
||||
strategy: relationshipFallback.strategy,
|
||||
investigationStrategy:
|
||||
relationshipFallback.investigationStrategy,
|
||||
}
|
||||
: null;
|
||||
})();
|
||||
|
||||
const resultGraphValidation = situationGraphSchema.safeParse(
|
||||
updatedSituationGraph,
|
||||
@@ -725,10 +812,13 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
|
||||
graphUpdate: validatedProposal,
|
||||
affectedNodeIds,
|
||||
resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds,
|
||||
resolvedReasoningNodeIds: reasoningResolution.resolvedReasoningNodeIds,
|
||||
previousActiveUnknownNodeId,
|
||||
newActiveUnknownNodeId,
|
||||
selectedQuestion: finalSelectedQuestion,
|
||||
changesApplied: buildChangesApplied(validatedProposal, affectedNodeIds),
|
||||
graphReferenceValidation: resultReferenceValidation,
|
||||
previousReasoningState: reasoningResolution.previousReasoningState,
|
||||
reasoningState: nextReasoningState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
import { buildInitialGraph, describeGraph } from "./builder.js";
|
||||
import { applyValidatedProposal } from "./apply-proposal.js";
|
||||
import { buildGraphUpdatePrompt } from "./prompt-builder.js";
|
||||
import { formulateTieResolutionQuestion } from "./question-formulator.js";
|
||||
import {
|
||||
buildReasoningState,
|
||||
formulateTieResolutionQuestion,
|
||||
} from "./question-formulator.js";
|
||||
import { parseGraphUpdateProposal } from "./update-proposal.js";
|
||||
import {
|
||||
explainUnknownSelection,
|
||||
@@ -83,6 +86,9 @@ function buildUpdateDiagnostics({
|
||||
graphReferenceValidation,
|
||||
selectedQuestion,
|
||||
unknownSelectionExplanation,
|
||||
previousReasoningState,
|
||||
reasoningState,
|
||||
resolvedReasoningNodeIds,
|
||||
}) {
|
||||
return {
|
||||
promptVersion: promptVersion ?? "v0.4",
|
||||
@@ -100,6 +106,14 @@ function buildUpdateDiagnostics({
|
||||
selectedQuestion?.investigationStrategy ??
|
||||
selectedQuestion?.strategy ??
|
||||
null,
|
||||
previousComparabilityStatus:
|
||||
previousReasoningState?.comparabilityStatus ?? null,
|
||||
comparabilityStatus: reasoningState?.comparabilityStatus ?? null,
|
||||
relationshipStatus: reasoningState?.relationshipStatus ?? null,
|
||||
relationshipAssessed: reasoningState?.relationshipAssessed ?? null,
|
||||
reasoningStagesBefore: previousReasoningState?.reasoningStages ?? [],
|
||||
reasoningStagesAfter: reasoningState?.reasoningStages ?? [],
|
||||
resolvedReasoningNodeIds: resolvedReasoningNodeIds ?? [],
|
||||
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
||||
};
|
||||
}
|
||||
@@ -159,6 +173,12 @@ export async function startCase(body) {
|
||||
activeUnknownNodeId,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary,
|
||||
reasoningState: buildReasoningState({
|
||||
centralStatement: scenario,
|
||||
nodes: initialGraph.nodes,
|
||||
edges: initialGraph.edges,
|
||||
resolvedNodeIds: [],
|
||||
}),
|
||||
});
|
||||
|
||||
situationGraphSchema.parse(situationGraph);
|
||||
@@ -322,6 +342,8 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
const applicationResult = applyProposalUpdate({
|
||||
situationGraph,
|
||||
proposal: parsedProposal.proposal,
|
||||
previousQuestion,
|
||||
answer,
|
||||
});
|
||||
|
||||
if (!applicationResult.success) {
|
||||
@@ -338,6 +360,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
graph: situationGraph,
|
||||
graphReferenceValidation: graphReferenceValidation,
|
||||
selectedQuestion: null,
|
||||
previousReasoningState: buildReasoningState(situationGraph),
|
||||
reasoningState: buildReasoningState(situationGraph),
|
||||
resolvedReasoningNodeIds: [],
|
||||
unknownSelectionExplanation: explainUnknownSelection(
|
||||
situationGraph,
|
||||
situationGraph.resolvedNodeIds || [],
|
||||
@@ -372,6 +397,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
graph: applicationResult.updatedSituationGraph,
|
||||
graphReferenceValidation: applicationResult.graphReferenceValidation,
|
||||
selectedQuestion: applicationResult.selectedQuestion,
|
||||
previousReasoningState: applicationResult.previousReasoningState,
|
||||
reasoningState: applicationResult.reasoningState,
|
||||
resolvedReasoningNodeIds: applicationResult.resolvedReasoningNodeIds,
|
||||
unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
|
||||
applicationResult.updatedSituationGraph,
|
||||
applicationResult.updatedSituationGraph.resolvedNodeIds || [],
|
||||
@@ -393,6 +421,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
graph: situationGraph,
|
||||
graphReferenceValidation,
|
||||
selectedQuestion: null,
|
||||
previousReasoningState: buildReasoningState(situationGraph),
|
||||
reasoningState: buildReasoningState(situationGraph),
|
||||
resolvedReasoningNodeIds: [],
|
||||
unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
|
||||
situationGraph,
|
||||
situationGraph.resolvedNodeIds || [],
|
||||
|
||||
@@ -152,7 +152,28 @@ function analyseObservationText(text) {
|
||||
};
|
||||
}
|
||||
|
||||
export const COMPARABILITY_REASONING_NODE_ID = "reasoning:comparability";
|
||||
|
||||
function readStoredComparabilityState(graph) {
|
||||
const reasoningState = graph?.reasoningState;
|
||||
if (!reasoningState?.comparabilityStatus) return null;
|
||||
|
||||
return {
|
||||
comparabilityStatus: reasoningState.comparabilityStatus,
|
||||
reason:
|
||||
reasoningState.comparabilityReason ||
|
||||
"Comparability state was carried forward from earlier reasoning.",
|
||||
contradictionReasoningAllowed:
|
||||
reasoningState.comparabilityStatus === "confirmed",
|
||||
};
|
||||
}
|
||||
|
||||
export function assessComparability(graph) {
|
||||
const storedState = readStoredComparabilityState(graph);
|
||||
if (storedState) {
|
||||
return storedState;
|
||||
}
|
||||
|
||||
const observations = collectObservationNodes(graph);
|
||||
const centralText = normaliseText(graph?.centralStatement || "");
|
||||
const profiles = observations.map((node) =>
|
||||
@@ -308,6 +329,9 @@ function classifyObservationRelationshipWhenComparable(graph) {
|
||||
const directions = profiles.map((profile) =>
|
||||
extractObservationDirection(profile),
|
||||
);
|
||||
const conceptUnion = new Set(conceptSets.flat());
|
||||
const hasRevenueCashPair =
|
||||
conceptUnion.has("revenue") && conceptUnion.has("cash");
|
||||
|
||||
if (
|
||||
sharedConcepts.size > 0 &&
|
||||
@@ -336,6 +360,19 @@ function classifyObservationRelationshipWhenComparable(graph) {
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
hasRevenueCashPair &&
|
||||
directions.every((direction) => direction !== "unknown")
|
||||
) {
|
||||
return {
|
||||
relationshipStatus: "potentially_related",
|
||||
reason:
|
||||
"The observations concern connected business signals but do not establish a direct contradiction or cause.",
|
||||
contradictionReasoningAllowed: false,
|
||||
questionRequired: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
sharedConcepts.size === 0 &&
|
||||
directions.every((direction) => direction !== "unknown")
|
||||
@@ -396,6 +433,30 @@ export function classifyObservationRelationship(graph) {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildReasoningState(graph, overrides = {}) {
|
||||
const relationship = classifyObservationRelationship({
|
||||
...graph,
|
||||
reasoningState: {
|
||||
...(graph?.reasoningState || {}),
|
||||
...(overrides || {}),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
comparabilityStatus: relationship.reasoningStages[0]?.status ?? null,
|
||||
comparabilityReason: relationship.reasoningStages[0]?.outcome ?? null,
|
||||
comparabilityEvidence:
|
||||
overrides.comparabilityEvidence ??
|
||||
graph?.reasoningState?.comparabilityEvidence ??
|
||||
[],
|
||||
relationshipStatus: relationship.relationshipStatus,
|
||||
relationshipReason: relationship.reason,
|
||||
relationshipAssessed: relationship.relationshipAssessed,
|
||||
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
|
||||
reasoningStages: relationship.reasoningStages,
|
||||
};
|
||||
}
|
||||
|
||||
function buildComparabilityQuestion(graph, assessment) {
|
||||
const centralText = normaliseText(graph?.centralStatement || "");
|
||||
const mentionsPeriod =
|
||||
@@ -430,6 +491,13 @@ function detectContradictionContext(graph) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildBroadInvestigationQuestion(graph) {
|
||||
const central = sanitizeQuestionText(
|
||||
stripTrailingPunctuation(graph?.centralStatement || "these observations"),
|
||||
);
|
||||
return `What changed during that period that could help explain why ${central}?`;
|
||||
}
|
||||
|
||||
export function formulateTieResolutionQuestion({ graph }) {
|
||||
const comparability = assessComparability(graph);
|
||||
if (comparability.comparabilityStatus === "uncertain") {
|
||||
@@ -475,8 +543,7 @@ export function formulateTieResolutionQuestion({ graph }) {
|
||||
|
||||
if (relationship.relationshipStatus === "potentially_related") {
|
||||
return {
|
||||
question:
|
||||
"What connection, if any, should we check between these observations?",
|
||||
question: buildBroadInvestigationQuestion(graph),
|
||||
reason:
|
||||
"Formulated as a neutral relationship question because the observations may be related without being contradictory.",
|
||||
strategy: null,
|
||||
|
||||
@@ -84,6 +84,25 @@ export const situationEdgeSchema = z.object({
|
||||
|
||||
// ── SituationGraph ───────────────────────────────────
|
||||
|
||||
const reasoningStageSchema = z.object({
|
||||
stage: z.string().min(1),
|
||||
status: z.string().min(1),
|
||||
outcome: z.string().min(1),
|
||||
});
|
||||
|
||||
export const reasoningStateSchema = z
|
||||
.object({
|
||||
comparabilityStatus: z.string().min(1).nullable().optional(),
|
||||
comparabilityReason: z.string().min(1).nullable().optional(),
|
||||
comparabilityEvidence: z.array(z.string()).default([]),
|
||||
relationshipStatus: z.string().min(1).nullable().optional(),
|
||||
relationshipReason: z.string().min(1).nullable().optional(),
|
||||
relationshipAssessed: z.boolean().optional(),
|
||||
contradictionReasoningAllowed: z.boolean().optional(),
|
||||
reasoningStages: z.array(reasoningStageSchema).default([]),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const situationGraphSchema = z.object({
|
||||
centralStatement: z.string().min(1),
|
||||
nodes: z.array(situationNodeSchema).min(1),
|
||||
@@ -91,6 +110,7 @@ export const situationGraphSchema = z.object({
|
||||
activeUnknownNodeId: z.string().nullable(),
|
||||
resolvedNodeIds: z.array(z.string()).default([]),
|
||||
currentSummary: z.string().min(1),
|
||||
reasoningState: reasoningStateSchema.optional(),
|
||||
});
|
||||
|
||||
/** @typedef {z.infer<typeof situationGraphSchema>} SituationGraph */
|
||||
@@ -201,5 +221,6 @@ export function makeGraph(opts) {
|
||||
activeUnknownNodeId: opts.activeUnknownNodeId ?? null,
|
||||
resolvedNodeIds: opts.resolvedNodeIds ?? [],
|
||||
currentSummary: opts.currentSummary || "",
|
||||
reasoningState: opts.reasoningState,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user