ЮИ - Добавить удаление сохраненных наборов автопрогонов с удалением файлов на бэке
This commit is contained in:
+290
-12
@@ -93,6 +93,16 @@ function clampInt(value, min, max, fallback) {
|
|||||||
return max;
|
return max;
|
||||||
return rounded;
|
return rounded;
|
||||||
}
|
}
|
||||||
|
function isAutoGenMode(value) {
|
||||||
|
return value === "qwen_seed" || value === "codex_creative" || value === "saved_user_sessions";
|
||||||
|
}
|
||||||
|
function parseAutoGenTitle(value) {
|
||||||
|
const title = toStringSafe(value);
|
||||||
|
if (!title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return title.slice(0, 160);
|
||||||
|
}
|
||||||
function parseManualCaseDecision(value, fallback = "needs_dialog_policy_fix") {
|
function parseManualCaseDecision(value, fallback = "needs_dialog_policy_fix") {
|
||||||
const normalized = toStringSafe(value);
|
const normalized = toStringSafe(value);
|
||||||
if (!normalized)
|
if (!normalized)
|
||||||
@@ -151,15 +161,11 @@ function readAutoGenHistory() {
|
|||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
generation_id: toStringSafe(item.generation_id) ?? "",
|
generation_id: toStringSafe(item.generation_id) ?? "",
|
||||||
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
|
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
|
||||||
mode: toStringSafe(item.mode) ?? "codex_creative",
|
mode: isAutoGenMode(toStringSafe(item.mode)) ? toStringSafe(item.mode) : "codex_creative",
|
||||||
|
title: parseAutoGenTitle(item.title),
|
||||||
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
|
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
|
||||||
domain: toStringSafe(item.domain),
|
domain: toStringSafe(item.domain),
|
||||||
questions: toArray(item.questions)
|
questions: parseAssistantSessionQuestions(item.questions),
|
||||||
.map((q) => toStringSafe(q))
|
|
||||||
.filter((q) => q !== null)
|
|
||||||
.map((q) => sanitizeGeneratedQuestion(q))
|
|
||||||
.filter((q) => q.length > 0)
|
|
||||||
.slice(0, 500),
|
|
||||||
generated_by: toStringSafe(item.generated_by),
|
generated_by: toStringSafe(item.generated_by),
|
||||||
saved_case_set_file: toStringSafe(item.saved_case_set_file),
|
saved_case_set_file: toStringSafe(item.saved_case_set_file),
|
||||||
context: toRecord(item.context)
|
context: toRecord(item.context)
|
||||||
@@ -174,7 +180,10 @@ function readAutoGenHistory() {
|
|||||||
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
|
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
|
||||||
autogen_personality_prompt: toStringSafe(toRecord(item.context)?.autogen_personality_prompt)
|
autogen_personality_prompt: toStringSafe(toRecord(item.context)?.autogen_personality_prompt)
|
||||||
? repairAutogenMojibake(String(toRecord(item.context)?.autogen_personality_prompt))
|
? repairAutogenMojibake(String(toRecord(item.context)?.autogen_personality_prompt))
|
||||||
: null
|
: null,
|
||||||
|
source_session_id: toStringSafe(toRecord(item.context)?.source_session_id),
|
||||||
|
saved_session_file: toStringSafe(toRecord(item.context)?.saved_session_file),
|
||||||
|
saved_case_set_kind: toStringSafe(toRecord(item.context)?.saved_case_set_kind)
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}))
|
}))
|
||||||
@@ -1057,7 +1066,7 @@ function parseDecisionFilter(value) {
|
|||||||
}
|
}
|
||||||
function parseAutoGenMode(value) {
|
function parseAutoGenMode(value) {
|
||||||
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
|
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
|
||||||
if (normalized === "qwen_seed" || normalized === "codex_creative") {
|
if (normalized === "qwen_seed" || normalized === "codex_creative" || normalized === "saved_user_sessions") {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
return "codex_creative";
|
return "codex_creative";
|
||||||
@@ -1150,6 +1159,12 @@ function sanitizeGeneratedQuestion(value) {
|
|||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
function parseAssistantSessionQuestions(value) {
|
||||||
|
return toArray(value)
|
||||||
|
.map((item) => sanitizeGeneratedQuestion(typeof item === "string" ? item : ""))
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
.slice(0, 500);
|
||||||
|
}
|
||||||
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
|
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
|
||||||
const AUTOGEN_QUESTION_TAIL_PATTERNS = [
|
const AUTOGEN_QUESTION_TAIL_PATTERNS = [
|
||||||
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
|
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
|
||||||
@@ -1413,6 +1428,18 @@ function buildAutogenCaseSetFileName(mode, generationId) {
|
|||||||
].join("");
|
].join("");
|
||||||
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
|
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
|
||||||
}
|
}
|
||||||
|
function buildSavedAssistantSessionSnapshotFileName(generationId) {
|
||||||
|
const now = new Date();
|
||||||
|
const stamp = [
|
||||||
|
now.getUTCFullYear(),
|
||||||
|
String(now.getUTCMonth() + 1).padStart(2, "0"),
|
||||||
|
String(now.getUTCDate()).padStart(2, "0"),
|
||||||
|
String(now.getUTCHours()).padStart(2, "0"),
|
||||||
|
String(now.getUTCMinutes()).padStart(2, "0"),
|
||||||
|
String(now.getUTCSeconds()).padStart(2, "0")
|
||||||
|
].join("");
|
||||||
|
return `assistant_saved_session_${stamp}_${generationId}.json`;
|
||||||
|
}
|
||||||
function buildAutogenCaseSetPayload(input) {
|
function buildAutogenCaseSetPayload(input) {
|
||||||
const normalizedQuestions = Array.from(new Set(input.questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0)));
|
const normalizedQuestions = Array.from(new Set(input.questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0)));
|
||||||
const cases = normalizedQuestions.map((question, index) => ({
|
const cases = normalizedQuestions.map((question, index) => ({
|
||||||
@@ -1439,6 +1466,99 @@ function buildAutogenCaseSetPayload(input) {
|
|||||||
cases
|
cases
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function buildSavedSessionCaseSetPayload(input) {
|
||||||
|
const questions = parseAssistantSessionQuestions(input.questions);
|
||||||
|
const turns = questions.map((question) => ({
|
||||||
|
user_message: question
|
||||||
|
}));
|
||||||
|
const caseId = "SAVED-001";
|
||||||
|
return {
|
||||||
|
suite_id: `assistant_saved_session_${input.generationId}`,
|
||||||
|
suite_version: "0.1.0",
|
||||||
|
schema_version: "assistant_saved_session_suite_v0_1",
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
generation_id: input.generationId,
|
||||||
|
mode: "saved_user_sessions",
|
||||||
|
title: input.title,
|
||||||
|
scenario_count: turns.length > 0 ? 1 : 0,
|
||||||
|
case_ids: turns.length > 0 ? [caseId] : [],
|
||||||
|
cases: turns.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
case_id: caseId,
|
||||||
|
scenario_tag: "saved_user_sessions",
|
||||||
|
title: input.title,
|
||||||
|
question_type: turns.length > 1 ? "followup" : "direct",
|
||||||
|
broadness_level: "medium",
|
||||||
|
turns
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function ensureDirSync(targetDir) {
|
||||||
|
if (!fs_1.default.existsSync(targetDir)) {
|
||||||
|
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function writeJsonFile(targetPath, payload) {
|
||||||
|
ensureDirSync(path_1.default.dirname(targetPath));
|
||||||
|
fs_1.default.writeFileSync(targetPath, JSON.stringify(payload, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
function rewriteAutoGenCaseSetFile(record) {
|
||||||
|
const caseSetFile = toStringSafe(record.saved_case_set_file);
|
||||||
|
if (!caseSetFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const targetPath = path_1.default.resolve(config_1.EVAL_CASES_DIR, caseSetFile);
|
||||||
|
const payload = record.mode === "saved_user_sessions"
|
||||||
|
? buildSavedSessionCaseSetPayload({
|
||||||
|
generationId: record.generation_id,
|
||||||
|
title: record.title,
|
||||||
|
questions: record.questions
|
||||||
|
})
|
||||||
|
: buildAutogenCaseSetPayload({
|
||||||
|
generationId: record.generation_id,
|
||||||
|
mode: record.mode,
|
||||||
|
domain: record.domain,
|
||||||
|
questions: record.questions
|
||||||
|
});
|
||||||
|
writeJsonFile(targetPath, payload);
|
||||||
|
return caseSetFile;
|
||||||
|
}
|
||||||
|
function writeSavedAssistantSessionSnapshot(input) {
|
||||||
|
const fileName = buildSavedAssistantSessionSnapshotFileName(input.generationId);
|
||||||
|
const targetPath = path_1.default.resolve(path_1.default.dirname(config_1.AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions", fileName);
|
||||||
|
writeJsonFile(targetPath, {
|
||||||
|
saved_at: new Date().toISOString(),
|
||||||
|
generation_id: input.generationId,
|
||||||
|
mode: "saved_user_sessions",
|
||||||
|
title: input.title,
|
||||||
|
source_session_id: input.sessionId,
|
||||||
|
questions: input.questions,
|
||||||
|
session: input.session
|
||||||
|
});
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
function resolveFileInsideDir(baseDir, fileName) {
|
||||||
|
const normalized = toStringSafe(fileName);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const targetPath = path_1.default.resolve(baseDir, normalized);
|
||||||
|
const relative = path_1.default.relative(baseDir, targetPath);
|
||||||
|
if (relative.startsWith("..") || path_1.default.isAbsolute(relative)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
function safeDeleteFile(targetPath) {
|
||||||
|
if (!targetPath || !fs_1.default.existsSync(targetPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
fs_1.default.unlinkSync(targetPath);
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
function collectPostAnalysis(annotations, runMap, limitPerQueue) {
|
function collectPostAnalysis(annotations, runMap, limitPerQueue) {
|
||||||
const byDecision = {};
|
const byDecision = {};
|
||||||
const byQueue = {};
|
const byQueue = {};
|
||||||
@@ -1522,7 +1642,7 @@ function collectPostAnalysis(annotations, runMap, limitPerQueue) {
|
|||||||
].slice(0, 60)
|
].slice(0, 60)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIResponsesClient()) {
|
function buildAutoRunsRouter(services, openaiClient = new openaiResponsesClient_1.OpenAIResponsesClient()) {
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
router.get("/api/autoruns/history", (req, res) => {
|
router.get("/api/autoruns/history", (req, res) => {
|
||||||
const filters = parseFilters(req.query);
|
const filters = parseFilters(req.query);
|
||||||
@@ -1884,7 +2004,7 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||||||
try {
|
try {
|
||||||
const limit = clampInt(toNumberSafe(req.query.limit), 1, 500, 120);
|
const limit = clampInt(toNumberSafe(req.query.limit), 1, 500, 120);
|
||||||
const rawMode = toStringSafe(req.query.mode);
|
const rawMode = toStringSafe(req.query.mode);
|
||||||
const includeAllModes = !rawMode || !["qwen_seed", "codex_creative"].includes(rawMode);
|
const includeAllModes = !rawMode || !isAutoGenMode(rawMode);
|
||||||
const modeFilter = rawMode ?? "codex_creative";
|
const modeFilter = rawMode ?? "codex_creative";
|
||||||
const items = readAutoGenHistory()
|
const items = readAutoGenHistory()
|
||||||
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
|
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
|
||||||
@@ -1911,6 +2031,157 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
router.post("/api/autoruns/autogen/save-assistant-session", (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const body = toRecord(req.body);
|
||||||
|
if (!body) {
|
||||||
|
throw new http_1.ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "JSON body is required", 400);
|
||||||
|
}
|
||||||
|
const sessionId = toStringSafe(body.session_id);
|
||||||
|
const title = parseAutoGenTitle(body.title);
|
||||||
|
const generatedBy = parseAnnotationAuthor(body.generated_by);
|
||||||
|
const context = toRecord(body.context);
|
||||||
|
if (!sessionId) {
|
||||||
|
throw new http_1.ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "session_id is required", 400);
|
||||||
|
}
|
||||||
|
if (!title) {
|
||||||
|
throw new http_1.ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "title 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);
|
||||||
|
}
|
||||||
|
const questions = session.items
|
||||||
|
.filter((item) => item.role === "user")
|
||||||
|
.map((item) => sanitizeGeneratedQuestion(item.text))
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
if (questions.length === 0) {
|
||||||
|
throw new http_1.ApiError("ASSISTANT_SESSION_EMPTY", "Assistant session has no user questions to save.", 400);
|
||||||
|
}
|
||||||
|
const generationId = generateAutogenId();
|
||||||
|
const caseSetFile = buildAutogenCaseSetFileName("saved_user_sessions", generationId);
|
||||||
|
const caseSetPath = path_1.default.resolve(config_1.EVAL_CASES_DIR, caseSetFile);
|
||||||
|
writeJsonFile(caseSetPath, buildSavedSessionCaseSetPayload({
|
||||||
|
generationId,
|
||||||
|
title,
|
||||||
|
questions
|
||||||
|
}));
|
||||||
|
const snapshotFile = writeSavedAssistantSessionSnapshot({
|
||||||
|
generationId,
|
||||||
|
sessionId,
|
||||||
|
title,
|
||||||
|
session: session,
|
||||||
|
questions
|
||||||
|
});
|
||||||
|
const record = {
|
||||||
|
generation_id: generationId,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
mode: "saved_user_sessions",
|
||||||
|
title,
|
||||||
|
count: questions.length,
|
||||||
|
domain: null,
|
||||||
|
questions,
|
||||||
|
generated_by: generatedBy,
|
||||||
|
saved_case_set_file: caseSetFile,
|
||||||
|
context: {
|
||||||
|
llm_provider: toStringSafe(context?.llm_provider),
|
||||||
|
model: toStringSafe(context?.model),
|
||||||
|
assistant_prompt_version: toStringSafe(context?.assistant_prompt_version),
|
||||||
|
decomposition_prompt_version: toStringSafe(context?.decomposition_prompt_version),
|
||||||
|
prompt_fingerprint: toStringSafe(context?.prompt_fingerprint)
|
||||||
|
? repairAutogenMojibake(String(context?.prompt_fingerprint))
|
||||||
|
: null,
|
||||||
|
autogen_personality_id: null,
|
||||||
|
autogen_personality_prompt: null,
|
||||||
|
source_session_id: sessionId,
|
||||||
|
saved_session_file: snapshotFile,
|
||||||
|
saved_case_set_kind: "assistant_session_scenario"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const history = readAutoGenHistory();
|
||||||
|
history.unshift(record);
|
||||||
|
writeAutoGenHistory(history.slice(0, 500));
|
||||||
|
(0, http_1.ok)(res, {
|
||||||
|
ok: true,
|
||||||
|
generation: record
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.patch("/api/autoruns/autogen/history/:generation_id/questions", (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const generationId = toStringSafe(req.params.generation_id);
|
||||||
|
const body = toRecord(req.body);
|
||||||
|
if (!generationId) {
|
||||||
|
throw new http_1.ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
|
||||||
|
}
|
||||||
|
if (!body) {
|
||||||
|
throw new http_1.ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "JSON body is required", 400);
|
||||||
|
}
|
||||||
|
const questions = parseAssistantSessionQuestions(body.questions);
|
||||||
|
if (questions.length === 0) {
|
||||||
|
throw new http_1.ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "questions must contain at least one item", 400);
|
||||||
|
}
|
||||||
|
const history = readAutoGenHistory();
|
||||||
|
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
|
||||||
|
if (targetIndex < 0) {
|
||||||
|
throw new http_1.ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
|
||||||
|
}
|
||||||
|
const current = history[targetIndex];
|
||||||
|
const updated = {
|
||||||
|
...current,
|
||||||
|
count: questions.length,
|
||||||
|
questions
|
||||||
|
};
|
||||||
|
rewriteAutoGenCaseSetFile(updated);
|
||||||
|
history[targetIndex] = updated;
|
||||||
|
writeAutoGenHistory(history);
|
||||||
|
(0, http_1.ok)(res, {
|
||||||
|
ok: true,
|
||||||
|
generation: updated
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.delete("/api/autoruns/autogen/history/:generation_id", (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const generationId = toStringSafe(req.params.generation_id);
|
||||||
|
if (!generationId) {
|
||||||
|
throw new http_1.ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
|
||||||
|
}
|
||||||
|
const history = readAutoGenHistory();
|
||||||
|
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
|
||||||
|
if (targetIndex < 0) {
|
||||||
|
throw new http_1.ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
|
||||||
|
}
|
||||||
|
const target = history[targetIndex];
|
||||||
|
const deletedFiles = [];
|
||||||
|
const caseSetPath = resolveFileInsideDir(config_1.EVAL_CASES_DIR, target.saved_case_set_file);
|
||||||
|
const savedSessionPath = resolveFileInsideDir(path_1.default.resolve(path_1.default.dirname(config_1.AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions"), target.context?.saved_session_file ?? null);
|
||||||
|
const deletedCaseSet = safeDeleteFile(caseSetPath);
|
||||||
|
if (deletedCaseSet) {
|
||||||
|
deletedFiles.push(deletedCaseSet);
|
||||||
|
}
|
||||||
|
const deletedSavedSession = safeDeleteFile(savedSessionPath);
|
||||||
|
if (deletedSavedSession) {
|
||||||
|
deletedFiles.push(deletedSavedSession);
|
||||||
|
}
|
||||||
|
history.splice(targetIndex, 1);
|
||||||
|
writeAutoGenHistory(history);
|
||||||
|
(0, http_1.ok)(res, {
|
||||||
|
ok: true,
|
||||||
|
generation_id: generationId,
|
||||||
|
deleted_files: deletedFiles
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = toRecord(req.body);
|
const body = toRecord(req.body);
|
||||||
@@ -1925,6 +2196,9 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||||||
const context = toRecord(body.context);
|
const context = toRecord(body.context);
|
||||||
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
||||||
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
|
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
|
||||||
|
if (mode === "saved_user_sessions") {
|
||||||
|
throw new http_1.ApiError("AUTOGEN_MODE_NOT_SUPPORTED", "Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.", 400);
|
||||||
|
}
|
||||||
let questions = [];
|
let questions = [];
|
||||||
if (mode === "qwen_seed") {
|
if (mode === "qwen_seed") {
|
||||||
if (!llmConfig) {
|
if (!llmConfig) {
|
||||||
@@ -1963,6 +2237,7 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||||||
generation_id: generationId,
|
generation_id: generationId,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
mode,
|
mode,
|
||||||
|
title: null,
|
||||||
count: questions.length,
|
count: questions.length,
|
||||||
domain,
|
domain,
|
||||||
questions,
|
questions,
|
||||||
@@ -1980,7 +2255,10 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||||||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
||||||
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
||||||
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
||||||
: null
|
: null,
|
||||||
|
source_session_id: null,
|
||||||
|
saved_session_file: null,
|
||||||
|
saved_case_set_kind: "single_turn_list"
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
};
|
};
|
||||||
|
|||||||
+52
-8
@@ -128,14 +128,23 @@ function splitQuestionCandidate(raw) {
|
|||||||
}
|
}
|
||||||
return normalizeRuntimeQuestionList(chunks);
|
return normalizeRuntimeQuestionList(chunks);
|
||||||
}
|
}
|
||||||
function normalizeRuntimeQuestions(value) {
|
function normalizeRuntimeQuestions(value, options) {
|
||||||
const raw = toArray(value)
|
const raw = toArray(value)
|
||||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
||||||
.filter((item) => item.length > 0);
|
.filter((item) => item.length > 0);
|
||||||
if (raw.length === 0) {
|
if (raw.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const expanded = normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)));
|
const splitCandidates = options?.splitCandidates ?? true;
|
||||||
|
const expanded = splitCandidates
|
||||||
|
? normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)))
|
||||||
|
: raw
|
||||||
|
.map((item) => normalizeQuestionChunk(item))
|
||||||
|
.filter((item) => Boolean(item));
|
||||||
|
const dedupe = options?.dedupe ?? true;
|
||||||
|
if (!dedupe) {
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
const deduped = [];
|
const deduped = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
for (const item of expanded) {
|
for (const item of expanded) {
|
||||||
@@ -272,6 +281,37 @@ function writeRuntimeAssistantSuiteFromQuestions(jobId, questions) {
|
|||||||
fs_1.default.writeFileSync(path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName), JSON.stringify(payload, null, 2), "utf-8");
|
fs_1.default.writeFileSync(path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName), JSON.stringify(payload, null, 2), "utf-8");
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
function writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, questions, title) {
|
||||||
|
if (!fs_1.default.existsSync(config_1.EVAL_CASES_DIR)) {
|
||||||
|
fs_1.default.mkdirSync(config_1.EVAL_CASES_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
const turns = questions.map((question) => ({
|
||||||
|
user_message: question
|
||||||
|
}));
|
||||||
|
const payload = {
|
||||||
|
suite_id: `assistant_saved_session_runtime_${jobId}`,
|
||||||
|
suite_version: "0.1.0",
|
||||||
|
schema_version: "assistant_saved_session_runtime_v0_1",
|
||||||
|
title: typeof title === "string" ? title.trim() || null : null,
|
||||||
|
scenario_count: turns.length > 0 ? 1 : 0,
|
||||||
|
case_ids: turns.length > 0 ? ["SAVED-001"] : [],
|
||||||
|
cases: turns.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
case_id: "SAVED-001",
|
||||||
|
scenario_tag: "saved_user_sessions_runtime",
|
||||||
|
title: typeof title === "string" ? title.trim() || null : null,
|
||||||
|
question_type: turns.length > 1 ? "followup" : "direct",
|
||||||
|
broadness_level: "medium",
|
||||||
|
turns
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
const fileName = `assistant_saved_session_runtime_${jobId}.json`;
|
||||||
|
fs_1.default.writeFileSync(path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName), JSON.stringify(payload, null, 2), "utf-8");
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
function readSessionConversation(runId, caseId) {
|
function readSessionConversation(runId, caseId) {
|
||||||
const sessionId = `${runId}-${caseId}`;
|
const sessionId = `${runId}-${caseId}`;
|
||||||
const filePath = path_1.default.resolve(config_1.ASSISTANT_SESSIONS_DIR, `${sessionId}.json`);
|
const filePath = path_1.default.resolve(config_1.ASSISTANT_SESSIONS_DIR, `${sessionId}.json`);
|
||||||
@@ -414,15 +454,19 @@ function buildEvalRouter(services) {
|
|||||||
throw new http_1.ApiError("UNSUPPORTED_ASYNC_EVAL_TARGET", "Async eval currently supports assistant_stage1 only.", 400);
|
throw new http_1.ApiError("UNSUPPORTED_ASYNC_EVAL_TARGET", "Async eval currently supports assistant_stage1 only.", 400);
|
||||||
}
|
}
|
||||||
const questions = normalizeRuntimeQuestions(body.questions);
|
const questions = normalizeRuntimeQuestions(body.questions);
|
||||||
|
const scenarioQuestions = normalizeRuntimeQuestions(body.scenarioQuestions, { dedupe: false, splitCandidates: false });
|
||||||
|
const scenarioTitle = toStringSafe(body.scenarioTitle);
|
||||||
const jobId = `job-${(0, nanoid_1.nanoid)(10)}`;
|
const jobId = `job-${(0, nanoid_1.nanoid)(10)}`;
|
||||||
const runId = `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
|
const runId = `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
|
||||||
const runtimeCaseSetFile = questions.length > 0
|
const runtimeCaseSetFile = scenarioQuestions.length > 0
|
||||||
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
? writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, scenarioQuestions, scenarioTitle ?? undefined)
|
||||||
: payload.caseSetFile
|
: questions.length > 0
|
||||||
? payload.caseSetFile
|
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
||||||
: undefined;
|
: payload.caseSetFile
|
||||||
|
? payload.caseSetFile
|
||||||
|
: undefined;
|
||||||
if (!runtimeCaseSetFile) {
|
if (!runtimeCaseSetFile) {
|
||||||
throw new http_1.ApiError("ASYNC_CASESET_REQUIRED", "Async assistant_stage1 run requires caseSetFile or explicit questions[] payload.", 400);
|
throw new http_1.ApiError("ASYNC_CASESET_REQUIRED", "Async assistant_stage1 run requires caseSetFile, scenarioQuestions[] or explicit questions[] payload.", 400);
|
||||||
}
|
}
|
||||||
const caseSeeds = readAssistantSuiteCaseSeeds(runtimeCaseSetFile);
|
const caseSeeds = readAssistantSuiteCaseSeeds(runtimeCaseSetFile);
|
||||||
if (caseSeeds.length === 0) {
|
if (caseSeeds.length === 0) {
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ function createApp() {
|
|||||||
app.use((0, normalize_1.buildNormalizeRouter)(services));
|
app.use((0, normalize_1.buildNormalizeRouter)(services));
|
||||||
app.use((0, eval_1.buildEvalRouter)(services));
|
app.use((0, eval_1.buildEvalRouter)(services));
|
||||||
app.use((0, assistant_1.buildAssistantRouter)(services));
|
app.use((0, assistant_1.buildAssistantRouter)(services));
|
||||||
app.use((0, autoRuns_1.buildAutoRunsRouter)(openaiClient));
|
app.use((0, autoRuns_1.buildAutoRunsRouter)(services, openaiClient));
|
||||||
app.use((0, history_1.buildHistoryRouter)());
|
app.use((0, history_1.buildHistoryRouter)());
|
||||||
app.use((0, presets_1.buildPresetsRouter)());
|
app.use((0, presets_1.buildPresetsRouter)());
|
||||||
app.use((0, accountingAgent_1.buildAccountingAgentRouter)(services));
|
app.use((0, accountingAgent_1.buildAccountingAgentRouter)(services));
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import {
|
|||||||
MANUAL_CASE_DECISION_SCHEMA_FILE,
|
MANUAL_CASE_DECISION_SCHEMA_FILE,
|
||||||
REPORTS_DIR
|
REPORTS_DIR
|
||||||
} from "../config";
|
} from "../config";
|
||||||
|
import type { AppServices } from "../serverContext";
|
||||||
import { ApiError, ok } from "../utils/http";
|
import { ApiError, ok } from "../utils/http";
|
||||||
import { loadCapabilitiesRegistry, resolveNearestCapabilityGroup, type CapabilityGroup } from "../services/capabilitiesRegistry";
|
import { loadCapabilitiesRegistry, resolveNearestCapabilityGroup, type CapabilityGroup } from "../services/capabilitiesRegistry";
|
||||||
import { OpenAIResponsesClient } from "../services/openaiResponsesClient";
|
import { OpenAIResponsesClient } from "../services/openaiResponsesClient";
|
||||||
|
|
||||||
type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
|
type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
|
||||||
type AutoRunTrend = "up" | "down" | "flat";
|
type AutoRunTrend = "up" | "down" | "flat";
|
||||||
type AutoGenMode = "qwen_seed" | "codex_creative";
|
type AutoGenMode = "qwen_seed" | "codex_creative" | "saved_user_sessions";
|
||||||
type ManualCaseDecision =
|
type ManualCaseDecision =
|
||||||
| "covered_ok"
|
| "covered_ok"
|
||||||
| "covered_but_bad_answer"
|
| "covered_but_bad_answer"
|
||||||
@@ -175,6 +176,7 @@ interface AutoGenHistoryRecord {
|
|||||||
generation_id: string;
|
generation_id: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
mode: AutoGenMode;
|
mode: AutoGenMode;
|
||||||
|
title: string | null;
|
||||||
count: number;
|
count: number;
|
||||||
domain: string | null;
|
domain: string | null;
|
||||||
questions: string[];
|
questions: string[];
|
||||||
@@ -188,6 +190,9 @@ interface AutoGenHistoryRecord {
|
|||||||
prompt_fingerprint: string | null;
|
prompt_fingerprint: string | null;
|
||||||
autogen_personality_id: string | null;
|
autogen_personality_id: string | null;
|
||||||
autogen_personality_prompt: string | null;
|
autogen_personality_prompt: string | null;
|
||||||
|
source_session_id?: string | null;
|
||||||
|
saved_session_file?: string | null;
|
||||||
|
saved_case_set_kind?: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +274,18 @@ function clampInt(value: number | null, min: number, max: number, fallback: numb
|
|||||||
return rounded;
|
return rounded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAutoGenMode(value: unknown): value is AutoGenMode {
|
||||||
|
return value === "qwen_seed" || value === "codex_creative" || value === "saved_user_sessions";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAutoGenTitle(value: unknown): string | null {
|
||||||
|
const title = toStringSafe(value);
|
||||||
|
if (!title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return title.slice(0, 160);
|
||||||
|
}
|
||||||
|
|
||||||
function parseManualCaseDecision(value: unknown, fallback: ManualCaseDecision = "needs_dialog_policy_fix"): ManualCaseDecision {
|
function parseManualCaseDecision(value: unknown, fallback: ManualCaseDecision = "needs_dialog_policy_fix"): ManualCaseDecision {
|
||||||
const normalized = toStringSafe(value);
|
const normalized = toStringSafe(value);
|
||||||
if (!normalized) return fallback;
|
if (!normalized) return fallback;
|
||||||
@@ -326,15 +343,11 @@ function readAutoGenHistory(): AutoGenHistoryRecord[] {
|
|||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
generation_id: toStringSafe(item.generation_id) ?? "",
|
generation_id: toStringSafe(item.generation_id) ?? "",
|
||||||
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
|
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
|
||||||
mode: (toStringSafe(item.mode) as AutoGenMode | null) ?? "codex_creative",
|
mode: isAutoGenMode(toStringSafe(item.mode)) ? (toStringSafe(item.mode) as AutoGenMode) : "codex_creative",
|
||||||
|
title: parseAutoGenTitle(item.title),
|
||||||
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
|
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
|
||||||
domain: toStringSafe(item.domain),
|
domain: toStringSafe(item.domain),
|
||||||
questions: toArray(item.questions)
|
questions: parseAssistantSessionQuestions(item.questions),
|
||||||
.map((q) => toStringSafe(q))
|
|
||||||
.filter((q): q is string => q !== null)
|
|
||||||
.map((q) => sanitizeGeneratedQuestion(q))
|
|
||||||
.filter((q) => q.length > 0)
|
|
||||||
.slice(0, 500),
|
|
||||||
generated_by: toStringSafe(item.generated_by),
|
generated_by: toStringSafe(item.generated_by),
|
||||||
saved_case_set_file: toStringSafe(item.saved_case_set_file),
|
saved_case_set_file: toStringSafe(item.saved_case_set_file),
|
||||||
context: toRecord(item.context)
|
context: toRecord(item.context)
|
||||||
@@ -349,7 +362,10 @@ function readAutoGenHistory(): AutoGenHistoryRecord[] {
|
|||||||
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
|
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
|
||||||
autogen_personality_prompt: toStringSafe(toRecord(item.context)?.autogen_personality_prompt)
|
autogen_personality_prompt: toStringSafe(toRecord(item.context)?.autogen_personality_prompt)
|
||||||
? repairAutogenMojibake(String(toRecord(item.context)?.autogen_personality_prompt))
|
? repairAutogenMojibake(String(toRecord(item.context)?.autogen_personality_prompt))
|
||||||
: null
|
: null,
|
||||||
|
source_session_id: toStringSafe(toRecord(item.context)?.source_session_id),
|
||||||
|
saved_session_file: toStringSafe(toRecord(item.context)?.saved_session_file),
|
||||||
|
saved_case_set_kind: toStringSafe(toRecord(item.context)?.saved_case_set_kind)
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}))
|
}))
|
||||||
@@ -1314,7 +1330,7 @@ function parseDecisionFilter(value: unknown): ManualCaseDecision | "all" {
|
|||||||
|
|
||||||
function parseAutoGenMode(value: unknown): AutoGenMode {
|
function parseAutoGenMode(value: unknown): AutoGenMode {
|
||||||
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
|
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
|
||||||
if (normalized === "qwen_seed" || normalized === "codex_creative") {
|
if (normalized === "qwen_seed" || normalized === "codex_creative" || normalized === "saved_user_sessions") {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
return "codex_creative";
|
return "codex_creative";
|
||||||
@@ -1416,6 +1432,13 @@ function sanitizeGeneratedQuestion(value: string): string {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAssistantSessionQuestions(value: unknown): string[] {
|
||||||
|
return toArray(value)
|
||||||
|
.map((item) => sanitizeGeneratedQuestion(typeof item === "string" ? item : ""))
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
.slice(0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
|
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
|
||||||
const AUTOGEN_QUESTION_TAIL_PATTERNS: RegExp[] = [
|
const AUTOGEN_QUESTION_TAIL_PATTERNS: RegExp[] = [
|
||||||
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
|
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
|
||||||
@@ -1723,6 +1746,19 @@ function buildAutogenCaseSetFileName(mode: AutoGenMode, generationId: string): s
|
|||||||
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
|
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSavedAssistantSessionSnapshotFileName(generationId: string): string {
|
||||||
|
const now = new Date();
|
||||||
|
const stamp = [
|
||||||
|
now.getUTCFullYear(),
|
||||||
|
String(now.getUTCMonth() + 1).padStart(2, "0"),
|
||||||
|
String(now.getUTCDate()).padStart(2, "0"),
|
||||||
|
String(now.getUTCHours()).padStart(2, "0"),
|
||||||
|
String(now.getUTCMinutes()).padStart(2, "0"),
|
||||||
|
String(now.getUTCSeconds()).padStart(2, "0")
|
||||||
|
].join("");
|
||||||
|
return `assistant_saved_session_${stamp}_${generationId}.json`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildAutogenCaseSetPayload(input: {
|
function buildAutogenCaseSetPayload(input: {
|
||||||
generationId: string;
|
generationId: string;
|
||||||
mode: AutoGenMode;
|
mode: AutoGenMode;
|
||||||
@@ -1757,6 +1793,118 @@ function buildAutogenCaseSetPayload(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSavedSessionCaseSetPayload(input: {
|
||||||
|
generationId: string;
|
||||||
|
title: string | null;
|
||||||
|
questions: string[];
|
||||||
|
}): Record<string, unknown> {
|
||||||
|
const questions = parseAssistantSessionQuestions(input.questions);
|
||||||
|
const turns = questions.map((question) => ({
|
||||||
|
user_message: question
|
||||||
|
}));
|
||||||
|
const caseId = "SAVED-001";
|
||||||
|
return {
|
||||||
|
suite_id: `assistant_saved_session_${input.generationId}`,
|
||||||
|
suite_version: "0.1.0",
|
||||||
|
schema_version: "assistant_saved_session_suite_v0_1",
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
generation_id: input.generationId,
|
||||||
|
mode: "saved_user_sessions",
|
||||||
|
title: input.title,
|
||||||
|
scenario_count: turns.length > 0 ? 1 : 0,
|
||||||
|
case_ids: turns.length > 0 ? [caseId] : [],
|
||||||
|
cases:
|
||||||
|
turns.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
case_id: caseId,
|
||||||
|
scenario_tag: "saved_user_sessions",
|
||||||
|
title: input.title,
|
||||||
|
question_type: turns.length > 1 ? "followup" : "direct",
|
||||||
|
broadness_level: "medium",
|
||||||
|
turns
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDirSync(targetDir: string): void {
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJsonFile(targetPath: string, payload: unknown): void {
|
||||||
|
ensureDirSync(path.dirname(targetPath));
|
||||||
|
fs.writeFileSync(targetPath, JSON.stringify(payload, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteAutoGenCaseSetFile(record: AutoGenHistoryRecord): string | null {
|
||||||
|
const caseSetFile = toStringSafe(record.saved_case_set_file);
|
||||||
|
if (!caseSetFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const targetPath = path.resolve(EVAL_CASES_DIR, caseSetFile);
|
||||||
|
const payload =
|
||||||
|
record.mode === "saved_user_sessions"
|
||||||
|
? buildSavedSessionCaseSetPayload({
|
||||||
|
generationId: record.generation_id,
|
||||||
|
title: record.title,
|
||||||
|
questions: record.questions
|
||||||
|
})
|
||||||
|
: buildAutogenCaseSetPayload({
|
||||||
|
generationId: record.generation_id,
|
||||||
|
mode: record.mode,
|
||||||
|
domain: record.domain,
|
||||||
|
questions: record.questions
|
||||||
|
});
|
||||||
|
writeJsonFile(targetPath, payload);
|
||||||
|
return caseSetFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSavedAssistantSessionSnapshot(input: {
|
||||||
|
generationId: string;
|
||||||
|
sessionId: string;
|
||||||
|
title: string | null;
|
||||||
|
session: Record<string, unknown>;
|
||||||
|
questions: string[];
|
||||||
|
}): string {
|
||||||
|
const fileName = buildSavedAssistantSessionSnapshotFileName(input.generationId);
|
||||||
|
const targetPath = path.resolve(path.dirname(AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions", fileName);
|
||||||
|
writeJsonFile(targetPath, {
|
||||||
|
saved_at: new Date().toISOString(),
|
||||||
|
generation_id: input.generationId,
|
||||||
|
mode: "saved_user_sessions",
|
||||||
|
title: input.title,
|
||||||
|
source_session_id: input.sessionId,
|
||||||
|
questions: input.questions,
|
||||||
|
session: input.session
|
||||||
|
});
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFileInsideDir(baseDir: string, fileName: string | null | undefined): string | null {
|
||||||
|
const normalized = toStringSafe(fileName);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const targetPath = path.resolve(baseDir, normalized);
|
||||||
|
const relative = path.relative(baseDir, targetPath);
|
||||||
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDeleteFile(targetPath: string | null): string | null {
|
||||||
|
if (!targetPath || !fs.existsSync(targetPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
fs.unlinkSync(targetPath);
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
function collectPostAnalysis(
|
function collectPostAnalysis(
|
||||||
annotations: AutoRunAnnotationRecord[],
|
annotations: AutoRunAnnotationRecord[],
|
||||||
runMap: Map<string, IndexedRun>,
|
runMap: Map<string, IndexedRun>,
|
||||||
@@ -1854,7 +2002,7 @@ function collectPostAnalysis(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()): Router {
|
export function buildAutoRunsRouter(services: AppServices, openaiClient = new OpenAIResponsesClient()): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/api/autoruns/history", (req, res) => {
|
router.get("/api/autoruns/history", (req, res) => {
|
||||||
@@ -2251,7 +2399,7 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||||||
try {
|
try {
|
||||||
const limit = clampInt(toNumberSafe((req.query as Record<string, unknown>).limit), 1, 500, 120);
|
const limit = clampInt(toNumberSafe((req.query as Record<string, unknown>).limit), 1, 500, 120);
|
||||||
const rawMode = toStringSafe((req.query as Record<string, unknown>).mode);
|
const rawMode = toStringSafe((req.query as Record<string, unknown>).mode);
|
||||||
const includeAllModes = !rawMode || !["qwen_seed", "codex_creative"].includes(rawMode);
|
const includeAllModes = !rawMode || !isAutoGenMode(rawMode);
|
||||||
const modeFilter = (rawMode as AutoGenMode | null) ?? "codex_creative";
|
const modeFilter = (rawMode as AutoGenMode | null) ?? "codex_creative";
|
||||||
const items = readAutoGenHistory()
|
const items = readAutoGenHistory()
|
||||||
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
|
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
|
||||||
@@ -2279,6 +2427,182 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/api/autoruns/autogen/save-assistant-session", (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const body = toRecord(req.body);
|
||||||
|
if (!body) {
|
||||||
|
throw new ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "JSON body is required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = toStringSafe(body.session_id);
|
||||||
|
const title = parseAutoGenTitle(body.title);
|
||||||
|
const generatedBy = parseAnnotationAuthor(body.generated_by);
|
||||||
|
const context = toRecord(body.context);
|
||||||
|
|
||||||
|
if (!sessionId) {
|
||||||
|
throw new ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "session_id is required", 400);
|
||||||
|
}
|
||||||
|
if (!title) {
|
||||||
|
throw new ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "title is required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = services.assistantService.getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
throw new ApiError("ASSISTANT_SESSION_NOT_FOUND", `Session not found: ${sessionId}`, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const questions = session.items
|
||||||
|
.filter((item: { role: string }) => item.role === "user")
|
||||||
|
.map((item: { text: string }) => sanitizeGeneratedQuestion(item.text))
|
||||||
|
.filter((item: string) => item.length > 0);
|
||||||
|
|
||||||
|
if (questions.length === 0) {
|
||||||
|
throw new ApiError("ASSISTANT_SESSION_EMPTY", "Assistant session has no user questions to save.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generationId = generateAutogenId();
|
||||||
|
const caseSetFile = buildAutogenCaseSetFileName("saved_user_sessions", generationId);
|
||||||
|
const caseSetPath = path.resolve(EVAL_CASES_DIR, caseSetFile);
|
||||||
|
writeJsonFile(
|
||||||
|
caseSetPath,
|
||||||
|
buildSavedSessionCaseSetPayload({
|
||||||
|
generationId,
|
||||||
|
title,
|
||||||
|
questions
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotFile = writeSavedAssistantSessionSnapshot({
|
||||||
|
generationId,
|
||||||
|
sessionId,
|
||||||
|
title,
|
||||||
|
session: session as unknown as Record<string, unknown>,
|
||||||
|
questions
|
||||||
|
});
|
||||||
|
|
||||||
|
const record: AutoGenHistoryRecord = {
|
||||||
|
generation_id: generationId,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
mode: "saved_user_sessions",
|
||||||
|
title,
|
||||||
|
count: questions.length,
|
||||||
|
domain: null,
|
||||||
|
questions,
|
||||||
|
generated_by: generatedBy,
|
||||||
|
saved_case_set_file: caseSetFile,
|
||||||
|
context: {
|
||||||
|
llm_provider: toStringSafe(context?.llm_provider),
|
||||||
|
model: toStringSafe(context?.model),
|
||||||
|
assistant_prompt_version: toStringSafe(context?.assistant_prompt_version),
|
||||||
|
decomposition_prompt_version: toStringSafe(context?.decomposition_prompt_version),
|
||||||
|
prompt_fingerprint: toStringSafe(context?.prompt_fingerprint)
|
||||||
|
? repairAutogenMojibake(String(context?.prompt_fingerprint))
|
||||||
|
: null,
|
||||||
|
autogen_personality_id: null,
|
||||||
|
autogen_personality_prompt: null,
|
||||||
|
source_session_id: sessionId,
|
||||||
|
saved_session_file: snapshotFile,
|
||||||
|
saved_case_set_kind: "assistant_session_scenario"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const history = readAutoGenHistory();
|
||||||
|
history.unshift(record);
|
||||||
|
writeAutoGenHistory(history.slice(0, 500));
|
||||||
|
|
||||||
|
ok(res, {
|
||||||
|
ok: true,
|
||||||
|
generation: record
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.patch("/api/autoruns/autogen/history/:generation_id/questions", (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const generationId = toStringSafe(req.params.generation_id);
|
||||||
|
const body = toRecord(req.body);
|
||||||
|
if (!generationId) {
|
||||||
|
throw new ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
|
||||||
|
}
|
||||||
|
if (!body) {
|
||||||
|
throw new ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "JSON body is required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const questions = parseAssistantSessionQuestions(body.questions);
|
||||||
|
if (questions.length === 0) {
|
||||||
|
throw new ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "questions must contain at least one item", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = readAutoGenHistory();
|
||||||
|
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
|
||||||
|
if (targetIndex < 0) {
|
||||||
|
throw new ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = history[targetIndex];
|
||||||
|
const updated: AutoGenHistoryRecord = {
|
||||||
|
...current,
|
||||||
|
count: questions.length,
|
||||||
|
questions
|
||||||
|
};
|
||||||
|
rewriteAutoGenCaseSetFile(updated);
|
||||||
|
history[targetIndex] = updated;
|
||||||
|
writeAutoGenHistory(history);
|
||||||
|
|
||||||
|
ok(res, {
|
||||||
|
ok: true,
|
||||||
|
generation: updated
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/api/autoruns/autogen/history/:generation_id", (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const generationId = toStringSafe(req.params.generation_id);
|
||||||
|
if (!generationId) {
|
||||||
|
throw new ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = readAutoGenHistory();
|
||||||
|
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
|
||||||
|
if (targetIndex < 0) {
|
||||||
|
throw new ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = history[targetIndex];
|
||||||
|
const deletedFiles: string[] = [];
|
||||||
|
const caseSetPath = resolveFileInsideDir(EVAL_CASES_DIR, target.saved_case_set_file);
|
||||||
|
const savedSessionPath = resolveFileInsideDir(
|
||||||
|
path.resolve(path.dirname(AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions"),
|
||||||
|
target.context?.saved_session_file ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCaseSet = safeDeleteFile(caseSetPath);
|
||||||
|
if (deletedCaseSet) {
|
||||||
|
deletedFiles.push(deletedCaseSet);
|
||||||
|
}
|
||||||
|
const deletedSavedSession = safeDeleteFile(savedSessionPath);
|
||||||
|
if (deletedSavedSession) {
|
||||||
|
deletedFiles.push(deletedSavedSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
history.splice(targetIndex, 1);
|
||||||
|
writeAutoGenHistory(history);
|
||||||
|
|
||||||
|
ok(res, {
|
||||||
|
ok: true,
|
||||||
|
generation_id: generationId,
|
||||||
|
deleted_files: deletedFiles
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = toRecord(req.body);
|
const body = toRecord(req.body);
|
||||||
@@ -2294,6 +2618,14 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||||||
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
||||||
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
|
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
|
||||||
|
|
||||||
|
if (mode === "saved_user_sessions") {
|
||||||
|
throw new ApiError(
|
||||||
|
"AUTOGEN_MODE_NOT_SUPPORTED",
|
||||||
|
"Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.",
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let questions: string[] = [];
|
let questions: string[] = [];
|
||||||
if (mode === "qwen_seed") {
|
if (mode === "qwen_seed") {
|
||||||
if (!llmConfig) {
|
if (!llmConfig) {
|
||||||
@@ -2340,6 +2672,7 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||||||
generation_id: generationId,
|
generation_id: generationId,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
mode,
|
mode,
|
||||||
|
title: null,
|
||||||
count: questions.length,
|
count: questions.length,
|
||||||
domain,
|
domain,
|
||||||
questions,
|
questions,
|
||||||
@@ -2357,7 +2690,10 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||||||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
||||||
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
||||||
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
||||||
: null
|
: null,
|
||||||
|
source_session_id: null,
|
||||||
|
saved_session_file: null,
|
||||||
|
saved_case_set_kind: "single_turn_list"
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ function splitQuestionCandidate(raw: string): string[] {
|
|||||||
return normalizeRuntimeQuestionList(chunks);
|
return normalizeRuntimeQuestionList(chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRuntimeQuestions(value: unknown): string[] {
|
function normalizeRuntimeQuestions(value: unknown, options?: { dedupe?: boolean; splitCandidates?: boolean }): string[] {
|
||||||
const raw = toArray(value)
|
const raw = toArray(value)
|
||||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
||||||
.filter((item) => item.length > 0);
|
.filter((item) => item.length > 0);
|
||||||
@@ -184,7 +184,16 @@ function normalizeRuntimeQuestions(value: unknown): string[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const expanded = normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)));
|
const splitCandidates = options?.splitCandidates ?? true;
|
||||||
|
const expanded = splitCandidates
|
||||||
|
? normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)))
|
||||||
|
: raw
|
||||||
|
.map((item) => normalizeQuestionChunk(item))
|
||||||
|
.filter((item): item is string => Boolean(item));
|
||||||
|
const dedupe = options?.dedupe ?? true;
|
||||||
|
if (!dedupe) {
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
const deduped: string[] = [];
|
const deduped: string[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const item of expanded) {
|
for (const item of expanded) {
|
||||||
@@ -342,6 +351,39 @@ function writeRuntimeAssistantSuiteFromQuestions(jobId: string, questions: strin
|
|||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeRuntimeAssistantScenarioSuiteFromQuestions(jobId: string, questions: string[], title?: string): string {
|
||||||
|
if (!fs.existsSync(EVAL_CASES_DIR)) {
|
||||||
|
fs.mkdirSync(EVAL_CASES_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
const turns = questions.map((question) => ({
|
||||||
|
user_message: question
|
||||||
|
}));
|
||||||
|
const payload = {
|
||||||
|
suite_id: `assistant_saved_session_runtime_${jobId}`,
|
||||||
|
suite_version: "0.1.0",
|
||||||
|
schema_version: "assistant_saved_session_runtime_v0_1",
|
||||||
|
title: typeof title === "string" ? title.trim() || null : null,
|
||||||
|
scenario_count: turns.length > 0 ? 1 : 0,
|
||||||
|
case_ids: turns.length > 0 ? ["SAVED-001"] : [],
|
||||||
|
cases:
|
||||||
|
turns.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
case_id: "SAVED-001",
|
||||||
|
scenario_tag: "saved_user_sessions_runtime",
|
||||||
|
title: typeof title === "string" ? title.trim() || null : null,
|
||||||
|
question_type: turns.length > 1 ? "followup" : "direct",
|
||||||
|
broadness_level: "medium",
|
||||||
|
turns
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
const fileName = `assistant_saved_session_runtime_${jobId}.json`;
|
||||||
|
fs.writeFileSync(path.resolve(EVAL_CASES_DIR, fileName), JSON.stringify(payload, null, 2), "utf-8");
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
function readSessionConversation(runId: string, caseId: string): EvalAsyncCaseInfo["messages"] {
|
function readSessionConversation(runId: string, caseId: string): EvalAsyncCaseInfo["messages"] {
|
||||||
const sessionId = `${runId}-${caseId}`;
|
const sessionId = `${runId}-${caseId}`;
|
||||||
const filePath = path.resolve(ASSISTANT_SESSIONS_DIR, `${sessionId}.json`);
|
const filePath = path.resolve(ASSISTANT_SESSIONS_DIR, `${sessionId}.json`);
|
||||||
@@ -489,11 +531,15 @@ export function buildEvalRouter(services: AppServices): Router {
|
|||||||
throw new ApiError("UNSUPPORTED_ASYNC_EVAL_TARGET", "Async eval currently supports assistant_stage1 only.", 400);
|
throw new ApiError("UNSUPPORTED_ASYNC_EVAL_TARGET", "Async eval currently supports assistant_stage1 only.", 400);
|
||||||
}
|
}
|
||||||
const questions = normalizeRuntimeQuestions(body.questions);
|
const questions = normalizeRuntimeQuestions(body.questions);
|
||||||
|
const scenarioQuestions = normalizeRuntimeQuestions(body.scenarioQuestions, { dedupe: false, splitCandidates: false });
|
||||||
|
const scenarioTitle = toStringSafe(body.scenarioTitle);
|
||||||
|
|
||||||
const jobId = `job-${nanoid(10)}`;
|
const jobId = `job-${nanoid(10)}`;
|
||||||
const runId = `assistant-stage1-${nanoid(10)}`;
|
const runId = `assistant-stage1-${nanoid(10)}`;
|
||||||
const runtimeCaseSetFile =
|
const runtimeCaseSetFile =
|
||||||
questions.length > 0
|
scenarioQuestions.length > 0
|
||||||
|
? writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, scenarioQuestions, scenarioTitle ?? undefined)
|
||||||
|
: questions.length > 0
|
||||||
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
||||||
: payload.caseSetFile
|
: payload.caseSetFile
|
||||||
? payload.caseSetFile
|
? payload.caseSetFile
|
||||||
@@ -502,7 +548,7 @@ export function buildEvalRouter(services: AppServices): Router {
|
|||||||
if (!runtimeCaseSetFile) {
|
if (!runtimeCaseSetFile) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
"ASYNC_CASESET_REQUIRED",
|
"ASYNC_CASESET_REQUIRED",
|
||||||
"Async assistant_stage1 run requires caseSetFile or explicit questions[] payload.",
|
"Async assistant_stage1 run requires caseSetFile, scenarioQuestions[] or explicit questions[] payload.",
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function createApp(): express.Express {
|
|||||||
app.use(buildNormalizeRouter(services));
|
app.use(buildNormalizeRouter(services));
|
||||||
app.use(buildEvalRouter(services));
|
app.use(buildEvalRouter(services));
|
||||||
app.use(buildAssistantRouter(services));
|
app.use(buildAssistantRouter(services));
|
||||||
app.use(buildAutoRunsRouter(openaiClient));
|
app.use(buildAutoRunsRouter(services, openaiClient));
|
||||||
app.use(buildHistoryRouter());
|
app.use(buildHistoryRouter());
|
||||||
app.use(buildPresetsRouter());
|
app.use(buildPresetsRouter());
|
||||||
app.use(buildAccountingAgentRouter(services));
|
app.use(buildAccountingAgentRouter(services));
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+215
@@ -0,0 +1,215 @@
|
|||||||
|
{
|
||||||
|
"saved_at": "2026-04-16T17:51:50.474Z",
|
||||||
|
"generation_id": "gen-mo1s0m9z-ndf56a3",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "Ручная сессия 16.04.2026, 20:51:30",
|
||||||
|
"source_session_id": "asst-iIpNheKZpP",
|
||||||
|
"questions": [
|
||||||
|
"привет как дела"
|
||||||
|
],
|
||||||
|
"session": {
|
||||||
|
"session_id": "asst-iIpNheKZpP",
|
||||||
|
"updated_at": "2026-04-16T17:51:30.711Z",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"message_id": "msg-R2if3yso9x",
|
||||||
|
"session_id": "asst-iIpNheKZpP",
|
||||||
|
"role": "user",
|
||||||
|
"text": "привет как дела",
|
||||||
|
"reply_type": null,
|
||||||
|
"created_at": "2026-04-16T17:51:19.402Z",
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "msg-tRiU0RadHy",
|
||||||
|
"session_id": "asst-iIpNheKZpP",
|
||||||
|
"role": "assistant",
|
||||||
|
"text": "Привет! У меня всё хорошо, спасибо за вопрос. А у тебя как дела? 😊",
|
||||||
|
"reply_type": "factual_with_explanation",
|
||||||
|
"created_at": "2026-04-16T17:51:30.711Z",
|
||||||
|
"trace_id": "chat-2RCiQ9Whq9",
|
||||||
|
"debug": {
|
||||||
|
"trace_id": "chat-2RCiQ9Whq9",
|
||||||
|
"prompt_version": "living_chat_router_v1",
|
||||||
|
"schema_version": "living_chat_router_v1",
|
||||||
|
"fallback_type": "none",
|
||||||
|
"detected_mode": "chat",
|
||||||
|
"detected_mode_confidence": "high",
|
||||||
|
"execution_lane": "living_chat",
|
||||||
|
"living_router_mode": "chat",
|
||||||
|
"living_router_reason": "non_domain_query_indexed",
|
||||||
|
"living_chat_response_source": "llm_chat",
|
||||||
|
"living_chat_script_guard_applied": false,
|
||||||
|
"living_chat_script_guard_reason": null,
|
||||||
|
"living_chat_grounding_guard_applied": false,
|
||||||
|
"living_chat_grounding_guard_reason": null,
|
||||||
|
"living_chat_data_scope_probe_status": null,
|
||||||
|
"living_chat_data_scope_probe_channel": null,
|
||||||
|
"living_chat_data_scope_probe_org_count": 0,
|
||||||
|
"living_chat_data_scope_probe_organizations": [],
|
||||||
|
"living_chat_data_scope_probe_error": null,
|
||||||
|
"living_chat_selected_organization": null,
|
||||||
|
"assistant_known_organizations": [],
|
||||||
|
"assistant_active_organization": null,
|
||||||
|
"address_llm_predecompose_attempted": true,
|
||||||
|
"address_llm_predecompose_applied": false,
|
||||||
|
"address_llm_predecompose_reason": "no_usable_fragment",
|
||||||
|
"address_llm_predecompose_contract": {
|
||||||
|
"schema_version": "address_llm_predecompose_contract_v1",
|
||||||
|
"source_message": "привет как дела",
|
||||||
|
"canonical_message": "привет как дела",
|
||||||
|
"mode": "unsupported",
|
||||||
|
"mode_confidence": "low",
|
||||||
|
"query_shape": "UNKNOWN",
|
||||||
|
"query_shape_confidence": "low",
|
||||||
|
"intent": "unknown",
|
||||||
|
"intent_confidence": "low",
|
||||||
|
"entities": {
|
||||||
|
"account": null,
|
||||||
|
"counterparty": null,
|
||||||
|
"contract": null,
|
||||||
|
"document_type": null,
|
||||||
|
"document_ref": null,
|
||||||
|
"organization": null
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"scope": "unspecified",
|
||||||
|
"period_from": null,
|
||||||
|
"period_to": null,
|
||||||
|
"as_of_date": null,
|
||||||
|
"has_explicit_period": false
|
||||||
|
},
|
||||||
|
"semantics": {
|
||||||
|
"scope_kind": "none",
|
||||||
|
"anchor_kind": "none",
|
||||||
|
"anchor_value": null,
|
||||||
|
"date_scope_kind": "none",
|
||||||
|
"date_basis_hint": null,
|
||||||
|
"self_scope_detected": false,
|
||||||
|
"selected_object_scope_detected": false
|
||||||
|
},
|
||||||
|
"aggregation_profile": "unknown"
|
||||||
|
},
|
||||||
|
"address_semantic_extraction_contract": {
|
||||||
|
"schema_version": "address_semantic_extraction_contract_v1",
|
||||||
|
"source_message": "привет как дела",
|
||||||
|
"canonical_message": "привет как дела",
|
||||||
|
"canonical_rewrite_applied": false,
|
||||||
|
"extraction": {
|
||||||
|
"mode": "unsupported",
|
||||||
|
"mode_confidence": "low",
|
||||||
|
"query_shape": "UNKNOWN",
|
||||||
|
"query_shape_confidence": "low",
|
||||||
|
"intent": "unknown",
|
||||||
|
"intent_confidence": "low",
|
||||||
|
"aggregation_profile": "unknown"
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"account": null,
|
||||||
|
"counterparty": null,
|
||||||
|
"contract": null,
|
||||||
|
"document_type": null,
|
||||||
|
"document_ref": null,
|
||||||
|
"organization": null
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"scope": "unspecified",
|
||||||
|
"period_from": null,
|
||||||
|
"period_to": null,
|
||||||
|
"as_of_date": null,
|
||||||
|
"has_explicit_period": false
|
||||||
|
},
|
||||||
|
"semantics": {
|
||||||
|
"scope_kind": "none",
|
||||||
|
"anchor_kind": "none",
|
||||||
|
"anchor_value": null,
|
||||||
|
"date_scope_kind": "none",
|
||||||
|
"date_basis_hint": null,
|
||||||
|
"self_scope_detected": false,
|
||||||
|
"selected_object_scope_detected": false
|
||||||
|
},
|
||||||
|
"guard_hints": {
|
||||||
|
"source_data_signal_detected": false,
|
||||||
|
"canonical_data_signal_detected": false,
|
||||||
|
"data_scope_meta_query_detected": false,
|
||||||
|
"deep_investigation_signal_detected": false,
|
||||||
|
"required_anchor_missing": false,
|
||||||
|
"unsupported_low_confidence": true,
|
||||||
|
"semantic_drift_suspected": false
|
||||||
|
},
|
||||||
|
"quality": "low",
|
||||||
|
"valid": false,
|
||||||
|
"apply_canonical_recommended": false,
|
||||||
|
"reason_codes": [
|
||||||
|
"unsupported_low_confidence_contract"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"orchestration_contract_v1": {
|
||||||
|
"schema_version": "assistant_orchestration_contract_v1",
|
||||||
|
"hard_meta_mode": "non_domain",
|
||||||
|
"address_mode": "unsupported",
|
||||||
|
"address_mode_confidence": "low",
|
||||||
|
"address_intent": "unknown",
|
||||||
|
"address_intent_confidence": "low",
|
||||||
|
"strong_data_signal_detected": false,
|
||||||
|
"data_retrieval_signal_detected": false,
|
||||||
|
"followup_context_detected": false,
|
||||||
|
"unsupported_address_intent_fallback_to_deep": false,
|
||||||
|
"final_decision": {
|
||||||
|
"run_address_lane": false,
|
||||||
|
"tool_gate_decision": "skip_address_lane",
|
||||||
|
"tool_gate_reason": "non_domain_query_indexed",
|
||||||
|
"living_mode": "chat",
|
||||||
|
"living_reason": "non_domain_query_indexed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tool_gate_decision": "skip_address_lane",
|
||||||
|
"tool_gate_reason": "non_domain_query_indexed",
|
||||||
|
"normalized": null,
|
||||||
|
"normalizer_output": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"investigation_state": {
|
||||||
|
"schema_version": "investigation_state_v1",
|
||||||
|
"session_id": "asst-iIpNheKZpP",
|
||||||
|
"status": "idle",
|
||||||
|
"turn_index": 0,
|
||||||
|
"updated_at": "2026-04-16T17:51:19.401Z",
|
||||||
|
"question_id": null,
|
||||||
|
"question_scope_id": null,
|
||||||
|
"scope_origin": null,
|
||||||
|
"focus": {
|
||||||
|
"domain": null,
|
||||||
|
"period": null,
|
||||||
|
"primary_accounts": [],
|
||||||
|
"active_query_subject": null
|
||||||
|
},
|
||||||
|
"narrowing_status": "unknown",
|
||||||
|
"evidence_refs": [],
|
||||||
|
"open_uncertainties": [],
|
||||||
|
"last_answer_mode": null,
|
||||||
|
"followup_context": null,
|
||||||
|
"query_mode_hint": "direct_answer"
|
||||||
|
},
|
||||||
|
"address_navigation_state": {
|
||||||
|
"schema_version": "address_navigation_state_v1",
|
||||||
|
"session_id": "asst-iIpNheKZpP",
|
||||||
|
"updated_at": "2026-04-16T17:51:19.401Z",
|
||||||
|
"session_context": {
|
||||||
|
"active_result_set_id": null,
|
||||||
|
"active_focus_object": null,
|
||||||
|
"last_confirmed_route": null,
|
||||||
|
"date_scope": {
|
||||||
|
"as_of_date": null,
|
||||||
|
"period_from": null,
|
||||||
|
"period_to": null
|
||||||
|
},
|
||||||
|
"organization_scope": null
|
||||||
|
},
|
||||||
|
"result_sets": [],
|
||||||
|
"navigation_history": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-878
File diff suppressed because one or more lines are too long
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_gen-mo1s0m9z-ndf56a3",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||||
|
"generated_at": "2026-04-16T17:51:50.471Z",
|
||||||
|
"generation_id": "gen-mo1s0m9z-ndf56a3",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "Ручная сессия 16.04.2026, 20:51:30",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "saved_user_sessions",
|
||||||
|
"title": "Ручная сессия 16.04.2026, 20:51:30",
|
||||||
|
"question_type": "direct",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "привет как дела"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_runtime_job-qvlcP-qH8S",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_runtime_v0_1",
|
||||||
|
"title": "Ручная сессия 16.04.2026, 20:51:30",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "saved_user_sessions_runtime",
|
||||||
|
"title": "Ручная сессия 16.04.2026, 20:51:30",
|
||||||
|
"question_type": "direct",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "привет как дела"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "shared_llm_connection_v1",
|
"schema_version": "shared_llm_connection_v1",
|
||||||
"updated_at": "2026-04-15T06:12:46.714Z",
|
"updated_at": "2026-04-16T17:54:57.636Z",
|
||||||
"connection": {
|
"connection": {
|
||||||
"llmProvider": "local",
|
"llmProvider": "local",
|
||||||
"model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
"model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NDC AI Normalizer Playground</title>
|
<title>NDC AI Normalizer Playground</title>
|
||||||
<script type="module" crossorigin src="/assets/index-VJV2AL7G.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bw40I8e3.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CaUiKcE3.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DkWsdP2H.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { apiClient } from "./api/client";
|
import { apiClient } from "./api/client";
|
||||||
import { AssistantSamPanel } from "./components/AssistantSamPanel";
|
|
||||||
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
|
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
|
||||||
import { AssistantPanel } from "./components/AssistantPanel";
|
|
||||||
import { ConnectionPanel } from "./components/ConnectionPanel";
|
import { ConnectionPanel } from "./components/ConnectionPanel";
|
||||||
import { HistoryPanel } from "./components/HistoryPanel";
|
import { HistoryPanel } from "./components/HistoryPanel";
|
||||||
import { MetricsPanel } from "./components/MetricsPanel";
|
import { MetricsPanel } from "./components/MetricsPanel";
|
||||||
import { OutputPanel } from "./components/OutputPanel";
|
import { OutputPanel } from "./components/OutputPanel";
|
||||||
import { PanelFrame } from "./components/PanelFrame";
|
|
||||||
import { PromptPanel } from "./components/PromptPanel";
|
import { PromptPanel } from "./components/PromptPanel";
|
||||||
import { QueryPanel } from "./components/QueryPanel";
|
import { QueryPanel } from "./components/QueryPanel";
|
||||||
import { RuntimePanel } from "./components/RuntimePanel";
|
import { RuntimePanel } from "./components/RuntimePanel";
|
||||||
import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults";
|
import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults";
|
||||||
import { designConfig } from "../../../designconfig";
|
import { designConfig } from "../../../designconfig";
|
||||||
import type {
|
import type {
|
||||||
AssistantConversationItem,
|
|
||||||
AssistantAnnotationRecord,
|
|
||||||
AssistantSelectionChip,
|
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
NormalizeResultState,
|
NormalizeResultState,
|
||||||
@@ -30,22 +24,10 @@ import type {
|
|||||||
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
|
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
|
||||||
const AUTORUNS_LAYOUT_CONFIG_KEY = "ndc_autoruns_layout_config_v1";
|
const AUTORUNS_LAYOUT_CONFIG_KEY = "ndc_autoruns_layout_config_v1";
|
||||||
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
|
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
|
||||||
const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
|
|
||||||
const DEFAULT_UI_MODE: UiMode = "autoruns";
|
const DEFAULT_UI_MODE: UiMode = "autoruns";
|
||||||
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
||||||
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
||||||
const TAB_KEYS: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
|
const TAB_KEYS: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
|
||||||
const DEFAULT_ASSISTANT_ANNOTATION_AUTHOR = "manual_reviewer";
|
|
||||||
|
|
||||||
interface AssistantCommentModalState {
|
|
||||||
open: boolean;
|
|
||||||
messageIndex: number;
|
|
||||||
rating: number;
|
|
||||||
comment: string;
|
|
||||||
annotationAuthor: string;
|
|
||||||
saving: boolean;
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function withTs(message: string): string {
|
function withTs(message: string): string {
|
||||||
return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`;
|
return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`;
|
||||||
@@ -61,25 +43,6 @@ function diffPrompts(current: PromptState, previous: PromptState | null): string
|
|||||||
return `Changed fields: ${changed.length}. ${changed.join(" | ")}`;
|
return `Changed fields: ${changed.length}. ${changed.join(" | ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAssistantFollowupMessage(inputValue: string, selectedChip: AssistantSelectionChip | null): string {
|
|
||||||
const trimmedInput = inputValue.trim();
|
|
||||||
if (!trimmedInput) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (!selectedChip) {
|
|
||||||
return trimmedInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedInput = trimmedInput.toLowerCase();
|
|
||||||
const selectionAnchor = selectedChip.anchor_text.trim();
|
|
||||||
const normalizedSelection = selectionAnchor.toLowerCase();
|
|
||||||
if (normalizedSelection && normalizedInput.includes(normalizedSelection)) {
|
|
||||||
return trimmedInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `По выбранному объекту "${selectionAnchor}": ${trimmedInput}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [connection, setConnection] = useState<ConnectionState>(DEFAULT_CONNECTION);
|
const [connection, setConnection] = useState<ConnectionState>(DEFAULT_CONNECTION);
|
||||||
const [prompts, setPrompts] = useState<PromptState>(DEFAULT_PROMPTS);
|
const [prompts, setPrompts] = useState<PromptState>(DEFAULT_PROMPTS);
|
||||||
@@ -123,11 +86,6 @@ export default function App() {
|
|||||||
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
|
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
|
||||||
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
|
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
|
||||||
const [showAutorunsCommentsMode, setShowAutorunsCommentsMode] = useState(true);
|
const [showAutorunsCommentsMode, setShowAutorunsCommentsMode] = useState(true);
|
||||||
const [showAssistantConnectionMode, setShowAssistantConnectionMode] = useState(true);
|
|
||||||
const [showAssistantPromptMode, setShowAssistantPromptMode] = useState(true);
|
|
||||||
const [showAssistantChatMode, setShowAssistantChatMode] = useState(true);
|
|
||||||
const [showAssistantCommentsMode, setShowAssistantCommentsMode] = useState(true);
|
|
||||||
const [showAssistantSamMode, setShowAssistantSamMode] = useState(true);
|
|
||||||
const [showDecompositionConnectionMode, setShowDecompositionConnectionMode] = useState(true);
|
const [showDecompositionConnectionMode, setShowDecompositionConnectionMode] = useState(true);
|
||||||
const [showDecompositionPromptMode, setShowDecompositionPromptMode] = useState(true);
|
const [showDecompositionPromptMode, setShowDecompositionPromptMode] = useState(true);
|
||||||
const [showDecompositionQueryMode, setShowDecompositionQueryMode] = useState(true);
|
const [showDecompositionQueryMode, setShowDecompositionQueryMode] = useState(true);
|
||||||
@@ -135,24 +93,6 @@ export default function App() {
|
|||||||
const [showDecompositionMetricsMode, setShowDecompositionMetricsMode] = useState(true);
|
const [showDecompositionMetricsMode, setShowDecompositionMetricsMode] = useState(true);
|
||||||
const [showDecompositionHistoryMode, setShowDecompositionHistoryMode] = useState(true);
|
const [showDecompositionHistoryMode, setShowDecompositionHistoryMode] = useState(true);
|
||||||
const [showDecompositionRuntimeMode, setShowDecompositionRuntimeMode] = useState(true);
|
const [showDecompositionRuntimeMode, setShowDecompositionRuntimeMode] = useState(true);
|
||||||
const [assistantSessionId, setAssistantSessionId] = useState("");
|
|
||||||
const [assistantConversation, setAssistantConversation] = useState<AssistantConversationItem[]>([]);
|
|
||||||
const [assistantInput, setAssistantInput] = useState("");
|
|
||||||
const [assistantSelectedChip, setAssistantSelectedChip] = useState<AssistantSelectionChip | null>(null);
|
|
||||||
const [assistantBusy, setAssistantBusy] = useState(false);
|
|
||||||
const [assistantStatus, setAssistantStatus] = useState("");
|
|
||||||
const [assistantError, setAssistantError] = useState("");
|
|
||||||
const [assistantAnnotations, setAssistantAnnotations] = useState<AssistantAnnotationRecord[]>([]);
|
|
||||||
const [assistantAnnotationsBusy, setAssistantAnnotationsBusy] = useState(false);
|
|
||||||
const [assistantCommentModal, setAssistantCommentModal] = useState<AssistantCommentModalState>({
|
|
||||||
open: false,
|
|
||||||
messageIndex: -1,
|
|
||||||
rating: 3,
|
|
||||||
comment: "",
|
|
||||||
annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
|
|
||||||
saving: false,
|
|
||||||
error: ""
|
|
||||||
});
|
|
||||||
const presetAutoloadDoneRef = useRef(false);
|
const presetAutoloadDoneRef = useRef(false);
|
||||||
const skipPresetAutoloadRef = useRef(false);
|
const skipPresetAutoloadRef = useRef(false);
|
||||||
const sharedConnectionSyncReadyRef = useRef(false);
|
const sharedConnectionSyncReadyRef = useRef(false);
|
||||||
@@ -184,16 +124,6 @@ export default function App() {
|
|||||||
setAppLogs((prev) => [withTs(message), ...prev].slice(0, 300));
|
setAppLogs((prev) => [withTs(message), ...prev].slice(0, 300));
|
||||||
};
|
};
|
||||||
|
|
||||||
function startAssistantStatusTicker(): () => void {
|
|
||||||
let index = 0;
|
|
||||||
setAssistantStatus(ASSISTANT_STAGES[0]);
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
index = Math.min(index + 1, ASSISTANT_STAGES.length - 1);
|
|
||||||
setAssistantStatus(ASSISTANT_STAGES[index]);
|
|
||||||
}, 650);
|
|
||||||
return () => window.clearInterval(timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bootstrapSharedConnection = async () => {
|
const bootstrapSharedConnection = async () => {
|
||||||
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
|
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
|
||||||
@@ -239,7 +169,7 @@ export default function App() {
|
|||||||
if (cachedAutorunsLayout) {
|
if (cachedAutorunsLayout) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(cachedAutorunsLayout) as {
|
const parsed = JSON.parse(cachedAutorunsLayout) as {
|
||||||
uiMode?: UiMode;
|
uiMode?: UiMode | "assistant";
|
||||||
activeTab?: TabKey;
|
activeTab?: TabKey;
|
||||||
showAutorunsSettingsMode?: boolean;
|
showAutorunsSettingsMode?: boolean;
|
||||||
showAutorunsAutoRunsMode?: boolean;
|
showAutorunsAutoRunsMode?: boolean;
|
||||||
@@ -247,11 +177,6 @@ export default function App() {
|
|||||||
showAutorunsDecompositionMode?: boolean;
|
showAutorunsDecompositionMode?: boolean;
|
||||||
showAutorunsProgressMode?: boolean;
|
showAutorunsProgressMode?: boolean;
|
||||||
showAutorunsCommentsMode?: boolean;
|
showAutorunsCommentsMode?: boolean;
|
||||||
showAssistantConnectionMode?: boolean;
|
|
||||||
showAssistantPromptMode?: boolean;
|
|
||||||
showAssistantChatMode?: boolean;
|
|
||||||
showAssistantCommentsMode?: boolean;
|
|
||||||
showAssistantSamMode?: boolean;
|
|
||||||
showDecompositionConnectionMode?: boolean;
|
showDecompositionConnectionMode?: boolean;
|
||||||
showDecompositionPromptMode?: boolean;
|
showDecompositionPromptMode?: boolean;
|
||||||
showDecompositionQueryMode?: boolean;
|
showDecompositionQueryMode?: boolean;
|
||||||
@@ -261,9 +186,7 @@ export default function App() {
|
|||||||
showDecompositionRuntimeMode?: boolean;
|
showDecompositionRuntimeMode?: boolean;
|
||||||
prompts?: PromptState;
|
prompts?: PromptState;
|
||||||
};
|
};
|
||||||
if (parsed.uiMode === "decomposition") {
|
if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns" || parsed.uiMode === "decomposition") {
|
||||||
setUiMode("decomposition");
|
|
||||||
} else if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns") {
|
|
||||||
setUiMode("autoruns");
|
setUiMode("autoruns");
|
||||||
}
|
}
|
||||||
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
|
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
|
||||||
@@ -287,21 +210,6 @@ export default function App() {
|
|||||||
if (typeof parsed.showAutorunsCommentsMode === "boolean") {
|
if (typeof parsed.showAutorunsCommentsMode === "boolean") {
|
||||||
setShowAutorunsCommentsMode(parsed.showAutorunsCommentsMode);
|
setShowAutorunsCommentsMode(parsed.showAutorunsCommentsMode);
|
||||||
}
|
}
|
||||||
if (typeof parsed.showAssistantConnectionMode === "boolean") {
|
|
||||||
setShowAssistantConnectionMode(parsed.showAssistantConnectionMode);
|
|
||||||
}
|
|
||||||
if (typeof parsed.showAssistantPromptMode === "boolean") {
|
|
||||||
setShowAssistantPromptMode(parsed.showAssistantPromptMode);
|
|
||||||
}
|
|
||||||
if (typeof parsed.showAssistantChatMode === "boolean") {
|
|
||||||
setShowAssistantChatMode(parsed.showAssistantChatMode);
|
|
||||||
}
|
|
||||||
if (typeof parsed.showAssistantCommentsMode === "boolean") {
|
|
||||||
setShowAssistantCommentsMode(parsed.showAssistantCommentsMode);
|
|
||||||
}
|
|
||||||
if (typeof parsed.showAssistantSamMode === "boolean") {
|
|
||||||
setShowAssistantSamMode(parsed.showAssistantSamMode);
|
|
||||||
}
|
|
||||||
if (typeof parsed.showDecompositionConnectionMode === "boolean") {
|
if (typeof parsed.showDecompositionConnectionMode === "boolean") {
|
||||||
setShowDecompositionConnectionMode(parsed.showDecompositionConnectionMode);
|
setShowDecompositionConnectionMode(parsed.showDecompositionConnectionMode);
|
||||||
}
|
}
|
||||||
@@ -448,11 +356,6 @@ export default function App() {
|
|||||||
showAutorunsDecompositionMode,
|
showAutorunsDecompositionMode,
|
||||||
showAutorunsProgressMode,
|
showAutorunsProgressMode,
|
||||||
showAutorunsCommentsMode,
|
showAutorunsCommentsMode,
|
||||||
showAssistantConnectionMode,
|
|
||||||
showAssistantPromptMode,
|
|
||||||
showAssistantChatMode,
|
|
||||||
showAssistantCommentsMode,
|
|
||||||
showAssistantSamMode,
|
|
||||||
showDecompositionConnectionMode,
|
showDecompositionConnectionMode,
|
||||||
showDecompositionPromptMode,
|
showDecompositionPromptMode,
|
||||||
showDecompositionQueryMode,
|
showDecompositionQueryMode,
|
||||||
@@ -740,207 +643,6 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantAnnotationsByMessageId = useMemo(() => {
|
|
||||||
const map = new Map<string, AssistantAnnotationRecord>();
|
|
||||||
for (const item of assistantAnnotations) {
|
|
||||||
if (item.message_id) {
|
|
||||||
map.set(item.message_id, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [assistantAnnotations]);
|
|
||||||
|
|
||||||
const assistantCommentModalMessage =
|
|
||||||
assistantCommentModal.messageIndex >= 0 ? assistantConversation[assistantCommentModal.messageIndex] ?? null : null;
|
|
||||||
|
|
||||||
const assistantCommentModalQuestion = useMemo(() => {
|
|
||||||
if (assistantCommentModal.messageIndex < 0) return null;
|
|
||||||
for (let index = assistantCommentModal.messageIndex - 1; index >= 0; index -= 1) {
|
|
||||||
const candidate = assistantConversation[index];
|
|
||||||
if (candidate?.role === "user") {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [assistantCommentModal.messageIndex, assistantConversation]);
|
|
||||||
|
|
||||||
async function loadAssistantAnnotationsForSession(sessionId: string): Promise<void> {
|
|
||||||
if (!sessionId.trim()) {
|
|
||||||
setAssistantAnnotations([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setAssistantAnnotationsBusy(true);
|
|
||||||
try {
|
|
||||||
const payload = await apiClient.loadAssistantAnnotations({
|
|
||||||
session_id: sessionId,
|
|
||||||
limit: 400
|
|
||||||
});
|
|
||||||
setAssistantAnnotations(payload.items ?? []);
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
log(`Assistant annotations load error: ${message}`);
|
|
||||||
} finally {
|
|
||||||
setAssistantAnnotationsBusy(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAssistantCommentModal(options?: { force?: boolean }) {
|
|
||||||
setAssistantCommentModal((prev) => {
|
|
||||||
if (prev.saving && !options?.force) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
open: false,
|
|
||||||
messageIndex: -1,
|
|
||||||
rating: 3,
|
|
||||||
comment: "",
|
|
||||||
annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
|
|
||||||
saving: false,
|
|
||||||
error: ""
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAssistantCommentModal(item: AssistantConversationItem, index: number): void {
|
|
||||||
if (item.role !== "assistant") return;
|
|
||||||
const sessionIdFromState = assistantSessionId.trim();
|
|
||||||
const sessionIdFromItem = String(item.session_id ?? "").trim();
|
|
||||||
const resolvedSessionId = sessionIdFromState || sessionIdFromItem;
|
|
||||||
if (!resolvedSessionId) {
|
|
||||||
setAssistantError("Сначала получите ответ ассистента в активной сессии.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!sessionIdFromState && sessionIdFromItem) {
|
|
||||||
setAssistantSessionId(sessionIdFromItem);
|
|
||||||
}
|
|
||||||
const existing = assistantAnnotationsByMessageId.get(item.message_id) ?? null;
|
|
||||||
setAssistantCommentModal({
|
|
||||||
open: true,
|
|
||||||
messageIndex: index,
|
|
||||||
rating: existing?.rating ?? 3,
|
|
||||||
comment: existing?.comment ?? "",
|
|
||||||
annotationAuthor: existing?.annotation_author ?? DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
|
|
||||||
saving: false,
|
|
||||||
error: ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function canCommentAssistantMessage(item: AssistantConversationItem): boolean {
|
|
||||||
return item.role === "assistant";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAssistantMessageCommented(item: AssistantConversationItem): boolean {
|
|
||||||
return item.role === "assistant" && assistantAnnotationsByMessageId.has(item.message_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitAssistantCommentModal(): Promise<void> {
|
|
||||||
if (!assistantSessionId.trim()) {
|
|
||||||
setAssistantCommentModal((prev) => ({ ...prev, error: "Сессия ассистента не найдена." }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (assistantCommentModal.messageIndex < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!assistantCommentModal.comment.trim()) {
|
|
||||||
setAssistantCommentModal((prev) => ({ ...prev, error: "Добавьте комментарий." }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAssistantCommentModal((prev) => ({ ...prev, saving: true, error: "" }));
|
|
||||||
try {
|
|
||||||
const payload = await apiClient.saveAssistantAnnotation({
|
|
||||||
session_id: assistantSessionId,
|
|
||||||
message_index: assistantCommentModal.messageIndex,
|
|
||||||
rating: assistantCommentModal.rating,
|
|
||||||
comment: assistantCommentModal.comment.trim(),
|
|
||||||
annotation_author: assistantCommentModal.annotationAuthor.trim() || undefined
|
|
||||||
});
|
|
||||||
setAssistantAnnotations((prev) => {
|
|
||||||
const next = [...prev];
|
|
||||||
const index = next.findIndex((item) => item.annotation_id === payload.annotation.annotation_id);
|
|
||||||
if (index >= 0) {
|
|
||||||
next[index] = payload.annotation;
|
|
||||||
} else {
|
|
||||||
next.unshift(payload.annotation);
|
|
||||||
}
|
|
||||||
return next.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
|
|
||||||
});
|
|
||||||
closeAssistantCommentModal({ force: true });
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
setAssistantCommentModal((prev) => ({ ...prev, saving: false, error: message }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetAssistantSession() {
|
|
||||||
setAssistantSessionId("");
|
|
||||||
setAssistantConversation([]);
|
|
||||||
setAssistantInput("");
|
|
||||||
setAssistantSelectedChip(null);
|
|
||||||
setAssistantStatus("");
|
|
||||||
setAssistantError("");
|
|
||||||
setAssistantAnnotations([]);
|
|
||||||
closeAssistantCommentModal({ force: true });
|
|
||||||
log("Assistant session reset.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendAssistantMessage() {
|
|
||||||
const userMessage = buildAssistantFollowupMessage(assistantInput, assistantSelectedChip);
|
|
||||||
if (!userMessage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAssistantBusy(true);
|
|
||||||
setAssistantError("");
|
|
||||||
setAssistantInput("");
|
|
||||||
setAssistantConversation((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
message_id: `local-${Date.now()}`,
|
|
||||||
session_id: assistantSessionId || "pending",
|
|
||||||
role: "user",
|
|
||||||
text: userMessage,
|
|
||||||
reply_type: null,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
trace_id: null,
|
|
||||||
debug: null
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const stopTicker = startAssistantStatusTicker();
|
|
||||||
try {
|
|
||||||
const response = await apiClient.sendAssistantMessage({
|
|
||||||
connection,
|
|
||||||
prompts,
|
|
||||||
userMessage,
|
|
||||||
sessionId: assistantSessionId || undefined,
|
|
||||||
promptVersion: ASSISTANT_PROMPT_VERSION,
|
|
||||||
useMock
|
|
||||||
});
|
|
||||||
setAssistantSessionId(response.session_id);
|
|
||||||
setAssistantConversation(response.conversation);
|
|
||||||
setAssistantStatus("Ответ готов");
|
|
||||||
await loadAssistantAnnotationsForSession(response.session_id);
|
|
||||||
log(`Assistant reply received: trace=${response.debug.trace_id}`);
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
setAssistantError(message);
|
|
||||||
setAssistantStatus("Ошибка ассистента");
|
|
||||||
log(`Assistant error: ${message}`);
|
|
||||||
} finally {
|
|
||||||
stopTicker();
|
|
||||||
setAssistantBusy(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!assistantSessionId.trim()) {
|
|
||||||
setAssistantAnnotations([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void loadAssistantAnnotationsForSession(assistantSessionId);
|
|
||||||
}, [assistantSessionId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedRunId) {
|
if (!selectedRunId) {
|
||||||
setRunTrace([]);
|
setRunTrace([]);
|
||||||
@@ -953,493 +655,89 @@ export default function App() {
|
|||||||
}, [selectedRunId]);
|
}, [selectedRunId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main className="app-root app-root-autoruns">
|
||||||
className={`app-root ${
|
|
||||||
uiMode === "assistant" || uiMode === "decomposition" || uiMode === "autoruns" ? "app-root-autoruns" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<header className="app-topbar">
|
<header className="app-topbar">
|
||||||
<div className="mode-switch-row">
|
<div className="mode-switch-row">
|
||||||
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
|
<button type="button" className="tab active" onClick={() => setUiMode("autoruns")}>
|
||||||
Управление ассистентом
|
Управление ассистентом
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
|
|
||||||
Декомпозиция
|
|
||||||
</button>
|
|
||||||
<button type="button" className="tab" onClick={saveAutorunsLayout}>
|
<button type="button" className="tab" onClick={saveAutorunsLayout}>
|
||||||
Сохранить
|
Сохранить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{uiMode === "assistant" ? (
|
<div className="mode-switch-row mode-switch-row-right">
|
||||||
<div className="mode-switch-row mode-switch-row-right">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={showAutorunsSettingsMode ? "tab active" : "tab"}
|
||||||
className={showAssistantConnectionMode ? "tab active" : "tab"}
|
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
|
||||||
onClick={() => setShowAssistantConnectionMode((prev) => !prev)}
|
>
|
||||||
>
|
Настройки
|
||||||
LLM Connector
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
|
||||||
className={showAssistantPromptMode ? "tab active" : "tab"}
|
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
|
||||||
onClick={() => setShowAssistantPromptMode((prev) => !prev)}
|
>
|
||||||
>
|
Автопрогоны
|
||||||
Prompt Manager
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button type="button" className={showAssistantChatMode ? "tab active" : "tab"} onClick={() => setShowAssistantChatMode((prev) => !prev)}>
|
type="button"
|
||||||
Режим ассистента
|
className={showAutorunsAssistantMode ? "tab active" : "tab"}
|
||||||
</button>
|
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
|
||||||
<button
|
>
|
||||||
type="button"
|
Режим ассистента
|
||||||
className={showAssistantCommentsMode ? "tab active" : "tab"}
|
</button>
|
||||||
onClick={() => setShowAssistantCommentsMode((prev) => !prev)}
|
<button
|
||||||
>
|
type="button"
|
||||||
Комментарии ассистента
|
className={showAutorunsProgressMode ? "tab active" : "tab"}
|
||||||
</button>
|
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
|
||||||
<button type="button" className={showAssistantSamMode ? "tab active" : "tab"} onClick={() => setShowAssistantSamMode((prev) => !prev)}>
|
>
|
||||||
SAM
|
Прогресс/регресс
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
) : uiMode === "decomposition" ? (
|
type="button"
|
||||||
<div className="mode-switch-row mode-switch-row-right">
|
className={showAutorunsCommentsMode ? "tab active" : "tab"}
|
||||||
<button
|
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
|
||||||
type="button"
|
>
|
||||||
className={showDecompositionConnectionMode ? "tab active" : "tab"}
|
Комментарии
|
||||||
onClick={() => setShowDecompositionConnectionMode((prev) => !prev)}
|
</button>
|
||||||
>
|
</div>
|
||||||
LLM
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showDecompositionPromptMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowDecompositionPromptMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Prompt
|
|
||||||
</button>
|
|
||||||
<button type="button" className={showDecompositionQueryMode ? "tab active" : "tab"} onClick={() => setShowDecompositionQueryMode((prev) => !prev)}>
|
|
||||||
Запрос
|
|
||||||
</button>
|
|
||||||
<button type="button" className={showDecompositionOutputMode ? "tab active" : "tab"} onClick={() => setShowDecompositionOutputMode((prev) => !prev)}>
|
|
||||||
Выход
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showDecompositionMetricsMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowDecompositionMetricsMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Метрики
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showDecompositionHistoryMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowDecompositionHistoryMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
История
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showDecompositionRuntimeMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowDecompositionRuntimeMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
NDC Run Monitor
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : uiMode === "autoruns" ? (
|
|
||||||
<div className="mode-switch-row mode-switch-row-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAutorunsSettingsMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Настройки
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Автопрогоны
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAutorunsAssistantMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Режим ассистента
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAutorunsDecompositionMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAutorunsDecompositionMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Режим декомпозиции
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAutorunsProgressMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Прогресс/регресс
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAutorunsCommentsMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Комментарии
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{uiMode === "assistant" ? (
|
<div className="layout-grid layout-grid-autoruns">
|
||||||
<div className="layout-grid layout-grid-mode-columns">
|
<AutoRunsHistoryPanel
|
||||||
<div className="mode-columns">
|
connection={connection}
|
||||||
{showAssistantConnectionMode ? (
|
modelOptions={modelOptions}
|
||||||
<div className="mode-col">
|
modelsBusy={modelsBusy}
|
||||||
<ConnectionPanel
|
connectionStatus={connectionStatus}
|
||||||
value={connection}
|
connectionBusy={busy}
|
||||||
modelOptions={modelOptions}
|
onConnectionChange={setConnection}
|
||||||
modelsBusy={modelsBusy}
|
onReloadModels={reloadModels}
|
||||||
onChange={setConnection}
|
onSaveLocalConfig={saveLocalConfig}
|
||||||
onReloadModels={reloadModels}
|
onTestConnection={testConnection}
|
||||||
onSaveLocalConfig={saveLocalConfig}
|
prompts={prompts}
|
||||||
onTestConnection={testConnection}
|
onPromptsChange={setPrompts}
|
||||||
lastStatus={connectionStatus}
|
promptPresets={presetList}
|
||||||
busy={busy || assistantBusy}
|
selectedPresetId={selectedPresetId}
|
||||||
/>
|
onSelectPreset={setSelectedPresetId}
|
||||||
</div>
|
onLoadPreset={loadSelectedPreset}
|
||||||
) : null}
|
onSavePreset={savePreset}
|
||||||
|
onResetDefaults={resetDefaults}
|
||||||
{showAssistantPromptMode ? (
|
onDiffPrevious={diffWithPrevious}
|
||||||
<div className="mode-col mode-col-wide">
|
presetName={presetName}
|
||||||
<PromptPanel
|
onPresetNameChange={setPresetName}
|
||||||
value={prompts}
|
diffSummary={diffSummary}
|
||||||
onChange={setPrompts}
|
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
||||||
presets={presetList}
|
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
||||||
selectedPresetId={selectedPresetId}
|
showSettingsMode={showAutorunsSettingsMode}
|
||||||
onSelectPreset={setSelectedPresetId}
|
showAutoRunsMode={showAutorunsAutoRunsMode}
|
||||||
onLoadPreset={loadSelectedPreset}
|
showAssistantMode={showAutorunsAssistantMode}
|
||||||
onSavePreset={savePreset}
|
showProgressMode={showAutorunsProgressMode}
|
||||||
onResetDefaults={resetDefaults}
|
showCommentsMode={showAutorunsCommentsMode}
|
||||||
onDiffPrevious={diffWithPrevious}
|
onLog={log}
|
||||||
presetName={presetName}
|
/>
|
||||||
onPresetNameChange={setPresetName}
|
</div>
|
||||||
diffSummary={diffSummary}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showAssistantChatMode ? (
|
|
||||||
<div className="mode-col mode-col-xwide">
|
|
||||||
<AssistantPanel
|
|
||||||
sessionId={assistantSessionId}
|
|
||||||
conversation={assistantConversation}
|
|
||||||
inputValue={assistantInput}
|
|
||||||
onInputChange={setAssistantInput}
|
|
||||||
selectedContextChip={assistantSelectedChip}
|
|
||||||
onSelectContextChip={setAssistantSelectedChip}
|
|
||||||
onClearContextChip={() => setAssistantSelectedChip(null)}
|
|
||||||
useMock={useMock}
|
|
||||||
onUseMockChange={setUseMock}
|
|
||||||
onSend={sendAssistantMessage}
|
|
||||||
onClear={resetAssistantSession}
|
|
||||||
busy={assistantBusy}
|
|
||||||
statusText={assistantStatus}
|
|
||||||
errorMessage={assistantError}
|
|
||||||
showCommentAction
|
|
||||||
onCommentAssistantMessage={openAssistantCommentModal}
|
|
||||||
isAssistantMessageCommented={isAssistantMessageCommented}
|
|
||||||
canCommentAssistantMessage={canCommentAssistantMessage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showAssistantCommentsMode ? (
|
|
||||||
<div className="mode-col">
|
|
||||||
<PanelFrame className="assistant-comments-frame" title="Комментарии ассистента">
|
|
||||||
<div className="assistant-comments-shell">
|
|
||||||
<div className="assistant-comments-toolbar">
|
|
||||||
<span className="muted">
|
|
||||||
{assistantSessionId ? `session: ${assistantSessionId}` : "Сессия не запущена"}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="tab"
|
|
||||||
onClick={() => void loadAssistantAnnotationsForSession(assistantSessionId)}
|
|
||||||
disabled={!assistantSessionId || assistantAnnotationsBusy}
|
|
||||||
>
|
|
||||||
{assistantAnnotationsBusy ? "Обновляю..." : "Обновить"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="assistant-comments-list">
|
|
||||||
{!assistantSessionId ? <p className="muted">Появится после первого ответа ассистента.</p> : null}
|
|
||||||
{assistantSessionId && assistantAnnotations.length === 0 && !assistantAnnotationsBusy ? (
|
|
||||||
<p className="muted">Комментариев по этой сессии пока нет.</p>
|
|
||||||
) : null}
|
|
||||||
{assistantAnnotations.map((item) => (
|
|
||||||
<article key={item.annotation_id} className="assistant-comment-item">
|
|
||||||
<div className="assistant-comment-head">
|
|
||||||
<strong>{`${"●".repeat(Math.max(1, Math.min(5, Math.round(item.rating))))}${"○".repeat(Math.max(0, 5 - Math.round(item.rating)))}`}</strong>
|
|
||||||
<span>{new Date(item.updated_at).toLocaleString("ru-RU")}</span>
|
|
||||||
</div>
|
|
||||||
{item.context.question_text ? <p>Q: {item.context.question_text}</p> : null}
|
|
||||||
{item.context.answer_text ? <p>A: {item.context.answer_text}</p> : null}
|
|
||||||
<p>{item.comment}</p>
|
|
||||||
<div className="assistant-comment-meta">
|
|
||||||
{item.context.trace_id ? <span>{`trace=${item.context.trace_id}`}</span> : null}
|
|
||||||
{item.context.reply_type ? <span>{`reply_type=${item.context.reply_type}`}</span> : null}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PanelFrame>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showAssistantSamMode ? (
|
|
||||||
<div className="mode-col">
|
|
||||||
<AssistantSamPanel
|
|
||||||
sessionId={assistantSessionId}
|
|
||||||
conversation={assistantConversation}
|
|
||||||
statusText={assistantStatus}
|
|
||||||
errorMessage={assistantError}
|
|
||||||
useMock={useMock}
|
|
||||||
appLogs={appLogs}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!showAssistantConnectionMode &&
|
|
||||||
!showAssistantPromptMode &&
|
|
||||||
!showAssistantChatMode &&
|
|
||||||
!showAssistantCommentsMode &&
|
|
||||||
!showAssistantSamMode ? (
|
|
||||||
<div className="mode-columns-empty">Все панели режима ассистента скрыты. Включите нужные блоки справа в шапке.</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : uiMode === "decomposition" ? (
|
|
||||||
<div className="layout-grid layout-grid-mode-columns">
|
|
||||||
<div className="mode-columns">
|
|
||||||
{showDecompositionConnectionMode ? (
|
|
||||||
<div className="mode-col">
|
|
||||||
<ConnectionPanel
|
|
||||||
value={connection}
|
|
||||||
modelOptions={modelOptions}
|
|
||||||
modelsBusy={modelsBusy}
|
|
||||||
onChange={setConnection}
|
|
||||||
onReloadModels={reloadModels}
|
|
||||||
onSaveLocalConfig={saveLocalConfig}
|
|
||||||
onTestConnection={testConnection}
|
|
||||||
lastStatus={connectionStatus}
|
|
||||||
busy={busy}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showDecompositionPromptMode ? (
|
|
||||||
<div className="mode-col mode-col-wide">
|
|
||||||
<PromptPanel
|
|
||||||
value={prompts}
|
|
||||||
onChange={setPrompts}
|
|
||||||
presets={presetList}
|
|
||||||
selectedPresetId={selectedPresetId}
|
|
||||||
onSelectPreset={setSelectedPresetId}
|
|
||||||
onLoadPreset={loadSelectedPreset}
|
|
||||||
onSavePreset={savePreset}
|
|
||||||
onResetDefaults={resetDefaults}
|
|
||||||
onDiffPrevious={diffWithPrevious}
|
|
||||||
presetName={presetName}
|
|
||||||
onPresetNameChange={setPresetName}
|
|
||||||
diffSummary={diffSummary}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showDecompositionQueryMode ? (
|
|
||||||
<div className="mode-col">
|
|
||||||
<QueryPanel
|
|
||||||
value={query}
|
|
||||||
onChange={setQuery}
|
|
||||||
onApplyBatchFormat={applyBatchFormat}
|
|
||||||
onNormalize={normalize}
|
|
||||||
busy={busy}
|
|
||||||
useMock={useMock}
|
|
||||||
onUseMockChange={setUseMock}
|
|
||||||
errorMessage={lastError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showDecompositionOutputMode ? (
|
|
||||||
<div className="mode-col mode-col-xwide">
|
|
||||||
<OutputPanel tab={activeTab} onTabChange={setActiveTab} result={result} appLogs={appLogs} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showDecompositionMetricsMode ? (
|
|
||||||
<div className="mode-col">
|
|
||||||
<MetricsPanel result={result} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showDecompositionHistoryMode ? (
|
|
||||||
<div className="mode-col">
|
|
||||||
<HistoryPanel items={historyItems} onRefresh={refreshHistory} onOpenTrace={openTrace} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showDecompositionRuntimeMode ? (
|
|
||||||
<div className="mode-col mode-col-xwide">
|
|
||||||
<RuntimePanel
|
|
||||||
runs={runs}
|
|
||||||
selectedRunId={selectedRunId}
|
|
||||||
onSelectRun={setSelectedRunId}
|
|
||||||
onStartRun={startRun}
|
|
||||||
onFinishRun={finishRun}
|
|
||||||
onRefreshRuns={refreshRuns}
|
|
||||||
onRunEval={runEval}
|
|
||||||
onCopyEvalReport={copyEvalReport}
|
|
||||||
evalBusy={evalBusy}
|
|
||||||
traceItems={runTrace}
|
|
||||||
evalReport={evalReport}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!showDecompositionConnectionMode &&
|
|
||||||
!showDecompositionPromptMode &&
|
|
||||||
!showDecompositionQueryMode &&
|
|
||||||
!showDecompositionOutputMode &&
|
|
||||||
!showDecompositionMetricsMode &&
|
|
||||||
!showDecompositionHistoryMode &&
|
|
||||||
!showDecompositionRuntimeMode ? (
|
|
||||||
<div className="mode-columns-empty">Все панели режима декомпозиции скрыты. Включите нужные блоки справа в шапке.</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="layout-grid layout-grid-autoruns">
|
|
||||||
<AutoRunsHistoryPanel
|
|
||||||
connection={connection}
|
|
||||||
modelOptions={modelOptions}
|
|
||||||
modelsBusy={modelsBusy}
|
|
||||||
connectionStatus={connectionStatus}
|
|
||||||
connectionBusy={busy}
|
|
||||||
onConnectionChange={setConnection}
|
|
||||||
onReloadModels={reloadModels}
|
|
||||||
onSaveLocalConfig={saveLocalConfig}
|
|
||||||
onTestConnection={testConnection}
|
|
||||||
prompts={prompts}
|
|
||||||
onPromptsChange={setPrompts}
|
|
||||||
promptPresets={presetList}
|
|
||||||
selectedPresetId={selectedPresetId}
|
|
||||||
onSelectPreset={setSelectedPresetId}
|
|
||||||
onLoadPreset={loadSelectedPreset}
|
|
||||||
onSavePreset={savePreset}
|
|
||||||
onResetDefaults={resetDefaults}
|
|
||||||
onDiffPrevious={diffWithPrevious}
|
|
||||||
presetName={presetName}
|
|
||||||
onPresetNameChange={setPresetName}
|
|
||||||
diffSummary={diffSummary}
|
|
||||||
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
|
||||||
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
|
||||||
showSettingsMode={showAutorunsSettingsMode}
|
|
||||||
showAutoRunsMode={showAutorunsAutoRunsMode}
|
|
||||||
showAssistantMode={showAutorunsAssistantMode}
|
|
||||||
showDecompositionMode={showAutorunsDecompositionMode}
|
|
||||||
showProgressMode={showAutorunsProgressMode}
|
|
||||||
showCommentsMode={showAutorunsCommentsMode}
|
|
||||||
onLog={log}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{assistantCommentModal.open ? (
|
|
||||||
<div
|
|
||||||
className="autoruns-comment-modal-backdrop"
|
|
||||||
onClick={(event) => {
|
|
||||||
if (event.target === event.currentTarget) {
|
|
||||||
closeAssistantCommentModal();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="autoruns-comment-modal">
|
|
||||||
<h3>Комментарий к ответу ассистента</h3>
|
|
||||||
<p className="muted">Эта разметка хранится отдельно от комментариев автопрогонов.</p>
|
|
||||||
|
|
||||||
{assistantCommentModalQuestion ? (
|
|
||||||
<details className="autoruns-prompt-details" open>
|
|
||||||
<summary>Вопрос пользователя</summary>
|
|
||||||
<p className="autoruns-comment-quote">{assistantCommentModalQuestion.text}</p>
|
|
||||||
</details>
|
|
||||||
) : null}
|
|
||||||
{assistantCommentModalMessage ? (
|
|
||||||
<details className="autoruns-prompt-details" open>
|
|
||||||
<summary>Ответ ассистента</summary>
|
|
||||||
<p className="autoruns-comment-quote">{assistantCommentModalMessage.text}</p>
|
|
||||||
</details>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="autoruns-rating-row" role="group" aria-label="Рейтинг ответа">
|
|
||||||
{[1, 2, 3, 4, 5].map((value) => (
|
|
||||||
<button
|
|
||||||
key={value}
|
|
||||||
type="button"
|
|
||||||
className={assistantCommentModal.rating >= value ? "autoruns-rating-dot active" : "autoruns-rating-dot"}
|
|
||||||
onClick={() => setAssistantCommentModal((prev) => ({ ...prev, rating: value }))}
|
|
||||||
disabled={assistantCommentModal.saving}
|
|
||||||
aria-label={`Оценка ${value}`}
|
|
||||||
>
|
|
||||||
{assistantCommentModal.rating >= value ? "●" : "○"}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="autoruns-form-grid">
|
|
||||||
<label>
|
|
||||||
Автор комментария
|
|
||||||
<input
|
|
||||||
value={assistantCommentModal.annotationAuthor}
|
|
||||||
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, annotationAuthor: event.target.value }))}
|
|
||||||
placeholder="manual_reviewer"
|
|
||||||
disabled={assistantCommentModal.saving}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Комментарий
|
|
||||||
<textarea
|
|
||||||
value={assistantCommentModal.comment}
|
|
||||||
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, comment: event.target.value }))}
|
|
||||||
placeholder="Что именно не так в ответе и что проверить."
|
|
||||||
rows={4}
|
|
||||||
disabled={assistantCommentModal.saving}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{assistantCommentModal.error ? <p className="error-text">{assistantCommentModal.error}</p> : null}
|
|
||||||
|
|
||||||
<div className="button-row">
|
|
||||||
<button type="button" onClick={() => void submitAssistantCommentModal()} disabled={assistantCommentModal.saving}>
|
|
||||||
{assistantCommentModal.saving ? "Сохраняю..." : "Готово"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="tab"
|
|
||||||
onClick={() => closeAssistantCommentModal()}
|
|
||||||
disabled={assistantCommentModal.saving}
|
|
||||||
>
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
AsyncEvalRunStatusResponse,
|
AsyncEvalRunStatusResponse,
|
||||||
AutoGenPersonalityCatalogResponse,
|
AutoGenPersonalityCatalogResponse,
|
||||||
AutoGenHistoryResponse,
|
AutoGenHistoryResponse,
|
||||||
|
AutoGenHistoryRecord,
|
||||||
AutoGenMode,
|
AutoGenMode,
|
||||||
AutoRunAnnotationsResponse,
|
AutoRunAnnotationsResponse,
|
||||||
AutoRunAnnotationRecord,
|
AutoRunAnnotationRecord,
|
||||||
@@ -230,6 +231,8 @@ export const apiClient = {
|
|||||||
evalTarget?: "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0";
|
evalTarget?: "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0";
|
||||||
compareWithReportFile?: string;
|
compareWithReportFile?: string;
|
||||||
questions?: string[];
|
questions?: string[];
|
||||||
|
scenarioQuestions?: string[];
|
||||||
|
scenarioTitle?: string;
|
||||||
analysisDate?: string;
|
analysisDate?: string;
|
||||||
}): Promise<AsyncEvalRunStartResponse> {
|
}): Promise<AsyncEvalRunStartResponse> {
|
||||||
return request("/eval/run-async/start", {
|
return request("/eval/run-async/start", {
|
||||||
@@ -256,6 +259,8 @@ export const apiClient = {
|
|||||||
eval_target: input.evalTarget,
|
eval_target: input.evalTarget,
|
||||||
compare_with_report_file: input.compareWithReportFile,
|
compare_with_report_file: input.compareWithReportFile,
|
||||||
questions: input.questions,
|
questions: input.questions,
|
||||||
|
scenarioQuestions: input.scenarioQuestions,
|
||||||
|
scenarioTitle: input.scenarioTitle,
|
||||||
analysis_date: input.analysisDate
|
analysis_date: input.analysisDate
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -342,6 +347,24 @@ export const apiClient = {
|
|||||||
return request(`/assistant/session/${sessionId}`);
|
return request(`/assistant/session/${sessionId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async saveAutoRunAssistantSession(input: {
|
||||||
|
session_id: string;
|
||||||
|
title: string;
|
||||||
|
generated_by?: string;
|
||||||
|
context?: {
|
||||||
|
llm_provider?: string;
|
||||||
|
model?: string;
|
||||||
|
assistant_prompt_version?: string;
|
||||||
|
decomposition_prompt_version?: string;
|
||||||
|
prompt_fingerprint?: string;
|
||||||
|
};
|
||||||
|
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
|
||||||
|
return request("/autoruns/autogen/save-assistant-session", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async loadAssistantAnnotations(input?: {
|
async loadAssistantAnnotations(input?: {
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -485,6 +508,28 @@ export const apiClient = {
|
|||||||
return request("/autoruns/autogen/personality-catalog");
|
return request("/autoruns/autogen/personality-catalog");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateAutoRunAutogenQuestions(input: {
|
||||||
|
generation_id: string;
|
||||||
|
questions: string[];
|
||||||
|
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
|
||||||
|
return request(`/autoruns/autogen/history/${encodeURIComponent(input.generation_id)}/questions`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({
|
||||||
|
questions: input.questions
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteAutoRunAutogenHistoryRecord(generationId: string): Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
generation_id: string;
|
||||||
|
deleted_files: string[];
|
||||||
|
}> {
|
||||||
|
return request(`/autoruns/autogen/history/${encodeURIComponent(generationId)}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async generateAutoRunQuestions(input: {
|
async generateAutoRunQuestions(input: {
|
||||||
mode: AutoGenMode;
|
mode: AutoGenMode;
|
||||||
count: number;
|
count: number;
|
||||||
@@ -508,7 +553,7 @@ export const apiClient = {
|
|||||||
autogen_personality_id?: string;
|
autogen_personality_id?: string;
|
||||||
autogen_personality_prompt?: string;
|
autogen_personality_prompt?: string;
|
||||||
};
|
};
|
||||||
}): Promise<{ ok: boolean; generation: { generation_id: string; created_at: string; mode: AutoGenMode; count: number; domain: string | null; questions: string[]; generated_by: string | null; saved_case_set_file: string | null; context: Record<string, unknown> | null } }> {
|
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
|
||||||
return request("/autoruns/autogen/generate", {
|
return request("/autoruns/autogen/generate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(input)
|
body: JSON.stringify(input)
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ interface AssistantPanelProps {
|
|||||||
onUseMockChange: (value: boolean) => void;
|
onUseMockChange: (value: boolean) => void;
|
||||||
onSend: () => Promise<void> | void;
|
onSend: () => Promise<void> | void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
|
onSaveSession?: () => void;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
|
saveBusy?: boolean;
|
||||||
|
saveDisabled?: boolean;
|
||||||
statusText: string;
|
statusText: string;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
showSaveAction?: boolean;
|
||||||
showCommentAction?: boolean;
|
showCommentAction?: boolean;
|
||||||
onCommentAssistantMessage?: (item: AssistantConversationItem, index: number) => void;
|
onCommentAssistantMessage?: (item: AssistantConversationItem, index: number) => void;
|
||||||
isAssistantMessageCommented?: (item: AssistantConversationItem, index: number) => boolean;
|
isAssistantMessageCommented?: (item: AssistantConversationItem, index: number) => boolean;
|
||||||
@@ -295,9 +299,13 @@ export function AssistantPanel({
|
|||||||
onUseMockChange,
|
onUseMockChange,
|
||||||
onSend,
|
onSend,
|
||||||
onClear,
|
onClear,
|
||||||
|
onSaveSession,
|
||||||
busy,
|
busy,
|
||||||
|
saveBusy = false,
|
||||||
|
saveDisabled = false,
|
||||||
statusText,
|
statusText,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
showSaveAction = false,
|
||||||
showCommentAction = false,
|
showCommentAction = false,
|
||||||
onCommentAssistantMessage,
|
onCommentAssistantMessage,
|
||||||
isAssistantMessageCommented,
|
isAssistantMessageCommented,
|
||||||
@@ -383,6 +391,16 @@ export function AssistantPanel({
|
|||||||
>
|
>
|
||||||
Скопировать техчат
|
Скопировать техчат
|
||||||
</button>
|
</button>
|
||||||
|
{showSaveAction ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="assistant-copy-btn"
|
||||||
|
onClick={() => onSaveSession?.()}
|
||||||
|
disabled={saveBusy || saveDisabled}
|
||||||
|
>
|
||||||
|
{saveBusy ? "Сохраняю..." : "Сохранить"}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<button type="button" className="assistant-copy-btn" onClick={() => onClear()} disabled={busy && conversation.length === 0}>
|
<button type="button" className="assistant-copy-btn" onClick={() => onClear()} disabled={busy && conversation.length === 0}>
|
||||||
Сбросить сессию
|
Сбросить сессию
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ interface AutoRunsHistoryPanelProps {
|
|||||||
showSettingsMode: boolean;
|
showSettingsMode: boolean;
|
||||||
showAutoRunsMode: boolean;
|
showAutoRunsMode: boolean;
|
||||||
showAssistantMode: boolean;
|
showAssistantMode: boolean;
|
||||||
showDecompositionMode: boolean;
|
|
||||||
showProgressMode: boolean;
|
showProgressMode: boolean;
|
||||||
showCommentsMode: boolean;
|
showCommentsMode: boolean;
|
||||||
onLog?: (message: string) => void;
|
onLog?: (message: string) => void;
|
||||||
@@ -112,6 +111,30 @@ interface AssistantLiveCommentModalState {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AssistantLiveSaveModalState {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
saving: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SavedSessionQuestionDeleteModalState {
|
||||||
|
open: boolean;
|
||||||
|
generationId: string;
|
||||||
|
questionIndex: number;
|
||||||
|
questionText: string;
|
||||||
|
saving: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoGenDeleteModalState {
|
||||||
|
open: boolean;
|
||||||
|
generationId: string;
|
||||||
|
title: string;
|
||||||
|
saving: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface AutoGenSettingsState {
|
interface AutoGenSettingsState {
|
||||||
mode: AutoGenMode;
|
mode: AutoGenMode;
|
||||||
count: number;
|
count: number;
|
||||||
@@ -259,6 +282,19 @@ function formatDateTime(iso: string | null): string {
|
|||||||
return new Date(parsed).toLocaleString("ru-RU");
|
return new Date(parsed).toLocaleString("ru-RU");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAutoGenModeLabel(mode: AutoGenMode): string {
|
||||||
|
if (mode === "saved_user_sessions") {
|
||||||
|
return "Пользовательские сессии";
|
||||||
|
}
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSavedSessionDefaultTitle(items: AssistantConversationItem[]): string {
|
||||||
|
const lastMessage = items[items.length - 1];
|
||||||
|
const timestamp = formatDateTime(lastMessage?.created_at ?? new Date().toISOString());
|
||||||
|
return `Ручная сессия ${timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
function toPercent(closed: number, total: number): number {
|
function toPercent(closed: number, total: number): number {
|
||||||
if (total <= 0) return 0;
|
if (total <= 0) return 0;
|
||||||
return Math.max(0, Math.min(100, Number(((closed / total) * 100).toFixed(1))));
|
return Math.max(0, Math.min(100, Number(((closed / total) * 100).toFixed(1))));
|
||||||
@@ -282,10 +318,6 @@ function trendLabel(value: "up" | "down" | "flat"): string {
|
|||||||
return "Без изменений";
|
return "Без изменений";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedCase(cases: AutoRunCaseSummary[], caseId: string): AutoRunCaseSummary | null {
|
|
||||||
return cases.find((item) => item.case_id === caseId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRatingDots(rating: number): string {
|
function renderRatingDots(rating: number): string {
|
||||||
const safe = Math.max(1, Math.min(5, Math.round(rating)));
|
const safe = Math.max(1, Math.min(5, Math.round(rating)));
|
||||||
return `${"●".repeat(safe)}${"○".repeat(5 - safe)}`;
|
return `${"●".repeat(safe)}${"○".repeat(5 - safe)}`;
|
||||||
@@ -527,7 +559,6 @@ export function AutoRunsHistoryPanel({
|
|||||||
showSettingsMode,
|
showSettingsMode,
|
||||||
showAutoRunsMode,
|
showAutoRunsMode,
|
||||||
showAssistantMode,
|
showAssistantMode,
|
||||||
showDecompositionMode,
|
|
||||||
showProgressMode,
|
showProgressMode,
|
||||||
showCommentsMode,
|
showCommentsMode,
|
||||||
onLog
|
onLog
|
||||||
@@ -598,19 +629,44 @@ export function AutoRunsHistoryPanel({
|
|||||||
saving: false,
|
saving: false,
|
||||||
error: ""
|
error: ""
|
||||||
});
|
});
|
||||||
|
const [assistantLiveSaveModal, setAssistantLiveSaveModal] = useState<AssistantLiveSaveModalState>({
|
||||||
|
open: false,
|
||||||
|
title: "",
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
});
|
||||||
|
const [savedSessionQuestionDeleteModal, setSavedSessionQuestionDeleteModal] = useState<SavedSessionQuestionDeleteModalState>({
|
||||||
|
open: false,
|
||||||
|
generationId: "",
|
||||||
|
questionIndex: -1,
|
||||||
|
questionText: "",
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
});
|
||||||
|
const [autoGenDeleteModal, setAutoGenDeleteModal] = useState<AutoGenDeleteModalState>({
|
||||||
|
open: false,
|
||||||
|
generationId: "",
|
||||||
|
title: "",
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
});
|
||||||
|
|
||||||
const initialLoadDoneRef = useRef(false);
|
const initialLoadDoneRef = useRef(false);
|
||||||
const asyncJobPollTimerRef = useRef<number | null>(null);
|
const asyncJobPollTimerRef = useRef<number | null>(null);
|
||||||
|
const isSavedUserSessionsMode = autoGenSettings.mode === "saved_user_sessions";
|
||||||
const selectedPersonality = useMemo(
|
const selectedPersonality = useMemo(
|
||||||
() => autogenPersonalities.find((item) => item.id === autoGenSettings.personalityId) ?? autogenPersonalities[0] ?? AUTOGEN_PERSONALITIES[0],
|
() => autogenPersonalities.find((item) => item.id === autoGenSettings.personalityId) ?? autogenPersonalities[0] ?? AUTOGEN_PERSONALITIES[0],
|
||||||
[autoGenSettings.personalityId, autogenPersonalities]
|
[autoGenSettings.personalityId, autogenPersonalities]
|
||||||
);
|
);
|
||||||
|
const visibleAutoGenHistory = useMemo(
|
||||||
|
() => autoGenHistory.filter((item) => item.mode === autoGenSettings.mode),
|
||||||
|
[autoGenHistory, autoGenSettings.mode]
|
||||||
|
);
|
||||||
const selectedAutogenGeneration = useMemo(
|
const selectedAutogenGeneration = useMemo(
|
||||||
() => autoGenHistory.find((item) => item.generation_id === selectedAutogenGenerationId) ?? autoGenHistory[0] ?? null,
|
() => visibleAutoGenHistory.find((item) => item.generation_id === selectedAutogenGenerationId) ?? visibleAutoGenHistory[0] ?? null,
|
||||||
[autoGenHistory, selectedAutogenGenerationId]
|
[selectedAutogenGenerationId, visibleAutoGenHistory]
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeCase = runDetail ? getSelectedCase(runDetail.cases, selectedCaseId) : null;
|
|
||||||
const visibleAnnotations = useMemo(
|
const visibleAnnotations = useMemo(
|
||||||
() => (hideResolvedAnnotations ? annotations.filter((item) => !item.resolved) : annotations),
|
() => (hideResolvedAnnotations ? annotations.filter((item) => !item.resolved) : annotations),
|
||||||
[annotations, hideResolvedAnnotations]
|
[annotations, hideResolvedAnnotations]
|
||||||
@@ -731,11 +787,56 @@ export function AutoRunsHistoryPanel({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const copyRunIdToClipboard = useCallback(
|
const closeAssistantLiveSaveModal = useCallback((options?: { force?: boolean }) => {
|
||||||
async (event: React.SyntheticEvent, runId: string) => {
|
setAssistantLiveSaveModal((prev) => {
|
||||||
|
if (prev.saving && !options?.force) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
open: false,
|
||||||
|
title: "",
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeSavedSessionQuestionDeleteModal = useCallback((options?: { force?: boolean }) => {
|
||||||
|
setSavedSessionQuestionDeleteModal((prev) => {
|
||||||
|
if (prev.saving && !options?.force) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
open: false,
|
||||||
|
generationId: "",
|
||||||
|
questionIndex: -1,
|
||||||
|
questionText: "",
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeAutoGenDeleteModal = useCallback((options?: { force?: boolean }) => {
|
||||||
|
setAutoGenDeleteModal((prev) => {
|
||||||
|
if (prev.saving && !options?.force) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
open: false,
|
||||||
|
generationId: "",
|
||||||
|
title: "",
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyIdentifierToClipboard = useCallback(
|
||||||
|
async (event: React.SyntheticEvent, valueRaw: string, label: string) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const value = String(runId ?? "").trim();
|
const value = String(valueRaw ?? "").trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -753,11 +854,11 @@ export function AutoRunsHistoryPanel({
|
|||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
document.body.removeChild(textarea);
|
document.body.removeChild(textarea);
|
||||||
}
|
}
|
||||||
log(`run id copied: ${value}`);
|
log(`${label} copied: ${value}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
setErrorText(`Копирование run id: ${message}`);
|
setErrorText(`Копирование ${label}: ${message}`);
|
||||||
log(`copy run id error: ${message}`);
|
log(`copy ${label} error: ${message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[log]
|
[log]
|
||||||
@@ -844,6 +945,80 @@ export function AutoRunsHistoryPanel({
|
|||||||
prompts
|
prompts
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const openAssistantLiveSaveModal = useCallback(() => {
|
||||||
|
if (!assistantLiveSessionId.trim() || assistantLiveConversation.length === 0) {
|
||||||
|
setAssistantLiveError("Сначала получите хотя бы один ответ в живой сессии ассистента.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAssistantLiveError("");
|
||||||
|
setAssistantLiveSaveModal({
|
||||||
|
open: true,
|
||||||
|
title: buildSavedSessionDefaultTitle(assistantLiveConversation),
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
});
|
||||||
|
}, [assistantLiveConversation, assistantLiveSessionId]);
|
||||||
|
|
||||||
|
const submitAssistantLiveSaveModal = useCallback(async () => {
|
||||||
|
const sessionId = assistantLiveSessionId.trim();
|
||||||
|
const title = assistantLiveSaveModal.title.trim();
|
||||||
|
if (!sessionId) {
|
||||||
|
setAssistantLiveSaveModal((prev) => ({ ...prev, error: "Активная сессия ассистента не найдена." }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!title) {
|
||||||
|
setAssistantLiveSaveModal((prev) => ({ ...prev, error: "Укажите название сессии." }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAssistantLiveSaveModal((prev) => ({ ...prev, saving: true, error: "" }));
|
||||||
|
try {
|
||||||
|
const promptFingerprint = [
|
||||||
|
prompts.systemPrompt,
|
||||||
|
prompts.developerPrompt,
|
||||||
|
prompts.domainPrompt,
|
||||||
|
prompts.schemaNotes,
|
||||||
|
prompts.fewShotExamples
|
||||||
|
].join("||");
|
||||||
|
const payload = await apiClient.saveAutoRunAssistantSession({
|
||||||
|
session_id: sessionId,
|
||||||
|
title,
|
||||||
|
generated_by: autoGenSettings.generatedBy.trim() || undefined,
|
||||||
|
context: {
|
||||||
|
llm_provider: connection.llmProvider,
|
||||||
|
model: connection.model,
|
||||||
|
assistant_prompt_version: assistantPromptVersion,
|
||||||
|
decomposition_prompt_version: decompositionPromptVersion,
|
||||||
|
prompt_fingerprint: promptFingerprint
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setAutoGenHistory((prev) => [payload.generation, ...prev.filter((item) => item.generation_id !== payload.generation.generation_id)]);
|
||||||
|
setAutoGenSettings((prev) => ({ ...prev, mode: "saved_user_sessions" }));
|
||||||
|
setSelectedAutogenGenerationId(payload.generation.generation_id);
|
||||||
|
closeAssistantLiveSaveModal({ force: true });
|
||||||
|
log(`Живая сессия сохранена в автопрогоны: ${payload.generation.generation_id}`);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
setAssistantLiveSaveModal((prev) => ({ ...prev, saving: false, error: message }));
|
||||||
|
log(`Assistant live save error: ${message}`);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
assistantLiveSaveModal.title,
|
||||||
|
assistantLiveSessionId,
|
||||||
|
assistantPromptVersion,
|
||||||
|
autoGenSettings.generatedBy,
|
||||||
|
closeAssistantLiveSaveModal,
|
||||||
|
connection.llmProvider,
|
||||||
|
connection.model,
|
||||||
|
decompositionPromptVersion,
|
||||||
|
log,
|
||||||
|
prompts.developerPrompt,
|
||||||
|
prompts.domainPrompt,
|
||||||
|
prompts.fewShotExamples,
|
||||||
|
prompts.schemaNotes,
|
||||||
|
prompts.systemPrompt
|
||||||
|
]);
|
||||||
|
|
||||||
const commitLimitInput = useCallback(
|
const commitLimitInput = useCallback(
|
||||||
(raw: string) => {
|
(raw: string) => {
|
||||||
const normalized = raw.trim();
|
const normalized = raw.trim();
|
||||||
@@ -1000,6 +1175,9 @@ export function AutoRunsHistoryPanel({
|
|||||||
setAutoGenBusy(true);
|
setAutoGenBusy(true);
|
||||||
setErrorText("");
|
setErrorText("");
|
||||||
try {
|
try {
|
||||||
|
if (autoGenSettings.mode === "saved_user_sessions") {
|
||||||
|
throw new Error("Пользовательские сессии сохраняются из живого чата, а не генерируются автоматически.");
|
||||||
|
}
|
||||||
const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? "";
|
const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? "";
|
||||||
const promptFingerprint = [
|
const promptFingerprint = [
|
||||||
prompts.systemPrompt,
|
prompts.systemPrompt,
|
||||||
@@ -1283,16 +1461,19 @@ export function AutoRunsHistoryPanel({
|
|||||||
|
|
||||||
const useMockForRun = filters.useMock === "true";
|
const useMockForRun = filters.useMock === "true";
|
||||||
const effectiveAnalysisDate = normalizeAnalysisDateInput(analysisDate);
|
const effectiveAnalysisDate = normalizeAnalysisDateInput(analysisDate);
|
||||||
|
const useScenarioReplay = generation.mode === "saved_user_sessions";
|
||||||
const payload = await apiClient.startEvalRunAsync({
|
const payload = await apiClient.startEvalRunAsync({
|
||||||
connection,
|
connection,
|
||||||
prompts,
|
prompts,
|
||||||
promptVersion: assistantPromptVersion,
|
promptVersion: assistantPromptVersion,
|
||||||
mode: "single-pass-strict",
|
mode: "single-pass-strict",
|
||||||
caseSetFile: generation.saved_case_set_file ?? undefined,
|
caseSetFile: useScenarioReplay ? undefined : generation.saved_case_set_file ?? undefined,
|
||||||
useMock: useMockForRun,
|
useMock: useMockForRun,
|
||||||
evalTarget: "assistant_stage1",
|
evalTarget: "assistant_stage1",
|
||||||
questions: questionsForRun,
|
questions: useScenarioReplay ? undefined : questionsForRun,
|
||||||
analysisDate: effectiveAnalysisDate || undefined
|
scenarioQuestions: useScenarioReplay ? questionsForRun : undefined,
|
||||||
|
scenarioTitle: useScenarioReplay ? generation.title ?? undefined : undefined,
|
||||||
|
analysisDate: useScenarioReplay ? undefined : effectiveAnalysisDate || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const liveJob = payload.job;
|
const liveJob = payload.job;
|
||||||
@@ -1307,7 +1488,11 @@ export function AutoRunsHistoryPanel({
|
|||||||
log(
|
log(
|
||||||
`Запущен async-прогон job=${liveJob.job_id}, run_id=${liveJob.run_id}, вопросов=${questionsForRun.length}` +
|
`Запущен async-прогон job=${liveJob.job_id}, run_id=${liveJob.run_id}, вопросов=${questionsForRun.length}` +
|
||||||
(generation.saved_case_set_file ? `, base_case_set=${generation.saved_case_set_file}` : "") +
|
(generation.saved_case_set_file ? `, base_case_set=${generation.saved_case_set_file}` : "") +
|
||||||
(effectiveAnalysisDate ? `, analysis_date=${effectiveAnalysisDate}` : ", analysis_date=current_state")
|
(useScenarioReplay
|
||||||
|
? ", replay_mode=saved_user_session_scenario"
|
||||||
|
: effectiveAnalysisDate
|
||||||
|
? `, analysis_date=${effectiveAnalysisDate}`
|
||||||
|
: ", analysis_date=current_state")
|
||||||
);
|
);
|
||||||
void pollAsyncJobStatus(liveJob.job_id);
|
void pollAsyncJobStatus(liveJob.job_id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1506,6 +1691,97 @@ export function AutoRunsHistoryPanel({
|
|||||||
closeAssistantLiveCommentModal
|
closeAssistantLiveCommentModal
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const requestDeleteSavedSessionQuestion = useCallback(
|
||||||
|
(questionIndex: number) => {
|
||||||
|
if (!selectedAutogenGeneration || selectedAutogenGeneration.mode !== "saved_user_sessions") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const questionText = editableGeneratedQuestions[questionIndex] ?? "";
|
||||||
|
setSavedSessionQuestionDeleteModal({
|
||||||
|
open: true,
|
||||||
|
generationId: selectedAutogenGeneration.generation_id,
|
||||||
|
questionIndex,
|
||||||
|
questionText,
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[editableGeneratedQuestions, selectedAutogenGeneration]
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitSavedSessionQuestionDelete = useCallback(async () => {
|
||||||
|
const generationId = savedSessionQuestionDeleteModal.generationId;
|
||||||
|
const questionIndex = savedSessionQuestionDeleteModal.questionIndex;
|
||||||
|
if (!generationId || questionIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextQuestions = editableGeneratedQuestions.filter((_, index) => index !== questionIndex);
|
||||||
|
if (nextQuestions.length === 0) {
|
||||||
|
setSavedSessionQuestionDeleteModal((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: "Нельзя удалить последний вопрос из сохраненной сессии."
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSavedSessionQuestionDeleteModal((prev) => ({ ...prev, saving: true, error: "" }));
|
||||||
|
try {
|
||||||
|
const payload = await apiClient.updateAutoRunAutogenQuestions({
|
||||||
|
generation_id: generationId,
|
||||||
|
questions: nextQuestions
|
||||||
|
});
|
||||||
|
setAutoGenHistory((prev) =>
|
||||||
|
prev.map((item) => (item.generation_id === generationId ? payload.generation : item))
|
||||||
|
);
|
||||||
|
setEditableGeneratedQuestions(payload.generation.questions);
|
||||||
|
closeSavedSessionQuestionDeleteModal({ force: true });
|
||||||
|
log(`Обновлена сохраненная сессия: ${generationId}`);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
setSavedSessionQuestionDeleteModal((prev) => ({ ...prev, saving: false, error: message }));
|
||||||
|
log(`Saved session question delete error: ${message}`);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
closeSavedSessionQuestionDeleteModal,
|
||||||
|
editableGeneratedQuestions,
|
||||||
|
log,
|
||||||
|
savedSessionQuestionDeleteModal.generationId,
|
||||||
|
savedSessionQuestionDeleteModal.questionIndex
|
||||||
|
]);
|
||||||
|
|
||||||
|
const openAutoGenDeleteModal = useCallback((item: AutoGenHistoryRecord) => {
|
||||||
|
setAutoGenDeleteModal({
|
||||||
|
open: true,
|
||||||
|
generationId: item.generation_id,
|
||||||
|
title: item.title ?? `${formatAutoGenModeLabel(item.mode)} ${formatDateTime(item.created_at)}`,
|
||||||
|
saving: false,
|
||||||
|
error: ""
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submitAutoGenDeleteModal = useCallback(async () => {
|
||||||
|
const generationId = autoGenDeleteModal.generationId.trim();
|
||||||
|
if (!generationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAutoGenDeleteModal((prev) => ({ ...prev, saving: true, error: "" }));
|
||||||
|
try {
|
||||||
|
const payload = await apiClient.deleteAutoRunAutogenHistoryRecord(generationId);
|
||||||
|
setAutoGenHistory((prev) => prev.filter((item) => item.generation_id !== payload.generation_id));
|
||||||
|
closeAutoGenDeleteModal({ force: true });
|
||||||
|
log(
|
||||||
|
`Удален набор автопрогона: ${payload.generation_id}` +
|
||||||
|
(payload.deleted_files.length > 0 ? `, files=${payload.deleted_files.length}` : "")
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
setAutoGenDeleteModal((prev) => ({ ...prev, saving: false, error: message }));
|
||||||
|
log(`Autogen record delete error: ${message}`);
|
||||||
|
}
|
||||||
|
}, [autoGenDeleteModal.generationId, closeAutoGenDeleteModal, log]);
|
||||||
|
|
||||||
const applyLocalAnnotationPatch = useCallback((annotation: AutoRunAnnotationRecord) => {
|
const applyLocalAnnotationPatch = useCallback((annotation: AutoRunAnnotationRecord) => {
|
||||||
setAnnotations((prev) =>
|
setAnnotations((prev) =>
|
||||||
prev.map((item) =>
|
prev.map((item) =>
|
||||||
@@ -1598,11 +1874,11 @@ export function AutoRunsHistoryPanel({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedAutogenGenerationId((prev) => {
|
setSelectedAutogenGenerationId((prev) => {
|
||||||
if (autoGenHistory.length === 0) return "";
|
if (visibleAutoGenHistory.length === 0) return "";
|
||||||
if (prev && autoGenHistory.some((item) => item.generation_id === prev)) return prev;
|
if (prev && visibleAutoGenHistory.some((item) => item.generation_id === prev)) return prev;
|
||||||
return autoGenHistory[0].generation_id;
|
return visibleAutoGenHistory[0].generation_id;
|
||||||
});
|
});
|
||||||
}, [autoGenHistory]);
|
}, [visibleAutoGenHistory]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedAutogenGeneration) {
|
if (!selectedAutogenGeneration) {
|
||||||
@@ -1610,7 +1886,7 @@ export function AutoRunsHistoryPanel({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setEditableGeneratedQuestions([...selectedAutogenGeneration.questions]);
|
setEditableGeneratedQuestions([...selectedAutogenGeneration.questions]);
|
||||||
}, [selectedAutogenGeneration?.generation_id]);
|
}, [selectedAutogenGeneration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLimitInput(String(filters.limit));
|
setLimitInput(String(filters.limit));
|
||||||
@@ -1708,7 +1984,9 @@ export function AutoRunsHistoryPanel({
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
mode:
|
mode:
|
||||||
parsed.autoGenSettings?.mode === "codex_creative" || parsed.autoGenSettings?.mode === "qwen_seed"
|
parsed.autoGenSettings?.mode === "codex_creative" ||
|
||||||
|
parsed.autoGenSettings?.mode === "qwen_seed" ||
|
||||||
|
parsed.autoGenSettings?.mode === "saved_user_sessions"
|
||||||
? parsed.autoGenSettings.mode
|
? parsed.autoGenSettings.mode
|
||||||
: prev.mode,
|
: prev.mode,
|
||||||
count:
|
count:
|
||||||
@@ -1949,140 +2227,155 @@ export function AutoRunsHistoryPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>Автогенерация вопросов</h4>
|
<h4>Автопрогоны</h4>
|
||||||
<div className="autoruns-form-grid">
|
<div className="autoruns-form-grid">
|
||||||
<label>
|
<label>
|
||||||
Режим генерации
|
Режимы
|
||||||
<select
|
<select
|
||||||
value={autoGenSettings.mode}
|
value={autoGenSettings.mode}
|
||||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, mode: event.target.value as AutoGenMode }))}
|
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, mode: event.target.value as AutoGenMode }))}
|
||||||
>
|
>
|
||||||
<option value="codex_creative">codex_creative</option>
|
<option value="codex_creative">codex_creative</option>
|
||||||
<option value="qwen_seed">qwen_seed</option>
|
<option value="qwen_seed">qwen_seed</option>
|
||||||
|
<option value="saved_user_sessions">Пользовательские сессии</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
{!isSavedUserSessionsMode ? (
|
||||||
Кол-во
|
<>
|
||||||
<input
|
<label>
|
||||||
type="number"
|
Кол-во
|
||||||
min={1}
|
<input
|
||||||
max={200}
|
type="number"
|
||||||
value={autogenCountInput}
|
min={1}
|
||||||
onChange={(event) => {
|
max={200}
|
||||||
const raw = event.target.value;
|
value={autogenCountInput}
|
||||||
if (raw === "" || /^\d+$/.test(raw)) {
|
onChange={(event) => {
|
||||||
setAutogenCountInput(raw);
|
const raw = event.target.value;
|
||||||
}
|
if (raw === "" || /^\d+$/.test(raw)) {
|
||||||
}}
|
setAutogenCountInput(raw);
|
||||||
onBlur={(event) => commitAutogenCountInput(event.target.value)}
|
}
|
||||||
onKeyDown={(event) => {
|
}}
|
||||||
if (event.key === "Enter") {
|
onBlur={(event) => commitAutogenCountInput(event.target.value)}
|
||||||
commitAutogenCountInput((event.target as HTMLInputElement).value);
|
onKeyDown={(event) => {
|
||||||
}
|
if (event.key === "Enter") {
|
||||||
}}
|
commitAutogenCountInput((event.target as HTMLInputElement).value);
|
||||||
/>
|
}
|
||||||
</label>
|
}}
|
||||||
<label>
|
/>
|
||||||
Личность автогенерации
|
</label>
|
||||||
<select
|
<label>
|
||||||
value={autoGenSettings.personalityId}
|
Личность автогенерации
|
||||||
onChange={(event) =>
|
<select
|
||||||
setAutoGenSettings((prev) => ({
|
value={autoGenSettings.personalityId}
|
||||||
...prev,
|
onChange={(event) =>
|
||||||
personalityId: event.target.value as AutoGenPersonalityId
|
setAutoGenSettings((prev) => ({
|
||||||
}))
|
...prev,
|
||||||
}
|
personalityId: event.target.value as AutoGenPersonalityId
|
||||||
>
|
}))
|
||||||
{autogenPersonalities.map((item) => (
|
|
||||||
<option key={item.id} value={item.id}>
|
|
||||||
{item.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Кто генерирует
|
|
||||||
<input
|
|
||||||
value={autoGenSettings.generatedBy}
|
|
||||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, generatedBy: event.target.value }))}
|
|
||||||
placeholder="manual_reviewer"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="full-width">
|
|
||||||
Промпт личности
|
|
||||||
<textarea
|
|
||||||
className="autoruns-personality-prompt"
|
|
||||||
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
|
|
||||||
onChange={(event) =>
|
|
||||||
setAutoGenSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
personalityPrompts: {
|
|
||||||
...prev.personalityPrompts,
|
|
||||||
[prev.personalityId]: event.target.value
|
|
||||||
}
|
}
|
||||||
}))
|
>
|
||||||
}
|
{autogenPersonalities.map((item) => (
|
||||||
placeholder="Текст промпта для выбранной личности автогенерации"
|
<option key={item.id} value={item.id}>
|
||||||
style={{ height: `${autogenPersonalityPromptHeight}px` }}
|
{item.label}
|
||||||
onMouseUp={captureAutogenPromptHeight}
|
</option>
|
||||||
onTouchEnd={captureAutogenPromptHeight}
|
))}
|
||||||
/>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox-row">
|
<label>
|
||||||
<input
|
Кто генерирует
|
||||||
type="checkbox"
|
<input
|
||||||
checked={autoGenSettings.persistToEvalCases}
|
value={autoGenSettings.generatedBy}
|
||||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, persistToEvalCases: event.target.checked }))}
|
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, generatedBy: event.target.value }))}
|
||||||
/>
|
placeholder="manual_reviewer"
|
||||||
Сохранять кейс-сет в `eval_cases`
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="full-width">
|
||||||
|
Промпт личности
|
||||||
|
<textarea
|
||||||
|
className="autoruns-personality-prompt"
|
||||||
|
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
|
||||||
|
onChange={(event) =>
|
||||||
|
setAutoGenSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
personalityPrompts: {
|
||||||
|
...prev.personalityPrompts,
|
||||||
|
[prev.personalityId]: event.target.value
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Текст промпта для выбранной личности автогенерации"
|
||||||
|
style={{ height: `${autogenPersonalityPromptHeight}px` }}
|
||||||
|
onMouseUp={captureAutogenPromptHeight}
|
||||||
|
onTouchEnd={captureAutogenPromptHeight}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="checkbox-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoGenSettings.persistToEvalCases}
|
||||||
|
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, persistToEvalCases: event.target.checked }))}
|
||||||
|
/>
|
||||||
|
Сохранять кейс-сет в `eval_cases`
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="autoruns-form-grid">
|
{!isSavedUserSessionsMode ? (
|
||||||
<label>
|
<div className="autoruns-form-grid">
|
||||||
Дата анализа (срез)
|
<label>
|
||||||
<input
|
Дата анализа (срез)
|
||||||
type="date"
|
<input
|
||||||
value={analysisDate}
|
type="date"
|
||||||
onChange={(event) => setAnalysisDate(normalizeAnalysisDateInput(event.target.value))}
|
value={analysisDate}
|
||||||
/>
|
onChange={(event) => setAnalysisDate(normalizeAnalysisDateInput(event.target.value))}
|
||||||
</label>
|
/>
|
||||||
<div className="button-row">
|
</label>
|
||||||
<button type="button" className="tab" disabled={!analysisDate} onClick={() => setAnalysisDate("")}>
|
<div className="button-row">
|
||||||
Сбросить дату среза
|
<button type="button" className="tab" disabled={!analysisDate} onClick={() => setAnalysisDate("")}>
|
||||||
</button>
|
Сбросить дату среза
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
{!isSavedUserSessionsMode ? (
|
||||||
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
<>
|
||||||
</button>
|
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
||||||
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
||||||
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
</button>
|
||||||
</button>
|
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
||||||
|
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="autoruns-run-launch-btn"
|
className="autoruns-run-launch-btn"
|
||||||
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0}
|
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration}
|
||||||
onClick={() => void runAutogenCampaign()}
|
onClick={() => void runAutogenCampaign()}
|
||||||
>
|
>
|
||||||
{autogenRunBusy ? "Запускаю..." : "Запустить прогоны"}
|
{autogenRunBusy ? "Запускаю..." : "Запустить прогон"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="autoruns-form-grid">
|
<div className="autoruns-form-grid">
|
||||||
<label className="full-width">
|
<label className="full-width">
|
||||||
Кейс-сет для запуска
|
{isSavedUserSessionsMode ? "Сохраненная сессия" : "Кейс-сет для запуска"}
|
||||||
<select
|
<select
|
||||||
value={selectedAutogenGenerationId}
|
value={selectedAutogenGenerationId}
|
||||||
onChange={(event) => setSelectedAutogenGenerationId(event.target.value)}
|
onChange={(event) => setSelectedAutogenGenerationId(event.target.value)}
|
||||||
disabled={autoGenHistory.length === 0}
|
disabled={visibleAutoGenHistory.length === 0}
|
||||||
>
|
>
|
||||||
{autoGenHistory.length === 0 ? <option value="">нет генераций</option> : null}
|
{visibleAutoGenHistory.length === 0 ? (
|
||||||
{autoGenHistory.map((item) => (
|
<option value="">
|
||||||
|
{isSavedUserSessionsMode ? "нет сохраненных сессий" : "нет генераций"}
|
||||||
|
</option>
|
||||||
|
) : null}
|
||||||
|
{visibleAutoGenHistory.map((item) => (
|
||||||
<option key={item.generation_id} value={item.generation_id}>
|
<option key={item.generation_id} value={item.generation_id}>
|
||||||
{formatDateTime(item.created_at)} | {item.mode} | {item.count} | {item.saved_case_set_file ?? "без файла"}
|
{formatDateTime(item.created_at)} | {item.title ?? formatAutoGenModeLabel(item.mode)} | {item.count}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -2101,7 +2394,11 @@ export function AutoRunsHistoryPanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{editableGeneratedQuestions.length === 0 ? (
|
{editableGeneratedQuestions.length === 0 ? (
|
||||||
<p className="muted">Список вопросов пуст. Сгенерируйте пачку или восстановите из выбранной генерации.</p>
|
<p className="muted">
|
||||||
|
{isSavedUserSessionsMode
|
||||||
|
? "Список вопросов пуст. Сначала сохраните живую пользовательскую сессию."
|
||||||
|
: "Список вопросов пуст. Сгенерируйте пачку или восстановите из выбранной генерации."}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="autoruns-generated-questions-list">
|
<div className="autoruns-generated-questions-list">
|
||||||
{editableGeneratedQuestions.map((question, index) => (
|
{editableGeneratedQuestions.map((question, index) => (
|
||||||
@@ -2110,41 +2407,93 @@ export function AutoRunsHistoryPanel({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="autoruns-remove-question-btn"
|
className="autoruns-remove-question-btn"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
setEditableGeneratedQuestions((prev) => prev.filter((_, itemIndex) => itemIndex !== index))
|
if (isSavedUserSessionsMode) {
|
||||||
}
|
requestDeleteSavedSessionQuestion(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditableGeneratedQuestions((prev) => prev.filter((_, itemIndex) => itemIndex !== index));
|
||||||
|
}}
|
||||||
title="Удалить вопрос из запуска"
|
title="Удалить вопрос из запуска"
|
||||||
aria-label="Удалить вопрос из запуска"
|
aria-label="Удалить вопрос из запуска"
|
||||||
>
|
>
|
||||||
+
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="muted">Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету.</p>
|
<p className="muted">
|
||||||
|
{isSavedUserSessionsMode
|
||||||
|
? "Запуск воспроизводит сохраненную пользовательскую сессию как один последовательный multi-turn сценарий assistant_stage1."
|
||||||
|
: "Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету."}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="autoruns-autogen-list">
|
<div className="autoruns-autogen-list">
|
||||||
{autogenHistoryBusy ? <p className="muted">Загружаю историю автогенераций...</p> : null}
|
{autogenHistoryBusy ? (
|
||||||
{!autogenHistoryBusy && autoGenHistory.length === 0 ? <p className="muted">История автогенераций пока пустая.</p> : null}
|
<p className="muted">
|
||||||
{autoGenHistory.slice(0, 30).map((item) => (
|
{isSavedUserSessionsMode ? "Загружаю сохраненные пользовательские сессии..." : "Загружаю историю автогенераций..."}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{!autogenHistoryBusy && visibleAutoGenHistory.length === 0 ? (
|
||||||
|
<p className="muted">
|
||||||
|
{isSavedUserSessionsMode ? "Сохраненные пользовательские сессии пока пусты." : "История автогенераций пока пустая."}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{visibleAutoGenHistory.slice(0, 30).map((item) => (
|
||||||
<article
|
<article
|
||||||
key={item.generation_id}
|
key={item.generation_id}
|
||||||
className={selectedAutogenGenerationId === item.generation_id ? "autoruns-autogen-item selected" : "autoruns-autogen-item"}
|
className={selectedAutogenGenerationId === item.generation_id ? "autoruns-autogen-item selected" : "autoruns-autogen-item"}
|
||||||
onClick={() => setSelectedAutogenGenerationId(item.generation_id)}
|
onClick={() => setSelectedAutogenGenerationId(item.generation_id)}
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<strong>{formatDateTime(item.created_at)}</strong>
|
<strong>{item.title ?? formatDateTime(item.created_at)}</strong>
|
||||||
<span>{item.mode}</span>
|
<div className="autoruns-autogen-card-actions">
|
||||||
|
<span>{formatDateTime(item.created_at)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="autoruns-autogen-delete-btn"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
openAutoGenDeleteModal(item);
|
||||||
|
}}
|
||||||
|
title="Удалить сохраненный набор"
|
||||||
|
aria-label={`Удалить набор ${item.generation_id}`}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="autoruns-run-meta">
|
<div className="autoruns-run-meta autoruns-run-id-row">
|
||||||
id={item.generation_id} | count={item.count}
|
<span>{item.generation_id}</span>
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="autoruns-copy-run-id-btn"
|
||||||
|
onClick={(event) => void copyIdentifierToClipboard(event, item.generation_id, "set id")}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
void copyIdentifierToClipboard(event, item.generation_id, "set id");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Скопировать id набора"
|
||||||
|
aria-label={`Скопировать id набора ${item.generation_id}`}
|
||||||
|
>
|
||||||
|
<CopyOutlineIcon />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="autoruns-run-meta">
|
<div className="autoruns-run-meta">
|
||||||
домен={item.domain ?? "общий"}
|
режим={formatAutoGenModeLabel(item.mode)} | count={item.count}
|
||||||
{item.generated_by ? ` | автор=${item.generated_by}` : ""}
|
|
||||||
</div>
|
</div>
|
||||||
|
{item.domain || item.generated_by ? (
|
||||||
|
<div className="autoruns-run-meta">
|
||||||
|
{item.domain ? `домен=${item.domain}` : "домен=общий"}
|
||||||
|
{item.generated_by ? ` | автор=${item.generated_by}` : ""}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{item.saved_case_set_file ? <div className="autoruns-run-meta">кейс-сет={item.saved_case_set_file}</div> : null}
|
{item.saved_case_set_file ? <div className="autoruns-run-meta">кейс-сет={item.saved_case_set_file}</div> : null}
|
||||||
{(item.questions ?? []).length > 0 ? <p>{item.questions[0]}</p> : null}
|
{(item.questions ?? []).length > 0 ? <p>{item.questions[0]}</p> : null}
|
||||||
</article>
|
</article>
|
||||||
@@ -2219,11 +2568,11 @@ export function AutoRunsHistoryPanel({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="autoruns-copy-run-id-btn"
|
className="autoruns-copy-run-id-btn"
|
||||||
onClick={(event) => void copyRunIdToClipboard(event, run.run_id)}
|
onClick={(event) => void copyIdentifierToClipboard(event, run.run_id, "run id")}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void copyRunIdToClipboard(event, run.run_id);
|
void copyIdentifierToClipboard(event, run.run_id, "run id");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Скопировать run id"
|
title="Скопировать run id"
|
||||||
@@ -2424,9 +2773,13 @@ export function AutoRunsHistoryPanel({
|
|||||||
onUseMockChange={setAssistantLiveUseMock}
|
onUseMockChange={setAssistantLiveUseMock}
|
||||||
onSend={sendAssistantLiveMessage}
|
onSend={sendAssistantLiveMessage}
|
||||||
onClear={resetAssistantLiveSession}
|
onClear={resetAssistantLiveSession}
|
||||||
|
onSaveSession={openAssistantLiveSaveModal}
|
||||||
busy={assistantLiveBusy}
|
busy={assistantLiveBusy}
|
||||||
|
saveBusy={assistantLiveSaveModal.saving}
|
||||||
|
saveDisabled={!assistantLiveSessionId.trim() || assistantLiveConversation.length === 0 || assistantLiveBusy}
|
||||||
statusText={assistantLiveStatus}
|
statusText={assistantLiveStatus}
|
||||||
errorMessage={assistantLiveError}
|
errorMessage={assistantLiveError}
|
||||||
|
showSaveAction
|
||||||
showCommentAction
|
showCommentAction
|
||||||
onCommentAssistantMessage={openAssistantLiveCommentModal}
|
onCommentAssistantMessage={openAssistantLiveCommentModal}
|
||||||
isAssistantMessageCommented={isAssistantLiveMessageCommented}
|
isAssistantMessageCommented={isAssistantLiveMessageCommented}
|
||||||
@@ -2435,42 +2788,6 @@ export function AutoRunsHistoryPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showDecompositionMode ? (
|
|
||||||
<section className="autoruns-col">
|
|
||||||
<div className="autoruns-col-header">
|
|
||||||
<h3>Режим декомпозиции</h3>
|
|
||||||
</div>
|
|
||||||
<div className="autoruns-meta-list">
|
|
||||||
<div>
|
|
||||||
<span>кейс:</span>
|
|
||||||
<strong>{activeCase?.case_id ?? "нет данных"}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>домен:</span>
|
|
||||||
<strong>{activeCase?.domain ?? "нет данных"}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>класс запроса:</span>
|
|
||||||
<strong>{activeCase?.query_class ?? "нет данных"}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>trace:</span>
|
|
||||||
<strong>{activeCase?.trace_id ?? "нет данных"}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h4>Шаги декомпозиции</h4>
|
|
||||||
{(dialog?.decomposition.length ?? 0) > 0 ? (
|
|
||||||
<ol className="autoruns-decomposition-list">
|
|
||||||
{(dialog?.decomposition ?? []).map((item, index) => (
|
|
||||||
<li key={`${index}-${item.slice(0, 24)}`}>{item}</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
) : (
|
|
||||||
<p className="muted">В логах кейса нет явной декомпозиции.</p>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showProgressMode ? (
|
{showProgressMode ? (
|
||||||
<section className="autoruns-col">
|
<section className="autoruns-col">
|
||||||
<div className="autoruns-col-header">
|
<div className="autoruns-col-header">
|
||||||
@@ -2746,6 +3063,104 @@ export function AutoRunsHistoryPanel({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{assistantLiveSaveModal.open ? (
|
||||||
|
<div
|
||||||
|
className="autoruns-comment-modal-backdrop"
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
closeAssistantLiveSaveModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="autoruns-comment-modal">
|
||||||
|
<h3>Сохранить ручную сессию</h3>
|
||||||
|
<p className="muted">Технический чат будет сохранен в автопрогоны как пользовательская multi-turn сессия.</p>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Название
|
||||||
|
<input
|
||||||
|
value={assistantLiveSaveModal.title}
|
||||||
|
onChange={(event) => setAssistantLiveSaveModal((prev) => ({ ...prev, title: event.target.value }))}
|
||||||
|
placeholder="Например: НДС и склад на март 2020"
|
||||||
|
disabled={assistantLiveSaveModal.saving}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{assistantLiveSaveModal.error ? <p className="error-text">{assistantLiveSaveModal.error}</p> : null}
|
||||||
|
|
||||||
|
<div className="button-row">
|
||||||
|
<button type="button" onClick={() => void submitAssistantLiveSaveModal()} disabled={assistantLiveSaveModal.saving}>
|
||||||
|
{assistantLiveSaveModal.saving ? "Сохраняю..." : "Сохранить"}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="tab" onClick={() => closeAssistantLiveSaveModal()} disabled={assistantLiveSaveModal.saving}>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{savedSessionQuestionDeleteModal.open ? (
|
||||||
|
<div
|
||||||
|
className="autoruns-comment-modal-backdrop"
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
closeSavedSessionQuestionDeleteModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="autoruns-comment-modal">
|
||||||
|
<h3>Удалить вопрос</h3>
|
||||||
|
<p className="muted">Действительно удалить вопрос из сохраненной пользовательской сессии?</p>
|
||||||
|
<p className="autoruns-comment-quote">{savedSessionQuestionDeleteModal.questionText}</p>
|
||||||
|
|
||||||
|
{savedSessionQuestionDeleteModal.error ? <p className="error-text">{savedSessionQuestionDeleteModal.error}</p> : null}
|
||||||
|
|
||||||
|
<div className="button-row">
|
||||||
|
<button type="button" onClick={() => void submitSavedSessionQuestionDelete()} disabled={savedSessionQuestionDeleteModal.saving}>
|
||||||
|
{savedSessionQuestionDeleteModal.saving ? "Удаляю..." : "Да"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="tab"
|
||||||
|
onClick={() => closeSavedSessionQuestionDeleteModal()}
|
||||||
|
disabled={savedSessionQuestionDeleteModal.saving}
|
||||||
|
>
|
||||||
|
Нет
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{autoGenDeleteModal.open ? (
|
||||||
|
<div
|
||||||
|
className="autoruns-comment-modal-backdrop"
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
closeAutoGenDeleteModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="autoruns-comment-modal">
|
||||||
|
<h3>Удалить сохраненный набор</h3>
|
||||||
|
<p className="muted">Будет удалена карточка истории и связанный файл кейс-сета на бэке.</p>
|
||||||
|
<p className="autoruns-comment-quote">{autoGenDeleteModal.title}</p>
|
||||||
|
|
||||||
|
{autoGenDeleteModal.error ? <p className="error-text">{autoGenDeleteModal.error}</p> : null}
|
||||||
|
|
||||||
|
<div className="button-row">
|
||||||
|
<button type="button" onClick={() => void submitAutoGenDeleteModal()} disabled={autoGenDeleteModal.saving}>
|
||||||
|
{autoGenDeleteModal.saving ? "Удаляю..." : "Да"}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="tab" onClick={() => closeAutoGenDeleteModal()} disabled={autoGenDeleteModal.saving}>
|
||||||
|
Нет
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{assistantLiveCommentModal.open ? (
|
{assistantLiveCommentModal.open ? (
|
||||||
<div
|
<div
|
||||||
className="autoruns-comment-modal-backdrop"
|
className="autoruns-comment-modal-backdrop"
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ export interface RuntimeRun {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UiMode = "assistant" | "decomposition" | "autoruns";
|
export type UiMode = "decomposition" | "autoruns";
|
||||||
|
|
||||||
export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
|
export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
|
||||||
export type AutoRunTrend = "up" | "down" | "flat";
|
export type AutoRunTrend = "up" | "down" | "flat";
|
||||||
export type AutoGenMode = "qwen_seed" | "codex_creative";
|
export type AutoGenMode = "qwen_seed" | "codex_creative" | "saved_user_sessions";
|
||||||
export type ManualCaseDecision =
|
export type ManualCaseDecision =
|
||||||
| "covered_ok"
|
| "covered_ok"
|
||||||
| "covered_but_bad_answer"
|
| "covered_but_bad_answer"
|
||||||
@@ -317,6 +317,7 @@ export interface AutoGenHistoryRecord {
|
|||||||
generation_id: string;
|
generation_id: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
mode: AutoGenMode;
|
mode: AutoGenMode;
|
||||||
|
title: string | null;
|
||||||
count: number;
|
count: number;
|
||||||
domain: string | null;
|
domain: string | null;
|
||||||
questions: string[];
|
questions: string[];
|
||||||
@@ -330,6 +331,9 @@ export interface AutoGenHistoryRecord {
|
|||||||
prompt_fingerprint: string | null;
|
prompt_fingerprint: string | null;
|
||||||
autogen_personality_id: string | null;
|
autogen_personality_id: string | null;
|
||||||
autogen_personality_prompt: string | null;
|
autogen_personality_prompt: string | null;
|
||||||
|
source_session_id?: string | null;
|
||||||
|
saved_session_file?: string | null;
|
||||||
|
saved_case_set_kind?: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1566,14 +1566,13 @@ button:disabled {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transform: rotate(45deg);
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoruns-remove-question-btn:hover {
|
.autoruns-remove-question-btn:hover {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: rgb(var(--rgb-active-text));
|
color: rgb(var(--rgb-active));
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1772,6 +1771,37 @@ button:disabled {
|
|||||||
background: rgb(var(--rgb-active));
|
background: rgb(var(--rgb-active));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.autoruns-autogen-card-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoruns-autogen-delete-btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
min-width: 22px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: rgb(var(--rgb-text-main));
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoruns-autogen-delete-btn:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: rgb(var(--rgb-active));
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
:root {
|
:root {
|
||||||
--mode-column-width: 400px;
|
--mode-column-width: 400px;
|
||||||
|
|||||||
Reference in New Issue
Block a user