Stage 2 завершён: problem-first ответы и follow-up continuity - ассистент переведён от entity-heavy логики к problem-first ответам с problem-unit слоем, удержанием контекста в follow-up и очисткой пользовательского ответа от сырых технических ссылок.
This commit is contained in:
@@ -16,9 +16,21 @@ import {
|
||||
INVESTIGATION_MAX_UNCERTAINTIES,
|
||||
INVESTIGATION_STATE_SCHEMA_VERSION
|
||||
} from "../types/stage1Contracts";
|
||||
import type {
|
||||
InvestigationProblemUnitState,
|
||||
InvestigationStateWithProblemUnits,
|
||||
ProblemUnit,
|
||||
ProblemUnitEntityBacklink
|
||||
} from "../types/stage2ProblemUnits";
|
||||
import {
|
||||
INVESTIGATION_MAX_ACTIVE_PROBLEM_UNITS,
|
||||
INVESTIGATION_MAX_FOCUS_PROBLEM_TYPES,
|
||||
INVESTIGATION_MAX_PROBLEM_UNIT_BACKLINKS,
|
||||
INVESTIGATION_MAX_RESOLVED_PROBLEM_UNITS
|
||||
} from "../types/stage2ProblemUnits";
|
||||
|
||||
interface UpdateInvestigationStateInput {
|
||||
previous: InvestigationState;
|
||||
previous: InvestigationStateWithProblemUnits;
|
||||
timestamp: string;
|
||||
questionId: string;
|
||||
userMessage: string;
|
||||
@@ -115,9 +127,142 @@ function collectOpenUncertainties(
|
||||
return capStrings([...requirementNotes, ...limitationNotes], INVESTIGATION_MAX_UNCERTAINTIES);
|
||||
}
|
||||
|
||||
export function cloneInvestigationState(state: InvestigationState | null): InvestigationState | null {
|
||||
if (!state) return null;
|
||||
function normalizeEntityBacklinks(values: ProblemUnitEntityBacklink[]): ProblemUnitEntityBacklink[] {
|
||||
const result: ProblemUnitEntityBacklink[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of values) {
|
||||
const entity = String(item.entity ?? "").trim();
|
||||
const id = String(item.id ?? "").trim();
|
||||
if (!entity || !id) {
|
||||
continue;
|
||||
}
|
||||
const key = `${entity}::${id}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
result.push({
|
||||
entity,
|
||||
id
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectProblemUnits(retrievalResults: UnifiedRetrievalResult[]): ProblemUnit[] {
|
||||
return retrievalResults.flatMap((result) => result.problem_units ?? []);
|
||||
}
|
||||
|
||||
function capProblemUnitState(state: InvestigationProblemUnitState): InvestigationProblemUnitState {
|
||||
return {
|
||||
active_problem_units: capStrings(state.active_problem_units, INVESTIGATION_MAX_ACTIVE_PROBLEM_UNITS),
|
||||
resolved_problem_units: capStrings(state.resolved_problem_units, INVESTIGATION_MAX_RESOLVED_PROBLEM_UNITS),
|
||||
problem_unit_backlinks: state.problem_unit_backlinks
|
||||
.map((item) => ({
|
||||
problem_unit_id: String(item.problem_unit_id ?? "").trim(),
|
||||
entity_backlinks: normalizeEntityBacklinks(item.entity_backlinks ?? [])
|
||||
}))
|
||||
.filter((item) => Boolean(item.problem_unit_id) && item.entity_backlinks.length > 0)
|
||||
.slice(0, INVESTIGATION_MAX_PROBLEM_UNIT_BACKLINKS),
|
||||
focus_problem_types: capStrings(
|
||||
state.focus_problem_types.map((item) => String(item)),
|
||||
INVESTIGATION_MAX_FOCUS_PROBLEM_TYPES
|
||||
) as InvestigationProblemUnitState["focus_problem_types"]
|
||||
};
|
||||
}
|
||||
|
||||
function updateProblemUnitState(
|
||||
previous: InvestigationStateWithProblemUnits,
|
||||
retrievalResults: UnifiedRetrievalResult[]
|
||||
): InvestigationProblemUnitState | undefined {
|
||||
const previousState = previous.problem_unit_state;
|
||||
const currentProblemUnits = collectProblemUnits(retrievalResults);
|
||||
const currentIds = capStrings(
|
||||
currentProblemUnits.map((item) => String(item.problem_unit_id ?? "")),
|
||||
INVESTIGATION_MAX_ACTIVE_PROBLEM_UNITS
|
||||
);
|
||||
const currentTypes = capStrings(
|
||||
currentProblemUnits.map((item) => String(item.problem_unit_type ?? "")),
|
||||
INVESTIGATION_MAX_FOCUS_PROBLEM_TYPES
|
||||
) as InvestigationProblemUnitState["focus_problem_types"];
|
||||
|
||||
const currentBacklinksRaw = currentProblemUnits
|
||||
.filter((item) => currentIds.includes(item.problem_unit_id))
|
||||
.map((item) => ({
|
||||
problem_unit_id: item.problem_unit_id,
|
||||
entity_backlinks: normalizeEntityBacklinks(item.entity_backlinks ?? [])
|
||||
}))
|
||||
.filter((item) => item.entity_backlinks.length > 0);
|
||||
|
||||
const currentBacklinksById = new Map(
|
||||
currentBacklinksRaw.map((item) => [item.problem_unit_id, item.entity_backlinks] as const)
|
||||
);
|
||||
const previousBacklinksById = new Map(
|
||||
(previousState?.problem_unit_backlinks ?? []).map((item) => [item.problem_unit_id, item.entity_backlinks] as const)
|
||||
);
|
||||
|
||||
const active_problem_units =
|
||||
currentIds.length > 0
|
||||
? currentIds
|
||||
: capStrings(previousState?.active_problem_units ?? [], INVESTIGATION_MAX_ACTIVE_PROBLEM_UNITS);
|
||||
|
||||
const resolved_problem_units =
|
||||
currentIds.length > 0
|
||||
? capStrings(
|
||||
[
|
||||
...(previousState?.active_problem_units ?? []).filter((item) => !currentIds.includes(item)),
|
||||
...(previousState?.resolved_problem_units ?? [])
|
||||
],
|
||||
INVESTIGATION_MAX_RESOLVED_PROBLEM_UNITS
|
||||
)
|
||||
: capStrings(previousState?.resolved_problem_units ?? [], INVESTIGATION_MAX_RESOLVED_PROBLEM_UNITS);
|
||||
|
||||
const problem_unit_backlinks = active_problem_units
|
||||
.map((problemUnitId) => {
|
||||
const entity_backlinks = normalizeEntityBacklinks(
|
||||
currentBacklinksById.get(problemUnitId) ?? previousBacklinksById.get(problemUnitId) ?? []
|
||||
);
|
||||
if (entity_backlinks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
problem_unit_id: problemUnitId,
|
||||
entity_backlinks
|
||||
};
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== null)
|
||||
.slice(0, INVESTIGATION_MAX_PROBLEM_UNIT_BACKLINKS);
|
||||
|
||||
const focus_problem_types =
|
||||
currentTypes.length > 0
|
||||
? currentTypes
|
||||
: capStrings(
|
||||
(previousState?.focus_problem_types ?? []).map((item) => String(item)),
|
||||
INVESTIGATION_MAX_FOCUS_PROBLEM_TYPES
|
||||
) as InvestigationProblemUnitState["focus_problem_types"];
|
||||
|
||||
const nextState = capProblemUnitState({
|
||||
active_problem_units,
|
||||
resolved_problem_units,
|
||||
problem_unit_backlinks,
|
||||
focus_problem_types
|
||||
});
|
||||
|
||||
if (
|
||||
nextState.active_problem_units.length === 0 &&
|
||||
nextState.resolved_problem_units.length === 0 &&
|
||||
nextState.problem_unit_backlinks.length === 0 &&
|
||||
nextState.focus_problem_types.length === 0
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
export function cloneInvestigationState(state: InvestigationStateWithProblemUnits | null): InvestigationStateWithProblemUnits | null {
|
||||
if (!state) return null;
|
||||
const cloned: InvestigationStateWithProblemUnits = {
|
||||
...state,
|
||||
focus: {
|
||||
...state.focus,
|
||||
@@ -132,9 +277,24 @@ export function cloneInvestigationState(state: InvestigationState | null): Inves
|
||||
}
|
||||
: null
|
||||
};
|
||||
if (state.problem_unit_state) {
|
||||
cloned.problem_unit_state = capProblemUnitState({
|
||||
active_problem_units: [...state.problem_unit_state.active_problem_units],
|
||||
resolved_problem_units: [...state.problem_unit_state.resolved_problem_units],
|
||||
problem_unit_backlinks: state.problem_unit_state.problem_unit_backlinks.map((item) => ({
|
||||
problem_unit_id: item.problem_unit_id,
|
||||
entity_backlinks: [...item.entity_backlinks]
|
||||
})),
|
||||
focus_problem_types: [...state.problem_unit_state.focus_problem_types]
|
||||
});
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function createEmptyInvestigationState(sessionId: string, timestamp = new Date().toISOString()): InvestigationState {
|
||||
export function createEmptyInvestigationState(
|
||||
sessionId: string,
|
||||
timestamp = new Date().toISOString()
|
||||
): InvestigationStateWithProblemUnits {
|
||||
return {
|
||||
schema_version: INVESTIGATION_STATE_SCHEMA_VERSION,
|
||||
session_id: sessionId,
|
||||
@@ -157,7 +317,7 @@ export function createEmptyInvestigationState(sessionId: string, timestamp = new
|
||||
};
|
||||
}
|
||||
|
||||
export function updateInvestigationState(input: UpdateInvestigationStateInput): InvestigationState {
|
||||
export function updateInvestigationState(input: UpdateInvestigationStateInput): InvestigationStateWithProblemUnits {
|
||||
const previous = input.previous;
|
||||
const focusFromMessage = capStrings(detectAccounts(input.userMessage), INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const requirementIds = capStrings(
|
||||
@@ -165,6 +325,7 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
|
||||
INVESTIGATION_MAX_REQUIREMENT_LINKS
|
||||
);
|
||||
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
|
||||
const problemUnitState = updateProblemUnitState(previous, input.retrievalResults);
|
||||
|
||||
return {
|
||||
schema_version: INVESTIGATION_STATE_SCHEMA_VERSION,
|
||||
@@ -194,6 +355,11 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
|
||||
last_user_message: input.userMessage.slice(0, 240),
|
||||
referenced_requirement_ids: requirementIds
|
||||
},
|
||||
query_mode_hint: deriveQueryModeHint(input.routeSummary)
|
||||
query_mode_hint: deriveQueryModeHint(input.routeSummary),
|
||||
...(problemUnitState
|
||||
? {
|
||||
problem_unit_state: problemUnitState
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user