ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.8 - юи

This commit is contained in:
2026-04-12 00:54:55 +03:00
parent d65969d2ff
commit 8167bd228d
18 changed files with 1408 additions and 70 deletions
+4 -2
View File
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = void 0;
exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_ANNOTATIONS_FILE = exports.ASSISTANT_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = void 0;
const path_1 = __importDefault(require("path"));
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
@@ -72,6 +72,8 @@ exports.TRACES_DIR = path_1.default.resolve(exports.DATA_DIR, "traces");
exports.PRESETS_DIR = path_1.default.resolve(exports.DATA_DIR, "presets");
exports.EVAL_CASES_DIR = path_1.default.resolve(exports.DATA_DIR, "eval_cases");
exports.ASSISTANT_SESSIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "assistant_sessions");
exports.ASSISTANT_ANNOTATIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "assistant_annotations");
exports.ASSISTANT_ANNOTATIONS_FILE = path_1.default.resolve(exports.ASSISTANT_ANNOTATIONS_DIR, "annotations.json");
exports.AUTORUN_ANNOTATIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_annotations");
exports.AUTORUN_ANNOTATIONS_FILE = path_1.default.resolve(exports.AUTORUN_ANNOTATIONS_DIR, "annotations.json");
exports.AUTORUN_GENERATOR_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_generators");
+208
View File
@@ -1,8 +1,118 @@
"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) => {
@@ -45,5 +155,103 @@ function buildAssistantRouter(services) {
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;
}
+1
View File
@@ -31,6 +31,7 @@ function createApp() {
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
(0, files_1.ensureDir)(config_1.ASSISTANT_SESSIONS_DIR);
(0, files_1.ensureDir)(config_1.ASSISTANT_ANNOTATIONS_DIR);
(0, files_1.ensureDir)(config_1.AUTORUN_ANNOTATIONS_DIR);
(0, files_1.ensureDir)(config_1.AUTORUN_GENERATOR_DIR);
const app = (0, express_1.default)();
+2
View File
@@ -134,6 +134,8 @@ export const TRACES_DIR = path.resolve(DATA_DIR, "traces");
export const PRESETS_DIR = path.resolve(DATA_DIR, "presets");
export const EVAL_CASES_DIR = path.resolve(DATA_DIR, "eval_cases");
export const ASSISTANT_SESSIONS_DIR = path.resolve(DATA_DIR, "assistant_sessions");
export const ASSISTANT_ANNOTATIONS_DIR = path.resolve(DATA_DIR, "assistant_annotations");
export const ASSISTANT_ANNOTATIONS_FILE = path.resolve(ASSISTANT_ANNOTATIONS_DIR, "annotations.json");
export const AUTORUN_ANNOTATIONS_DIR = path.resolve(DATA_DIR, "autorun_annotations");
export const AUTORUN_ANNOTATIONS_FILE = path.resolve(AUTORUN_ANNOTATIONS_DIR, "annotations.json");
export const AUTORUN_GENERATOR_DIR = path.resolve(DATA_DIR, "autorun_generators");
@@ -1,8 +1,140 @@
import fs from "fs";
import path from "path";
import { Router } from "express";
import { ASSISTANT_ANNOTATIONS_FILE } from "../config";
import type { AppServices } from "../serverContext";
import type { AssistantMessageRequestPayload } from "../types/assistant";
import { ApiError, ok } from "../utils/http";
interface AssistantAnnotationRecord {
annotation_id: string;
session_id: string;
message_id: string;
message_index: number;
rating: number;
comment: string;
annotation_author: string | null;
created_at: string;
updated_at: string;
context: {
trace_id: string | null;
reply_type: string | null;
question_text: string | null;
answer_text: string | null;
};
technical_context: Record<string, unknown> | null;
}
function toStringSafe(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toNumberSafe(value: unknown): number | null {
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: number | null, min: number, max: number, fallback: number): number {
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: unknown): string {
if (typeof value !== "string") return "";
return value.trim().slice(0, 4000);
}
function parseAnnotationAuthor(value: unknown): string | null {
const normalized = toStringSafe(value);
if (!normalized) return null;
return normalized.slice(0, 80);
}
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function generateAnnotationId(): string {
return `asann-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
function ensureAnnotationStoreDir(): void {
const dir = path.dirname(ASSISTANT_ANNOTATIONS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function readAssistantAnnotations(): AssistantAnnotationRecord[] {
if (!fs.existsSync(ASSISTANT_ANNOTATIONS_FILE)) {
return [];
}
try {
const parsed = JSON.parse(fs.readFileSync(ASSISTANT_ANNOTATIONS_FILE, "utf-8")) as unknown;
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.map((item) => toRecord(item))
.filter((item): item is Record<string, unknown> => 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
} satisfies AssistantAnnotationRecord;
})
.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: AssistantAnnotationRecord[]): void {
ensureAnnotationStoreDir();
fs.writeFileSync(ASSISTANT_ANNOTATIONS_FILE, JSON.stringify(items, null, 2), "utf-8");
}
function annotationKey(sessionId: string, messageIndex: number): string {
return `${sessionId}::${messageIndex}`;
}
export function buildAssistantRouter(services: AppServices): Router {
const router = Router();
@@ -48,5 +180,111 @@ export function buildAssistantRouter(services: AppServices): Router {
}
});
router.get("/api/assistant/annotations", (req, res, next) => {
try {
const query = req.query as Record<string, unknown>;
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);
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 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 ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "session_id is required", 400);
}
if (messageIndexRaw === null) {
throw new ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "message_index is required", 400);
}
const messageIndex = clampInt(messageIndexRaw, 0, 100_000, 0);
if (ratingRaw === null) {
throw new ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "rating is required", 400);
}
const rating = clampInt(ratingRaw, 1, 5, 3);
if (!comment) {
throw new ApiError("INVALID_ASSISTANT_ANNOTATION_PAYLOAD", "comment is required", 400);
}
const session = services.assistantService.getSession(sessionId);
if (!session) {
throw new ApiError("ASSISTANT_SESSION_NOT_FOUND", `Session not found: ${sessionId}`, 404);
}
if (messageIndex >= session.items.length) {
throw new ApiError("ASSISTANT_MESSAGE_NOT_FOUND", `Message index ${messageIndex} out of range`, 400);
}
const targetMessage = session.items[messageIndex];
if (!targetMessage || targetMessage.role !== "assistant") {
throw new 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: AssistantAnnotationRecord = {
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);
ok(res, {
ok: true,
annotation
});
} catch (error) {
next(error);
}
});
return router;
}
+2
View File
@@ -9,6 +9,7 @@ import {
REPORTS_DIR,
TIMEZONE,
ASSISTANT_SESSIONS_DIR,
ASSISTANT_ANNOTATIONS_DIR,
AUTORUN_ANNOTATIONS_DIR,
AUTORUN_GENERATOR_DIR
} from "./config";
@@ -37,6 +38,7 @@ export function createApp(): express.Express {
ensureDir(EVAL_CASES_DIR);
ensureDir(REPORTS_DIR);
ensureDir(ASSISTANT_SESSIONS_DIR);
ensureDir(ASSISTANT_ANNOTATIONS_DIR);
ensureDir(AUTORUN_ANNOTATIONS_DIR);
ensureDir(AUTORUN_GENERATOR_DIR);