258 lines
10 KiB
JavaScript
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;
|
|
}
|