"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; }