140 lines
4.6 KiB
JavaScript
140 lines
4.6 KiB
JavaScript
import { formulateQuestion } from "@/lib/graph/question-formulator.js";
|
|
|
|
const FOCUSED_ANSWER_SCHEMA_FIELDS = [
|
|
"targetNodeId",
|
|
"observations",
|
|
"uncertainties",
|
|
"assumptions",
|
|
"relationships",
|
|
"possibleFollowUpQuestions",
|
|
];
|
|
|
|
const FORBIDDEN_GRAPH_MUTATION_FIELDS = [
|
|
"addedNodes",
|
|
"updatedNodes",
|
|
"removedNodes",
|
|
"addedEdges",
|
|
"removedEdges",
|
|
"resolvedNodeIds",
|
|
"activeUnknownNodeId",
|
|
"selectedQuestion",
|
|
];
|
|
|
|
/**
|
|
* Formulate a question for the explicitly user-selected unresolved node.
|
|
*
|
|
* Input: { situationGraph, targetNodeId }
|
|
* Validates that target exists, is an unknown, and is not resolved.
|
|
* Calls formulateQuestion with only factual explicit-user-targeting context.
|
|
* Does NOT invoke selectActiveUnknownCandidate, determineGraphBackedQuestion,
|
|
* global ranking, or global recommendation.
|
|
* Does NOT mutate activeUnknownNodeId, selectedQuestion, SituationGraph, or resolvedNodeIds.
|
|
*/
|
|
export function formulateQuestionForTarget({ situationGraph, targetNodeId }) {
|
|
const nodesById = new Map(situationGraph?.nodes?.map((n) => [n.id, n]) || []);
|
|
const targetNode = nodesById.get(targetNodeId);
|
|
|
|
if (!targetNode) {
|
|
return { success: false, error: `Target node ${targetNodeId} not found in graph.` };
|
|
}
|
|
|
|
if (targetNode.kind !== "unknown") {
|
|
return { success: false, error: `Target node ${targetNodeId} is not an unknown (kind=${targetNode.kind}).` };
|
|
}
|
|
|
|
if (targetNode.status === "resolved") {
|
|
return { success: false, error: `Target node ${targetNodeId} is already resolved.` };
|
|
}
|
|
|
|
// Build explicit user-targeting context — only factual fields from the targeting request.
|
|
// No activeUnknownNodeId override, no selectedQuestion mutation, no global selection state.
|
|
const context = {
|
|
resolvedValues: [],
|
|
suppressDecisionSufficiencyConfirmation: false,
|
|
_explicitTargetNodeId: targetNodeId,
|
|
_explicitTargetLabel: targetNode.label,
|
|
_explicitTargetDescription: targetNode.description,
|
|
};
|
|
|
|
const result = formulateQuestion({ node: targetNode, graph: situationGraph, context });
|
|
|
|
return {
|
|
success: true,
|
|
targetNodeId,
|
|
question: result.question,
|
|
strategy: result.strategy ?? null,
|
|
reasoningPattern: result.reasoningPattern,
|
|
reasoningPatternReason: result.reasoningPatternReason,
|
|
reason: result.reason,
|
|
questionFamily: result.questionFamily,
|
|
selectedQuestionTemplate: result.selectedQuestionTemplate,
|
|
allowedQuestionFamilies: result.allowedQuestionFamilies,
|
|
rejectedQuestionFamilies: result.rejectedQuestionFamilies,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build a focused answer deconstruction prompt for one explicitly user-chosen investigation.
|
|
*/
|
|
export function buildFocusedDeconstructPrompt({ targetLabel, targetDescription, centralStatement, question, answer }) {
|
|
return `You are performing focused answer deconstruction for one explicitly user-chosen investigation.
|
|
|
|
Return exactly one JSON object. Return JSON only.
|
|
|
|
This is NOT a graph update task.
|
|
Do NOT output graph mutations.
|
|
Do NOT output selection, ranking, ownership, recommendation, confidence, or next-best-question semantics.
|
|
Do NOT include any of these fields: addedNodes, updatedNodes, removedNodes, addedEdges, removedEdges, resolvedNodeIds, activeUnknownNodeId, selectedQuestion.
|
|
|
|
Required top-level fields:
|
|
- targetNodeId
|
|
- observations
|
|
- uncertainties
|
|
- assumptions
|
|
- relationships
|
|
- possibleFollowUpQuestions
|
|
|
|
Field rules:
|
|
- targetNodeId must be included as a string identifying this investigation node
|
|
- observations: only statements directly supported by the answer
|
|
- uncertainties: only things the answer explicitly leaves unknown or unclear
|
|
- assumptions: include only if the answer itself relies on an assumption
|
|
- relationships: only direct supported relationships among extracted items, each with { from, to, type, rationale }
|
|
- possibleFollowUpQuestions: unresolved questions genuinely exposed by this answer, unranked
|
|
|
|
Focused case context:
|
|
- target label: ${targetLabel}
|
|
- target description: ${targetDescription}
|
|
- central case statement: ${centralStatement}
|
|
|
|
Question:
|
|
${question}
|
|
|
|
Answer:
|
|
${answer}`;
|
|
}
|
|
|
|
/**
|
|
* Validate that a focused-answer deconstruction result contains only the expected fields.
|
|
* Returns an array of errors (empty = valid).
|
|
*/
|
|
export function validateFocusedDeconstructSchema(result) {
|
|
const errors = [];
|
|
|
|
for (const field of FOCUSED_ANSWER_SCHEMA_FIELDS) {
|
|
if (!(field in result)) {
|
|
errors.push(`Missing required field: ${field}`);
|
|
}
|
|
}
|
|
|
|
for (const field of FORBIDDEN_GRAPH_MUTATION_FIELDS) {
|
|
if (field in result) {
|
|
errors.push(`Forbidden graph-mutation field present: ${field}`);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
export { FOCUSED_ANSWER_SCHEMA_FIELDS, FORBIDDEN_GRAPH_MUTATION_FIELDS };
|