feat: back next questions with explicit graph unknowns
This commit is contained in:
+163
-21
@@ -1,11 +1,15 @@
|
||||
import { describeGraph } from "./builder.js";
|
||||
import {
|
||||
buildReasoningState,
|
||||
classifyObservationRelationship,
|
||||
COMPARABILITY_REASONING_NODE_ID,
|
||||
formulateQuestion,
|
||||
formulateTieResolutionQuestion,
|
||||
} from "./question-formulator.js";
|
||||
import { graphUpdateSchema, situationGraphSchema } from "./schema.js";
|
||||
import {
|
||||
graphUpdateSchema,
|
||||
makeNodeId,
|
||||
situationGraphSchema,
|
||||
} from "./schema.js";
|
||||
import {
|
||||
applyGraphUpdate,
|
||||
detectDuplicateNodeIds,
|
||||
@@ -428,6 +432,124 @@ function buildChangesApplied(proposal, affectedNodeIds) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildEmergentReasoningUnknownLabel(graph) {
|
||||
const central = String(graph?.centralStatement || "these observations")
|
||||
.trim()
|
||||
.replace(/[.?!:;]+$/g, "");
|
||||
return `Explanation for why ${central}`;
|
||||
}
|
||||
|
||||
function findEquivalentEmergentUnknown(graph, label, description) {
|
||||
const targetId = makeNodeId(label);
|
||||
const targetTexts = [normaliseText(label), normaliseText(description)].filter(
|
||||
Boolean,
|
||||
);
|
||||
|
||||
return (graph.nodes || []).find((node) => {
|
||||
if (
|
||||
node.kind !== "unknown" ||
|
||||
(graph.resolvedNodeIds || []).includes(node.id)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.id === targetId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nodeTexts = [
|
||||
normaliseText(node.label),
|
||||
normaliseText(node.description),
|
||||
].filter(Boolean);
|
||||
|
||||
return targetTexts.some((text) => nodeTexts.includes(text));
|
||||
});
|
||||
}
|
||||
|
||||
function buildEmergentReasoningUnknown(graph, relationshipAssessment) {
|
||||
if (!relationshipAssessment?.relationshipAssessed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!relationshipAssessment.questionRequired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
![
|
||||
"potentially_related",
|
||||
"insufficient_information",
|
||||
"contradictory",
|
||||
].includes(relationshipAssessment.relationshipStatus)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = buildEmergentReasoningUnknownLabel(graph);
|
||||
const description =
|
||||
"Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.";
|
||||
const existingNode = findEquivalentEmergentUnknown(graph, label, description);
|
||||
if (existingNode) {
|
||||
return {
|
||||
created: false,
|
||||
node: existingNode,
|
||||
edges: [],
|
||||
reason:
|
||||
"Reused an existing unresolved reasoning unknown for the next investigation stage.",
|
||||
};
|
||||
}
|
||||
|
||||
const observationNodes = (graph.nodes || []).filter(
|
||||
(node) => node.kind === "observation" && node.status === "supported",
|
||||
);
|
||||
const relationshipNode = (graph.nodes || []).find(
|
||||
(node) => node.kind === "relationship" && node.status === "supported",
|
||||
);
|
||||
const nodeId = makeNodeId(label);
|
||||
const relatedNodeIds = relationshipNode
|
||||
? [relationshipNode.id]
|
||||
: observationNodes.slice(0, 2).map((node) => node.id);
|
||||
|
||||
if (relatedNodeIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: nodeId,
|
||||
label,
|
||||
description,
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
value: null,
|
||||
unit: null,
|
||||
evidenceIds: [],
|
||||
dependsOn: relatedNodeIds,
|
||||
affects: [],
|
||||
parentId: relationshipNode?.id ?? null,
|
||||
childIds: [],
|
||||
};
|
||||
|
||||
const edges = relatedNodeIds.map((relatedNodeId) => ({
|
||||
id: `e-${relatedNodeId.slice(0, 6)}-${nodeId.slice(0, 6)}`,
|
||||
fromNodeId: relatedNodeId,
|
||||
toNodeId: nodeId,
|
||||
relationship:
|
||||
relationshipNode?.id === relatedNodeId ? "depends_on" : "other",
|
||||
confidence: "medium",
|
||||
description:
|
||||
"This unresolved explanation arises from the now-assessed relationship between the observations.",
|
||||
}));
|
||||
|
||||
return {
|
||||
created: true,
|
||||
node,
|
||||
edges,
|
||||
reason:
|
||||
"Created a new unresolved reasoning unknown so the next justified question is backed by the graph.",
|
||||
};
|
||||
}
|
||||
|
||||
function isComparabilityQuestion(question) {
|
||||
const text = String(question || "").toLowerCase();
|
||||
return (
|
||||
@@ -643,6 +765,36 @@ export function applyValidatedProposal({
|
||||
resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds,
|
||||
});
|
||||
|
||||
const provisionalApplied = applyGraphUpdate(graphSnapshot, proposalSnapshot);
|
||||
if (!provisionalApplied.success) {
|
||||
return {
|
||||
success: false,
|
||||
stage: "application",
|
||||
errors: provisionalApplied.errors,
|
||||
};
|
||||
}
|
||||
const provisionalGraph = {
|
||||
...graphSnapshot,
|
||||
nodes: provisionalApplied.nodes,
|
||||
edges: provisionalApplied.edges,
|
||||
resolvedNodeIds: provisionalApplied.resolvedNodeIds,
|
||||
};
|
||||
provisionalGraph.reasoningState = buildReasoningState(
|
||||
provisionalGraph,
|
||||
reasoningResolution.reasoningStateOverride,
|
||||
);
|
||||
const relationshipAssessment =
|
||||
classifyObservationRelationship(provisionalGraph);
|
||||
const emergentReasoningUnknown = buildEmergentReasoningUnknown(
|
||||
provisionalGraph,
|
||||
relationshipAssessment,
|
||||
);
|
||||
|
||||
if (emergentReasoningUnknown?.created) {
|
||||
proposalSnapshot.addedNodes.push(emergentReasoningUnknown.node);
|
||||
proposalSnapshot.addedEdges.push(...emergentReasoningUnknown.edges);
|
||||
}
|
||||
|
||||
const applied = applyGraphUpdate(graphSnapshot, proposalSnapshot);
|
||||
if (!applied.success) {
|
||||
return {
|
||||
@@ -738,7 +890,8 @@ export function applyValidatedProposal({
|
||||
? {
|
||||
nodeId: null,
|
||||
tiedCandidateIds: deterministicSelection.tiedCandidateIds,
|
||||
...formulateTieResolutionQuestion({ graph: updatedSituationGraph }),
|
||||
question: null,
|
||||
reason: deterministicSelection.reason,
|
||||
}
|
||||
: deterministicSelection?.status === "selected"
|
||||
? {
|
||||
@@ -749,21 +902,7 @@ export function applyValidatedProposal({
|
||||
strategy: formulatedQuestion?.strategy,
|
||||
investigationStrategy: formulatedQuestion?.investigationStrategy,
|
||||
}
|
||||
: (() => {
|
||||
const relationshipFallback = formulateTieResolutionQuestion({
|
||||
graph: updatedSituationGraph,
|
||||
});
|
||||
return relationshipFallback?.question
|
||||
? {
|
||||
nodeId: null,
|
||||
question: relationshipFallback.question,
|
||||
reason: relationshipFallback.reason,
|
||||
strategy: relationshipFallback.strategy,
|
||||
investigationStrategy:
|
||||
relationshipFallback.investigationStrategy,
|
||||
}
|
||||
: null;
|
||||
})();
|
||||
: null;
|
||||
|
||||
const resultGraphValidation = situationGraphSchema.safeParse(
|
||||
updatedSituationGraph,
|
||||
@@ -809,14 +948,17 @@ export function applyValidatedProposal({
|
||||
return {
|
||||
success: true,
|
||||
updatedSituationGraph,
|
||||
graphUpdate: validatedProposal,
|
||||
graphUpdate: proposalSnapshot,
|
||||
affectedNodeIds,
|
||||
resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds,
|
||||
resolvedUnknownNodeIds: proposalSnapshot.resolvedUnknownNodeIds,
|
||||
resolvedReasoningNodeIds: reasoningResolution.resolvedReasoningNodeIds,
|
||||
emergentReasoningNodeCreated: Boolean(emergentReasoningUnknown?.created),
|
||||
emergentReasoningNodeId: emergentReasoningUnknown?.node?.id ?? null,
|
||||
emergentReasoningNodeReason: emergentReasoningUnknown?.reason ?? null,
|
||||
previousActiveUnknownNodeId,
|
||||
newActiveUnknownNodeId,
|
||||
selectedQuestion: finalSelectedQuestion,
|
||||
changesApplied: buildChangesApplied(validatedProposal, affectedNodeIds),
|
||||
changesApplied: buildChangesApplied(proposalSnapshot, affectedNodeIds),
|
||||
graphReferenceValidation: resultReferenceValidation,
|
||||
previousReasoningState: reasoningResolution.previousReasoningState,
|
||||
reasoningState: nextReasoningState,
|
||||
|
||||
@@ -89,6 +89,9 @@ function buildUpdateDiagnostics({
|
||||
previousReasoningState,
|
||||
reasoningState,
|
||||
resolvedReasoningNodeIds,
|
||||
emergentReasoningNodeCreated,
|
||||
emergentReasoningNodeId,
|
||||
emergentReasoningNodeReason,
|
||||
}) {
|
||||
return {
|
||||
promptVersion: promptVersion ?? "v0.4",
|
||||
@@ -114,6 +117,9 @@ function buildUpdateDiagnostics({
|
||||
reasoningStagesBefore: previousReasoningState?.reasoningStages ?? [],
|
||||
reasoningStagesAfter: reasoningState?.reasoningStages ?? [],
|
||||
resolvedReasoningNodeIds: resolvedReasoningNodeIds ?? [],
|
||||
emergentReasoningNodeCreated: emergentReasoningNodeCreated ?? false,
|
||||
emergentReasoningNodeId: emergentReasoningNodeId ?? null,
|
||||
emergentReasoningNodeReason: emergentReasoningNodeReason ?? null,
|
||||
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
||||
};
|
||||
}
|
||||
@@ -363,6 +369,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
previousReasoningState: buildReasoningState(situationGraph),
|
||||
reasoningState: buildReasoningState(situationGraph),
|
||||
resolvedReasoningNodeIds: [],
|
||||
emergentReasoningNodeCreated: false,
|
||||
emergentReasoningNodeId: null,
|
||||
emergentReasoningNodeReason: null,
|
||||
unknownSelectionExplanation: explainUnknownSelection(
|
||||
situationGraph,
|
||||
situationGraph.resolvedNodeIds || [],
|
||||
@@ -400,6 +409,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
previousReasoningState: applicationResult.previousReasoningState,
|
||||
reasoningState: applicationResult.reasoningState,
|
||||
resolvedReasoningNodeIds: applicationResult.resolvedReasoningNodeIds,
|
||||
emergentReasoningNodeCreated:
|
||||
applicationResult.emergentReasoningNodeCreated,
|
||||
emergentReasoningNodeId: applicationResult.emergentReasoningNodeId,
|
||||
emergentReasoningNodeReason:
|
||||
applicationResult.emergentReasoningNodeReason,
|
||||
unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
|
||||
applicationResult.updatedSituationGraph,
|
||||
applicationResult.updatedSituationGraph.resolvedNodeIds || [],
|
||||
@@ -424,6 +438,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
previousReasoningState: buildReasoningState(situationGraph),
|
||||
reasoningState: buildReasoningState(situationGraph),
|
||||
resolvedReasoningNodeIds: [],
|
||||
emergentReasoningNodeCreated: false,
|
||||
emergentReasoningNodeId: null,
|
||||
emergentReasoningNodeReason: null,
|
||||
unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
|
||||
situationGraph,
|
||||
situationGraph.resolvedNodeIds || [],
|
||||
|
||||
@@ -498,6 +498,16 @@ function buildBroadInvestigationQuestion(graph) {
|
||||
return `What changed during that period that could help explain why ${central}?`;
|
||||
}
|
||||
|
||||
function isRelationshipExplanationUnknown(node, graph) {
|
||||
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
||||
return (
|
||||
collectObservationNodes(graph).length >= 2 &&
|
||||
/\b(explain|explanation|divergence|moved differently|difference between|change or event|what changed|why the observations)/.test(
|
||||
text,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function formulateTieResolutionQuestion({ graph }) {
|
||||
const comparability = assessComparability(graph);
|
||||
if (comparability.comparabilityStatus === "uncertain") {
|
||||
@@ -909,7 +919,9 @@ export function formulateQuestion({ node, graph, context = {} }) {
|
||||
|
||||
let question = investigationStrategy
|
||||
? buildQuestionFromStrategy(investigationStrategy)
|
||||
: buildNeutralClarificationQuestion(extractMeaning(node));
|
||||
: isRelationshipExplanationUnknown(node, graph)
|
||||
? buildBroadInvestigationQuestion(graph)
|
||||
: buildNeutralClarificationQuestion(extractMeaning(node));
|
||||
|
||||
question = sanitizeQuestionText(question);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user