NODEDC_1C/llm_normalizer/backend/src/services/traceLogger.ts

151 lines
4.0 KiB
TypeScript

import fs from "fs";
import path from "path";
import { EVAL_CASES_DIR, PRESETS_DIR, TRACES_DIR } from "../config";
import { ensureDir, writeJsonFile } from "../utils/files";
import type { PromptPreset } from "../types/preset";
export interface TraceRecord {
trace_id: string;
timestamp: string;
model: string;
prompt_version: string;
schema_version: string;
case_id?: string;
user_question_raw: string;
context: Record<string, unknown>;
request_payload_redacted: Record<string, unknown>;
raw_model_response: unknown;
parsed_normalized_json: unknown;
validation_result: {
passed: boolean;
errors: string[];
};
route_hint_summary?: unknown;
route_hint: string | null;
confidence: string | null;
usage: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
};
latency_ms: number;
expected_route?: string;
eval_label?: string;
eval_mode?: string;
request_count_for_case: number;
}
export interface HistoryListItem {
trace_id: string;
timestamp: string;
model: string;
question_short: string;
confidence: string | null;
validation_passed: boolean;
route_hint: string | null;
save_status: "saved";
}
function redactSecrets(payload: Record<string, unknown>): Record<string, unknown> {
const output = { ...payload };
delete output.apiKey;
return output;
}
function isNoSpaceError(error: unknown): boolean {
const code = (error as { code?: unknown } | null)?.code;
return code === "ENOSPC";
}
export function saveTrace(record: TraceRecord): void {
try {
ensureDir(TRACES_DIR);
const target = path.resolve(TRACES_DIR, `${record.trace_id}.json`);
writeJsonFile(target, record);
} catch (error) {
if (isNoSpaceError(error)) {
return;
}
throw error;
}
}
export function listTraces(limit = 100): HistoryListItem[] {
ensureDir(TRACES_DIR);
const files = fs
.readdirSync(TRACES_DIR)
.filter((item) => item.endsWith(".json"))
.sort((a, b) => {
const pa = path.resolve(TRACES_DIR, a);
const pb = path.resolve(TRACES_DIR, b);
return fs.statSync(pb).mtimeMs - fs.statSync(pa).mtimeMs;
})
.slice(0, limit);
return files.map((fileName) => {
const raw = fs.readFileSync(path.resolve(TRACES_DIR, fileName), "utf-8");
const item = JSON.parse(raw) as TraceRecord;
return {
trace_id: item.trace_id,
timestamp: item.timestamp,
model: item.model,
question_short: item.user_question_raw.slice(0, 110),
confidence: item.confidence,
validation_passed: item.validation_result.passed,
route_hint: item.route_hint,
save_status: "saved"
};
});
}
export function getTrace(traceId: string): TraceRecord | null {
ensureDir(TRACES_DIR);
const target = path.resolve(TRACES_DIR, `${traceId}.json`);
if (!fs.existsSync(target)) {
return null;
}
const raw = fs.readFileSync(target, "utf-8");
return JSON.parse(raw) as TraceRecord;
}
export function savePreset(preset: PromptPreset): void {
try {
ensureDir(PRESETS_DIR);
writeJsonFile(path.resolve(PRESETS_DIR, `${preset.id}.json`), preset);
} catch (error) {
if (isNoSpaceError(error)) {
return;
}
throw error;
}
}
export function listPresets(): PromptPreset[] {
ensureDir(PRESETS_DIR);
return fs
.readdirSync(PRESETS_DIR)
.filter((item) => item.endsWith(".json"))
.map((fileName) => {
const raw = fs.readFileSync(path.resolve(PRESETS_DIR, fileName), "utf-8");
return JSON.parse(raw) as PromptPreset;
})
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
export function saveEvalCase(casePayload: Record<string, unknown>): string {
const id = String(casePayload.case_id ?? `NQ-${Date.now()}`);
try {
ensureDir(EVAL_CASES_DIR);
writeJsonFile(path.resolve(EVAL_CASES_DIR, `${id}.json`), casePayload);
} catch (error) {
if (!isNoSpaceError(error)) {
throw error;
}
}
return id;
}
export function redactRequestPayload(payload: Record<string, unknown>): Record<string, unknown> {
return redactSecrets(payload);
}