Initial import NDC_1C
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import path from "path";
|
||||
import { ASSISTANT_SESSIONS_DIR } from "../config";
|
||||
import type { AssistantConversationItem, AssistantReplyType, AssistantSessionState } from "../types/assistant";
|
||||
import { ensureDir, writeJsonFile } from "../utils/files";
|
||||
|
||||
interface AssistantTurnLogRecord {
|
||||
turn_id: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
human_block: string;
|
||||
human_readable: {
|
||||
question_raw: string;
|
||||
question_understood: string;
|
||||
decomposition: string[];
|
||||
answer: string;
|
||||
reply_type: AssistantReplyType | null;
|
||||
};
|
||||
technical_json: {
|
||||
trace_id: string | null;
|
||||
user_message: AssistantConversationItem;
|
||||
assistant_message: AssistantConversationItem;
|
||||
debug: AssistantConversationItem["debug"];
|
||||
};
|
||||
}
|
||||
|
||||
interface AssistantSessionLogRecord {
|
||||
schema_version: "assistant_session_log_v1";
|
||||
session_id: string;
|
||||
started_at: string;
|
||||
updated_at: string;
|
||||
counters: {
|
||||
total_messages: number;
|
||||
user_messages: number;
|
||||
assistant_messages: number;
|
||||
};
|
||||
trace_ids: string[];
|
||||
reply_types: AssistantReplyType[];
|
||||
investigation_state: AssistantSessionState["investigation_state"];
|
||||
turns: AssistantTurnLogRecord[];
|
||||
conversation: AssistantConversationItem[];
|
||||
last_assistant: {
|
||||
message_id: string | null;
|
||||
reply_type: AssistantReplyType | null;
|
||||
trace_id: string | null;
|
||||
created_at: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
function unique(values: Array<string | null>): string[] {
|
||||
return Array.from(new Set(values.filter((item): item is string => typeof item === "string" && item.length > 0)));
|
||||
}
|
||||
|
||||
function toObject(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function toStringOrNull(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function extractFragments(assistantItem: AssistantConversationItem): Array<Record<string, unknown>> {
|
||||
if (!assistantItem.debug || !Array.isArray(assistantItem.debug.fragments)) {
|
||||
return [];
|
||||
}
|
||||
return assistantItem.debug.fragments
|
||||
.map((item) => toObject(item))
|
||||
.filter((item): item is Record<string, unknown> => item !== null);
|
||||
}
|
||||
|
||||
function extractNormalizedQuestion(userText: string, assistantItem: AssistantConversationItem): string {
|
||||
const normalized = toObject(assistantItem.debug?.normalized);
|
||||
if (normalized) {
|
||||
const fromUserMessageRaw = toStringOrNull(normalized.user_message_raw);
|
||||
if (fromUserMessageRaw) return fromUserMessageRaw;
|
||||
const fromUserQuestionRaw = toStringOrNull(normalized.user_question_raw);
|
||||
if (fromUserQuestionRaw) return fromUserQuestionRaw;
|
||||
const fromNormalizedQuestion = toStringOrNull(normalized.normalized_question);
|
||||
if (fromNormalizedQuestion) return fromNormalizedQuestion;
|
||||
}
|
||||
|
||||
const fragments = extractFragments(assistantItem);
|
||||
if (fragments.length > 0) {
|
||||
const joined = fragments
|
||||
.map((fragment) => toStringOrNull(fragment.normalized_fragment_text) ?? toStringOrNull(fragment.raw_fragment_text))
|
||||
.filter((item): item is string => Boolean(item))
|
||||
.join(" | ");
|
||||
if (joined) {
|
||||
return joined;
|
||||
}
|
||||
}
|
||||
|
||||
return userText;
|
||||
}
|
||||
|
||||
function buildRouteLookup(assistantItem: AssistantConversationItem): Map<string, Record<string, unknown>> {
|
||||
const output = new Map<string, Record<string, unknown>>();
|
||||
if (!assistantItem.debug || !Array.isArray(assistantItem.debug.routes)) {
|
||||
return output;
|
||||
}
|
||||
for (const route of assistantItem.debug.routes) {
|
||||
const routeObject = toObject(route);
|
||||
if (!routeObject) continue;
|
||||
const fragmentId = toStringOrNull(routeObject.fragment_id);
|
||||
if (!fragmentId) continue;
|
||||
output.set(fragmentId, routeObject);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function buildDecompositionLines(assistantItem: AssistantConversationItem): string[] {
|
||||
const fragments = extractFragments(assistantItem);
|
||||
if (fragments.length === 0) {
|
||||
return ["Фрагменты декомпозиции не выделены."];
|
||||
}
|
||||
|
||||
const routeLookup = buildRouteLookup(assistantItem);
|
||||
|
||||
return fragments.map((fragment, index) => {
|
||||
const fragmentId = toStringOrNull(fragment.fragment_id) ?? `F${index + 1}`;
|
||||
const fragmentText =
|
||||
toStringOrNull(fragment.normalized_fragment_text) ??
|
||||
toStringOrNull(fragment.raw_fragment_text) ??
|
||||
"текст фрагмента отсутствует";
|
||||
const executionReadiness = toStringOrNull(fragment.execution_readiness);
|
||||
const routeStatus = toStringOrNull(fragment.route_status);
|
||||
|
||||
const routeObject = routeLookup.get(fragmentId);
|
||||
const route = toStringOrNull(routeObject?.route);
|
||||
const noRouteReason =
|
||||
toStringOrNull(fragment.no_route_reason) ?? toStringOrNull(routeObject?.no_route_reason);
|
||||
|
||||
const parts = [`${fragmentId}: ${fragmentText}`];
|
||||
if (executionReadiness) parts.push(`execution_readiness=${executionReadiness}`);
|
||||
if (routeStatus) parts.push(`route_status=${routeStatus}`);
|
||||
if (route) parts.push(`route=${route}`);
|
||||
if (noRouteReason) parts.push(`no_route_reason=${noRouteReason}`);
|
||||
|
||||
return parts.join("; ");
|
||||
});
|
||||
}
|
||||
|
||||
function toHumanBlock(input: {
|
||||
questionRaw: string;
|
||||
questionUnderstood: string;
|
||||
decomposition: string[];
|
||||
answer: string;
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`Вопрос: ${input.questionRaw}`);
|
||||
lines.push(`Понято как: ${input.questionUnderstood}`);
|
||||
lines.push("Декомпозиция:");
|
||||
lines.push(...input.decomposition.map((item) => `- ${item}`));
|
||||
lines.push(`Ответ: ${input.answer}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildTurns(items: AssistantConversationItem[]): AssistantTurnLogRecord[] {
|
||||
const turns: AssistantTurnLogRecord[] = [];
|
||||
const pendingUsers: AssistantConversationItem[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.role === "user") {
|
||||
pendingUsers.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pairedUser = pendingUsers.shift();
|
||||
if (!pairedUser) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const questionRaw = pairedUser.text;
|
||||
const questionUnderstood = extractNormalizedQuestion(questionRaw, item);
|
||||
const decomposition = buildDecompositionLines(item);
|
||||
const answer = item.text;
|
||||
|
||||
turns.push({
|
||||
turn_id: `turn-${turns.length + 1}`,
|
||||
started_at: pairedUser.created_at ?? null,
|
||||
completed_at: item.created_at ?? null,
|
||||
human_block: toHumanBlock({
|
||||
questionRaw,
|
||||
questionUnderstood,
|
||||
decomposition,
|
||||
answer
|
||||
}),
|
||||
human_readable: {
|
||||
question_raw: questionRaw,
|
||||
question_understood: questionUnderstood,
|
||||
decomposition,
|
||||
answer,
|
||||
reply_type: item.reply_type
|
||||
},
|
||||
technical_json: {
|
||||
trace_id: item.trace_id,
|
||||
user_message: pairedUser,
|
||||
assistant_message: item,
|
||||
debug: item.debug
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return turns;
|
||||
}
|
||||
|
||||
export class AssistantSessionLogger {
|
||||
constructor(private readonly rootDir: string = ASSISTANT_SESSIONS_DIR) {}
|
||||
|
||||
public persistSession(session: AssistantSessionState): void {
|
||||
ensureDir(this.rootDir);
|
||||
const filePath = path.resolve(this.rootDir, `${session.session_id}.json`);
|
||||
|
||||
const startedAt = session.items[0]?.created_at ?? session.updated_at;
|
||||
const userMessages = session.items.filter((item) => item.role === "user").length;
|
||||
const assistantMessages = session.items.filter((item) => item.role === "assistant").length;
|
||||
const assistantItems = session.items.filter((item) => item.role === "assistant");
|
||||
const lastAssistant = assistantItems.length > 0 ? assistantItems[assistantItems.length - 1] : null;
|
||||
|
||||
const traceIds = unique(session.items.map((item) => item.trace_id));
|
||||
const replyTypes = Array.from(
|
||||
new Set(
|
||||
session.items
|
||||
.map((item) => item.reply_type)
|
||||
.filter((item): item is AssistantReplyType => typeof item === "string" && item.length > 0)
|
||||
)
|
||||
);
|
||||
const turns = buildTurns(session.items);
|
||||
|
||||
const record: AssistantSessionLogRecord = {
|
||||
schema_version: "assistant_session_log_v1",
|
||||
session_id: session.session_id,
|
||||
started_at: startedAt,
|
||||
updated_at: session.updated_at,
|
||||
counters: {
|
||||
total_messages: session.items.length,
|
||||
user_messages: userMessages,
|
||||
assistant_messages: assistantMessages
|
||||
},
|
||||
trace_ids: traceIds,
|
||||
reply_types: replyTypes,
|
||||
investigation_state: session.investigation_state,
|
||||
turns,
|
||||
conversation: session.items,
|
||||
last_assistant: {
|
||||
message_id: lastAssistant?.message_id ?? null,
|
||||
reply_type: lastAssistant?.reply_type ?? null,
|
||||
trace_id: lastAssistant?.trace_id ?? null,
|
||||
created_at: lastAssistant?.created_at ?? null
|
||||
}
|
||||
};
|
||||
|
||||
writeJsonFile(filePath, record);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user