Files
NODEDC_1C/llm_normalizer/backend/src/services/assistantSessionStore.ts
T

148 lines
6.0 KiB
TypeScript

import { nanoid } from "nanoid";
import type { AssistantConversationItem, AssistantSessionState } from "../types/assistant";
import type { InvestigationState } from "../types/stage1Contracts";
import type { AddressNavigationState } from "../types/addressNavigation";
import {
FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1,
FEATURE_ASSISTANT_INVESTIGATION_STATE_V1
} from "../config";
import { cloneInvestigationState, createEmptyInvestigationState } from "./investigationState";
import {
cloneAddressNavigationState,
createEmptyAddressNavigationState,
evolveAddressNavigationStateWithAssistantItem,
normalizeAddressNavigationState
} from "./addressNavigationState";
const MAX_ITEMS_PER_SESSION = 200;
function cloneItem(item: AssistantConversationItem): AssistantConversationItem {
return {
...item,
debug: item.debug ? { ...item.debug } : null
};
}
function cloneSession(state: AssistantSessionState): AssistantSessionState {
return {
session_id: state.session_id,
updated_at: state.updated_at,
items: state.items.map(cloneItem),
investigation_state: cloneInvestigationState(state.investigation_state),
address_navigation_state: cloneAddressNavigationState(state.address_navigation_state)
};
}
function normalizeSessionShape(state: AssistantSessionState): AssistantSessionState {
const legacy = state as AssistantSessionState & {
investigation_state?: InvestigationState | null;
address_navigation_state?: AddressNavigationState | null;
items?: AssistantConversationItem[];
updated_at?: string;
};
const normalizedItems = Array.isArray(legacy.items) ? legacy.items : [];
const investigationState =
FEATURE_ASSISTANT_INVESTIGATION_STATE_V1
? legacy.investigation_state ?? createEmptyInvestigationState(state.session_id)
: legacy.investigation_state ?? null;
const addressNavigationState =
FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1
? normalizeAddressNavigationState(
legacy.address_navigation_state ?? createEmptyAddressNavigationState(state.session_id),
state.session_id
)
: legacy.address_navigation_state ?? null;
state.items = normalizedItems;
state.updated_at = typeof legacy.updated_at === "string" && legacy.updated_at.trim() ? legacy.updated_at : new Date().toISOString();
state.investigation_state = investigationState;
state.address_navigation_state = addressNavigationState;
return state;
}
export class AssistantSessionStore {
private readonly sessions = new Map<string, AssistantSessionState>();
public ensureSession(sessionId?: string): AssistantSessionState {
const resolvedId = (sessionId ?? "").trim() || `asst-${nanoid(10)}`;
const existing = this.sessions.get(resolvedId);
if (existing) {
return cloneSession(normalizeSessionShape(existing));
}
const created: AssistantSessionState = {
session_id: resolvedId,
updated_at: new Date().toISOString(),
items: [],
investigation_state: FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 ? createEmptyInvestigationState(resolvedId) : null,
address_navigation_state: FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 ? createEmptyAddressNavigationState(resolvedId) : null
};
this.sessions.set(resolvedId, created);
return cloneSession(created);
}
public appendItem(sessionId: string, item: AssistantConversationItem): AssistantConversationItem {
const session = this.ensureMutableSession(sessionId);
session.items.push(item);
if (session.items.length > MAX_ITEMS_PER_SESSION) {
session.items = session.items.slice(session.items.length - MAX_ITEMS_PER_SESSION);
}
if (FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1) {
const baseNavigationState = normalizeAddressNavigationState(session.address_navigation_state, sessionId);
session.address_navigation_state = evolveAddressNavigationStateWithAssistantItem(
baseNavigationState,
item,
Math.max(0, session.items.length - 1)
);
}
session.updated_at = new Date().toISOString();
return cloneItem(item);
}
public getSession(sessionId: string): AssistantSessionState | null {
const found = this.sessions.get(sessionId);
return found ? cloneSession(normalizeSessionShape(found)) : null;
}
public setInvestigationState(sessionId: string, state: InvestigationState | null): InvestigationState | null {
const session = this.ensureMutableSession(sessionId);
session.investigation_state = cloneInvestigationState(state);
session.updated_at = new Date().toISOString();
return cloneInvestigationState(session.investigation_state);
}
public getAddressNavigationState(sessionId: string): AddressNavigationState | null {
const session = this.ensureMutableSession(sessionId);
if (!FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1) {
return null;
}
return cloneAddressNavigationState(session.address_navigation_state);
}
public setAddressNavigationState(sessionId: string, state: AddressNavigationState | null): AddressNavigationState | null {
const session = this.ensureMutableSession(sessionId);
if (!FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1) {
session.address_navigation_state = null;
return null;
}
session.address_navigation_state = normalizeAddressNavigationState(state, sessionId);
session.updated_at = new Date().toISOString();
return cloneAddressNavigationState(session.address_navigation_state);
}
private ensureMutableSession(sessionId: string): AssistantSessionState {
const existing = this.sessions.get(sessionId);
if (existing) {
return normalizeSessionShape(existing);
}
const created: AssistantSessionState = {
session_id: sessionId,
updated_at: new Date().toISOString(),
items: [],
investigation_state: FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 ? createEmptyInvestigationState(sessionId) : null,
address_navigation_state: FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 ? createEmptyAddressNavigationState(sessionId) : null
};
this.sessions.set(sessionId, created);
return created;
}
}