ЮИ - Добавить удаление сохраненных наборов автопрогонов с удалением файлов на бэке
This commit is contained in:
parent
f3255cb3b8
commit
a3a61b3a0f
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
? writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, scenarioQuestions, scenarioTitle ?? undefined)
|
||||||
|
: questions.length > 0
|
||||||
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
||||||
: payload.caseSetFile
|
: payload.caseSetFile
|
||||||
? payload.caseSetFile
|
? payload.caseSetFile
|
||||||
: undefined;
|
: 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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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,99 +655,17 @@ 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">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAssistantConnectionMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAssistantConnectionMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
LLM Connector
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAssistantPromptMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAssistantPromptMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Prompt Manager
|
|
||||||
</button>
|
|
||||||
<button type="button" className={showAssistantChatMode ? "tab active" : "tab"} onClick={() => setShowAssistantChatMode((prev) => !prev)}>
|
|
||||||
Режим ассистента
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAssistantCommentsMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAssistantCommentsMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Комментарии ассистента
|
|
||||||
</button>
|
|
||||||
<button type="button" className={showAssistantSamMode ? "tab active" : "tab"} onClick={() => setShowAssistantSamMode((prev) => !prev)}>
|
|
||||||
SAM
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : uiMode === "decomposition" ? (
|
|
||||||
<div className="mode-switch-row mode-switch-row-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showDecompositionConnectionMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowDecompositionConnectionMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
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">
|
<div className="mode-switch-row mode-switch-row-right">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1068,13 +688,6 @@ export default function App() {
|
||||||
>
|
>
|
||||||
Режим ассистента
|
Режим ассистента
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={showAutorunsDecompositionMode ? "tab active" : "tab"}
|
|
||||||
onClick={() => setShowAutorunsDecompositionMode((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Режим декомпозиции
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={showAutorunsProgressMode ? "tab active" : "tab"}
|
className={showAutorunsProgressMode ? "tab active" : "tab"}
|
||||||
|
|
@ -1090,238 +703,8 @@ export default function App() {
|
||||||
Комментарии
|
Комментарии
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{uiMode === "assistant" ? (
|
|
||||||
<div className="layout-grid layout-grid-mode-columns">
|
|
||||||
<div className="mode-columns">
|
|
||||||
{showAssistantConnectionMode ? (
|
|
||||||
<div className="mode-col">
|
|
||||||
<ConnectionPanel
|
|
||||||
value={connection}
|
|
||||||
modelOptions={modelOptions}
|
|
||||||
modelsBusy={modelsBusy}
|
|
||||||
onChange={setConnection}
|
|
||||||
onReloadModels={reloadModels}
|
|
||||||
onSaveLocalConfig={saveLocalConfig}
|
|
||||||
onTestConnection={testConnection}
|
|
||||||
lastStatus={connectionStatus}
|
|
||||||
busy={busy || assistantBusy}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showAssistantPromptMode ? (
|
|
||||||
<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}
|
|
||||||
|
|
||||||
{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">
|
<div className="layout-grid layout-grid-autoruns">
|
||||||
<AutoRunsHistoryPanel
|
<AutoRunsHistoryPanel
|
||||||
connection={connection}
|
connection={connection}
|
||||||
|
|
@ -1350,96 +733,11 @@ export default function App() {
|
||||||
showSettingsMode={showAutorunsSettingsMode}
|
showSettingsMode={showAutorunsSettingsMode}
|
||||||
showAutoRunsMode={showAutorunsAutoRunsMode}
|
showAutoRunsMode={showAutorunsAutoRunsMode}
|
||||||
showAssistantMode={showAutorunsAssistantMode}
|
showAssistantMode={showAutorunsAssistantMode}
|
||||||
showDecompositionMode={showAutorunsDecompositionMode}
|
|
||||||
showProgressMode={showAutorunsProgressMode}
|
showProgressMode={showAutorunsProgressMode}
|
||||||
showCommentsMode={showAutorunsCommentsMode}
|
showCommentsMode={showAutorunsCommentsMode}
|
||||||
onLog={log}
|
onLog={log}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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,18 +2227,21 @@ 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>
|
||||||
|
{!isSavedUserSessionsMode ? (
|
||||||
|
<>
|
||||||
<label>
|
<label>
|
||||||
Кол-во
|
Кол-во
|
||||||
<input
|
<input
|
||||||
|
|
@ -2036,8 +2317,11 @@ export function AutoRunsHistoryPanel({
|
||||||
/>
|
/>
|
||||||
Сохранять кейс-сет в `eval_cases`
|
Сохранять кейс-сет в `eval_cases`
|
||||||
</label>
|
</label>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isSavedUserSessionsMode ? (
|
||||||
<div className="autoruns-form-grid">
|
<div className="autoruns-form-grid">
|
||||||
<label>
|
<label>
|
||||||
Дата анализа (срез)
|
Дата анализа (срез)
|
||||||
|
|
@ -2053,36 +2337,45 @@ export function AutoRunsHistoryPanel({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
|
{!isSavedUserSessionsMode ? (
|
||||||
|
<>
|
||||||
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
||||||
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
||||||
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
||||||
</button>
|
</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}
|
||||||
|
</div>
|
||||||
|
{item.domain || item.generated_by ? (
|
||||||
|
<div className="autoruns-run-meta">
|
||||||
|
{item.domain ? `домен=${item.domain}` : "домен=общий"}
|
||||||
{item.generated_by ? ` | автор=${item.generated_by}` : ""}
|
{item.generated_by ? ` | автор=${item.generated_by}` : ""}
|
||||||
</div>
|
</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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue