NODEDC_1C/llm_normalizer/backend/dist/routes/assistant.js

258 lines
10 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantRouter = buildAssistantRouter;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const express_1 = require("express");
const config_1 = require("../config");
const http_1 = require("../utils/http");
function toStringSafe(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toNumberSafe(value) {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function clampInt(value, min, max, fallback) {
if (value === null || !Number.isFinite(value)) {
return fallback;
}
const rounded = Math.trunc(value);
if (rounded < min)
return min;
if (rounded > max)
return max;
return rounded;
}
function parseComment(value) {
if (typeof value !== "string")
return "";
return value.trim().slice(0, 4000);
}
function parseAnnotationAuthor(value) {
const normalized = toStringSafe(value);
if (!normalized)
return null;
return normalized.slice(0, 80);
}
function toRecord(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function generateAnnotationId() {
return `asann-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
function ensureAnnotationStoreDir() {
const dir = path_1.default.dirname(config_1.ASSISTANT_ANNOTATIONS_FILE);
if (!fs_1.default.existsSync(dir)) {
fs_1.default.mkdirSync(dir, { recursive: true });
}
}
function readAssistantAnnotations() {
if (!fs_1.default.existsSync(config_1.ASSISTANT_ANNOTATIONS_FILE)) {
return [];
}
try {
const parsed = JSON.parse(fs_1.default.readFileSync(config_1.ASSISTANT_ANNOTATIONS_FILE, "utf-8"));
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.map((item) => toRecord(item))
.filter((item) => item !== null)
.map((item) => {
const context = toRecord(item.context);
const technicalContext = toRecord(item.technical_context);
const createdAt = toStringSafe(item.created_at) ?? new Date().toISOString();
const updatedAt = toStringSafe(item.updated_at) ?? createdAt;
return {
annotation_id: toStringSafe(item.annotation_id) ?? "",
session_id: toStringSafe(item.session_id) ?? "",
message_id: toStringSafe(item.message_id) ?? "",
message_index: clampInt(toNumberSafe(item.message_index), 0, 100_000, 0),
rating: clampInt(toNumberSafe(item.rating), 1, 5, 3),
comment: parseComment(item.comment),
annotation_author: parseAnnotationAuthor(item.annotation_author),
created_at: createdAt,
updated_at: updatedAt,
context: {
trace_id: toStringSafe(context?.trace_id),
reply_type: toStringSafe(context?.reply_type),
question_text: toStringSafe(context?.question_text),
answer_text: toStringSafe(context?.answer_text)
},
technical_context: technicalContext
};
})
.filter((item) => item.annotation_id && item.session_id && item.message_id && item.comment.length > 0)
.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
}
catch {
return [];
}
}
function writeAssistantAnnotations(items) {
ensureAnnotationStoreDir();
fs_1.default.writeFileSync(config_1.ASSISTANT_ANNOTATIONS_FILE, JSON.stringify(items, null, 2), "utf-8");
}
function annotationKey(sessionId, messageIndex) {
return `${sessionId}::${messageIndex}`;
}
function buildAssistantRouter(services) {
const router = (0, express_1.Router)();
router.post("/api/assistant/message", async (req, res, next) => {
try {
const payload = (req.body ?? {});
const userMessageSource = typeof payload.user_message === "string"
? payload.user_message
: typeof payload.message === "string"
? payload.message
: "";
const userMessage = userMessageSource.trim();
if (!userMessage) {
throw new http_1.ApiError("INVALID_ASSISTANT_MESSAGE", "Field `user_message` or `message` is required.", 400);
}
const response = await services.assistantService.handleMessage({
...payload,
user_message: userMessage,
message: userMessage,
mode: typeof payload.mode === "string" ? payload.mode : "assistant"
});
(0, http_1.ok)(res, response);
}
catch (error) {
next(error);
}
});
router.get("/api/assistant/session/:session_id", (req, res, next) => {
try {
const sessionId = String(req.params.session_id ?? "");
const session = services.assistantService.getSession(sessionId);
if (!session) {
throw new http_1.ApiError("ASSISTANT_SESSION_NOT_FOUND", `Session not found: ${sessionId}`, 404);
}
(0, http_1.ok)(res, {
ok: true,
session
});
}
catch (error) {
next(error);
}
});
router.get("/api/assistant/annotations", (req, res, next) => {
try {
const query = req.query;
const sessionIdFilter = toStringSafe(query.session_id);
const limit = clampInt(toNumberSafe(query.limit), 1, 1000, 300);
const items = readAssistantAnnotations()
.filter((item) => (sessionIdFilter ? item.session_id === sessionIdFilter : true))
.slice(0, limit);
(0, http_1.ok)(res, {
ok: true,
generated_at: new Date().toISOString(),
filters_applied: {
session_id: sessionIdFilter ?? null,
limit
},
items
});
}
catch (error) {
next(error);
}
});
router.post("/api/assistant/annotations", (req, res, next) => {
try {
const body = toRecord(req.body);
if (!body) {
throw new http_1.ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "JSON body is required", 400);
}
const sessionId = toStringSafe(body.session_id);
const messageIndexRaw = toNumberSafe(body.message_index);
const ratingRaw = toNumberSafe(body.rating);
const comment = parseComment(body.comment);
const annotationAuthor = parseAnnotationAuthor(body.annotation_author);
if (!sessionId) {
throw new http_1.ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "session_id is required", 400);
}
if (messageIndexRaw === null) {
throw new http_1.ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "message_index is required", 400);
}
const messageIndex = clampInt(messageIndexRaw, 0, 100_000, 0);
if (ratingRaw === null) {
throw new http_1.ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "rating is required", 400);
}
const rating = clampInt(ratingRaw, 1, 5, 3);
if (!comment) {
throw new http_1.ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "comment is required", 400);
}
const session = services.assistantService.getSession(sessionId);
if (!session) {
throw new http_1.ApiError("ASSISTANT_SESSION_NOT_FOUND", `Session not found: ${sessionId}`, 404);
}
if (messageIndex >= session.items.length) {
throw new http_1.ApiError("ASSISTANT_MESSAGE_NOT_FOUND", `Message index ${messageIndex} out of range`, 400);
}
const targetMessage = session.items[messageIndex];
if (!targetMessage || targetMessage.role !== "assistant") {
throw new http_1.ApiError("ASSISTANT_MESSAGE_NOT_ASSISTANT", "Only assistant answers can be annotated", 400);
}
const pairedUserQuestion = [...session.items.slice(0, messageIndex)].reverse().find((item) => item.role === "user") ?? null;
const nowIso = new Date().toISOString();
const annotations = readAssistantAnnotations();
const key = annotationKey(sessionId, messageIndex);
const existingIndex = annotations.findIndex((item) => annotationKey(item.session_id, item.message_index) === key);
const existing = existingIndex >= 0 ? annotations[existingIndex] : null;
const annotation = {
annotation_id: existing?.annotation_id ?? generateAnnotationId(),
session_id: sessionId,
message_id: targetMessage.message_id,
message_index: messageIndex,
rating,
comment,
annotation_author: annotationAuthor,
created_at: existing?.created_at ?? nowIso,
updated_at: nowIso,
context: {
trace_id: toStringSafe(targetMessage.trace_id),
reply_type: toStringSafe(targetMessage.reply_type),
question_text: toStringSafe(pairedUserQuestion?.text),
answer_text: toStringSafe(targetMessage.text)
},
technical_context: toRecord(targetMessage.debug)
};
if (existingIndex >= 0) {
annotations[existingIndex] = annotation;
}
else {
annotations.push(annotation);
}
writeAssistantAnnotations(annotations);
(0, http_1.ok)(res, {
ok: true,
annotation
});
}
catch (error) {
next(error);
}
});
return router;
}