151 lines
4.0 KiB
TypeScript
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);
|
|
}
|