ЮИ - Добавить удаление сохраненных наборов автопрогонов с удалением файлов на бэке
This commit is contained in:
parent
f3255cb3b8
commit
a3a61b3a0f
|
|
@ -93,6 +93,16 @@ function clampInt(value, min, max, fallback) {
|
|||
return max;
|
||||
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") {
|
||||
const normalized = toStringSafe(value);
|
||||
if (!normalized)
|
||||
|
|
@ -151,15 +161,11 @@ function readAutoGenHistory() {
|
|||
.map((item) => ({
|
||||
generation_id: toStringSafe(item.generation_id) ?? "",
|
||||
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),
|
||||
domain: toStringSafe(item.domain),
|
||||
questions: toArray(item.questions)
|
||||
.map((q) => toStringSafe(q))
|
||||
.filter((q) => q !== null)
|
||||
.map((q) => sanitizeGeneratedQuestion(q))
|
||||
.filter((q) => q.length > 0)
|
||||
.slice(0, 500),
|
||||
questions: parseAssistantSessionQuestions(item.questions),
|
||||
generated_by: toStringSafe(item.generated_by),
|
||||
saved_case_set_file: toStringSafe(item.saved_case_set_file),
|
||||
context: toRecord(item.context)
|
||||
|
|
@ -174,7 +180,10 @@ function readAutoGenHistory() {
|
|||
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
|
||||
autogen_personality_prompt: toStringSafe(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
|
||||
}))
|
||||
|
|
@ -1057,7 +1066,7 @@ function parseDecisionFilter(value) {
|
|||
}
|
||||
function parseAutoGenMode(value) {
|
||||
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 "codex_creative";
|
||||
|
|
@ -1150,6 +1159,12 @@ function sanitizeGeneratedQuestion(value) {
|
|||
.replace(/\s+/g, " ")
|
||||
.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_TAIL_PATTERNS = [
|
||||
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
|
||||
|
|
@ -1413,6 +1428,18 @@ function buildAutogenCaseSetFileName(mode, generationId) {
|
|||
].join("");
|
||||
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) {
|
||||
const normalizedQuestions = Array.from(new Set(input.questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0)));
|
||||
const cases = normalizedQuestions.map((question, index) => ({
|
||||
|
|
@ -1439,6 +1466,99 @@ function buildAutogenCaseSetPayload(input) {
|
|||
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) {
|
||||
const byDecision = {};
|
||||
const byQueue = {};
|
||||
|
|
@ -1522,7 +1642,7 @@ function collectPostAnalysis(annotations, runMap, limitPerQueue) {
|
|||
].slice(0, 60)
|
||||
};
|
||||
}
|
||||
function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIResponsesClient()) {
|
||||
function buildAutoRunsRouter(services, openaiClient = new openaiResponsesClient_1.OpenAIResponsesClient()) {
|
||||
const router = (0, express_1.Router)();
|
||||
router.get("/api/autoruns/history", (req, res) => {
|
||||
const filters = parseFilters(req.query);
|
||||
|
|
@ -1884,7 +2004,7 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||
try {
|
||||
const limit = clampInt(toNumberSafe(req.query.limit), 1, 500, 120);
|
||||
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 items = readAutoGenHistory()
|
||||
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
|
||||
|
|
@ -1911,6 +2031,157 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||
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) => {
|
||||
try {
|
||||
const body = toRecord(req.body);
|
||||
|
|
@ -1925,6 +2196,9 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||
const context = toRecord(body.context);
|
||||
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
||||
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 = [];
|
||||
if (mode === "qwen_seed") {
|
||||
if (!llmConfig) {
|
||||
|
|
@ -1963,6 +2237,7 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||
generation_id: generationId,
|
||||
created_at: new Date().toISOString(),
|
||||
mode,
|
||||
title: null,
|
||||
count: questions.length,
|
||||
domain,
|
||||
questions,
|
||||
|
|
@ -1980,7 +2255,10 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
|
|||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
||||
autogen_personality_prompt: toStringSafe(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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -128,14 +128,23 @@ function splitQuestionCandidate(raw) {
|
|||
}
|
||||
return normalizeRuntimeQuestionList(chunks);
|
||||
}
|
||||
function normalizeRuntimeQuestions(value) {
|
||||
function normalizeRuntimeQuestions(value, options) {
|
||||
const raw = toArray(value)
|
||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
||||
.filter((item) => item.length > 0);
|
||||
if (raw.length === 0) {
|
||||
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 seen = new Set();
|
||||
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");
|
||||
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) {
|
||||
const sessionId = `${runId}-${caseId}`;
|
||||
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);
|
||||
}
|
||||
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 runId = `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
|
||||
const runtimeCaseSetFile = questions.length > 0
|
||||
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
||||
: payload.caseSetFile
|
||||
? payload.caseSetFile
|
||||
: undefined;
|
||||
const runtimeCaseSetFile = scenarioQuestions.length > 0
|
||||
? writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, scenarioQuestions, scenarioTitle ?? undefined)
|
||||
: questions.length > 0
|
||||
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
||||
: payload.caseSetFile
|
||||
? payload.caseSetFile
|
||||
: undefined;
|
||||
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);
|
||||
if (caseSeeds.length === 0) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function createApp() {
|
|||
app.use((0, normalize_1.buildNormalizeRouter)(services));
|
||||
app.use((0, eval_1.buildEvalRouter)(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, presets_1.buildPresetsRouter)());
|
||||
app.use((0, accountingAgent_1.buildAccountingAgentRouter)(services));
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ import {
|
|||
MANUAL_CASE_DECISION_SCHEMA_FILE,
|
||||
REPORTS_DIR
|
||||
} from "../config";
|
||||
import type { AppServices } from "../serverContext";
|
||||
import { ApiError, ok } from "../utils/http";
|
||||
import { loadCapabilitiesRegistry, resolveNearestCapabilityGroup, type CapabilityGroup } from "../services/capabilitiesRegistry";
|
||||
import { OpenAIResponsesClient } from "../services/openaiResponsesClient";
|
||||
|
||||
type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
|
||||
type AutoRunTrend = "up" | "down" | "flat";
|
||||
type AutoGenMode = "qwen_seed" | "codex_creative";
|
||||
type AutoGenMode = "qwen_seed" | "codex_creative" | "saved_user_sessions";
|
||||
type ManualCaseDecision =
|
||||
| "covered_ok"
|
||||
| "covered_but_bad_answer"
|
||||
|
|
@ -175,6 +176,7 @@ interface AutoGenHistoryRecord {
|
|||
generation_id: string;
|
||||
created_at: string;
|
||||
mode: AutoGenMode;
|
||||
title: string | null;
|
||||
count: number;
|
||||
domain: string | null;
|
||||
questions: string[];
|
||||
|
|
@ -188,6 +190,9 @@ interface AutoGenHistoryRecord {
|
|||
prompt_fingerprint: string | null;
|
||||
autogen_personality_id: string | null;
|
||||
autogen_personality_prompt: string | null;
|
||||
source_session_id?: string | null;
|
||||
saved_session_file?: string | null;
|
||||
saved_case_set_kind?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
|
@ -269,6 +274,18 @@ function clampInt(value: number | null, min: number, max: number, fallback: numb
|
|||
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 {
|
||||
const normalized = toStringSafe(value);
|
||||
if (!normalized) return fallback;
|
||||
|
|
@ -326,15 +343,11 @@ function readAutoGenHistory(): AutoGenHistoryRecord[] {
|
|||
.map((item) => ({
|
||||
generation_id: toStringSafe(item.generation_id) ?? "",
|
||||
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),
|
||||
domain: toStringSafe(item.domain),
|
||||
questions: toArray(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),
|
||||
questions: parseAssistantSessionQuestions(item.questions),
|
||||
generated_by: toStringSafe(item.generated_by),
|
||||
saved_case_set_file: toStringSafe(item.saved_case_set_file),
|
||||
context: toRecord(item.context)
|
||||
|
|
@ -349,7 +362,10 @@ function readAutoGenHistory(): AutoGenHistoryRecord[] {
|
|||
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
|
||||
autogen_personality_prompt: toStringSafe(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
|
||||
}))
|
||||
|
|
@ -1314,7 +1330,7 @@ function parseDecisionFilter(value: unknown): ManualCaseDecision | "all" {
|
|||
|
||||
function parseAutoGenMode(value: unknown): AutoGenMode {
|
||||
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 "codex_creative";
|
||||
|
|
@ -1416,6 +1432,13 @@ function sanitizeGeneratedQuestion(value: string): string {
|
|||
.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_TAIL_PATTERNS: RegExp[] = [
|
||||
/^(?:без\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`;
|
||||
}
|
||||
|
||||
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: {
|
||||
generationId: string;
|
||||
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(
|
||||
annotations: AutoRunAnnotationRecord[],
|
||||
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();
|
||||
|
||||
router.get("/api/autoruns/history", (req, res) => {
|
||||
|
|
@ -2251,7 +2399,7 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||
try {
|
||||
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 includeAllModes = !rawMode || !["qwen_seed", "codex_creative"].includes(rawMode);
|
||||
const includeAllModes = !rawMode || !isAutoGenMode(rawMode);
|
||||
const modeFilter = (rawMode as AutoGenMode | null) ?? "codex_creative";
|
||||
const items = readAutoGenHistory()
|
||||
.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) => {
|
||||
try {
|
||||
const body = toRecord(req.body);
|
||||
|
|
@ -2294,6 +2618,14 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
||||
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[] = [];
|
||||
if (mode === "qwen_seed") {
|
||||
if (!llmConfig) {
|
||||
|
|
@ -2340,6 +2672,7 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||
generation_id: generationId,
|
||||
created_at: new Date().toISOString(),
|
||||
mode,
|
||||
title: null,
|
||||
count: questions.length,
|
||||
domain,
|
||||
questions,
|
||||
|
|
@ -2357,7 +2690,10 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
|
|||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
||||
autogen_personality_prompt: toStringSafe(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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ function splitQuestionCandidate(raw: string): string[] {
|
|||
return normalizeRuntimeQuestionList(chunks);
|
||||
}
|
||||
|
||||
function normalizeRuntimeQuestions(value: unknown): string[] {
|
||||
function normalizeRuntimeQuestions(value: unknown, options?: { dedupe?: boolean; splitCandidates?: boolean }): string[] {
|
||||
const raw = toArray(value)
|
||||
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
||||
.filter((item) => item.length > 0);
|
||||
|
|
@ -184,7 +184,16 @@ function normalizeRuntimeQuestions(value: unknown): string[] {
|
|||
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 seen = new Set<string>();
|
||||
for (const item of expanded) {
|
||||
|
|
@ -342,6 +351,39 @@ function writeRuntimeAssistantSuiteFromQuestions(jobId: string, questions: strin
|
|||
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"] {
|
||||
const sessionId = `${runId}-${caseId}`;
|
||||
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);
|
||||
}
|
||||
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 runId = `assistant-stage1-${nanoid(10)}`;
|
||||
const runtimeCaseSetFile =
|
||||
questions.length > 0
|
||||
scenarioQuestions.length > 0
|
||||
? writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, scenarioQuestions, scenarioTitle ?? undefined)
|
||||
: questions.length > 0
|
||||
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
|
||||
: payload.caseSetFile
|
||||
? payload.caseSetFile
|
||||
|
|
@ -502,7 +548,7 @@ export function buildEvalRouter(services: AppServices): Router {
|
|||
if (!runtimeCaseSetFile) {
|
||||
throw new ApiError(
|
||||
"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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export function createApp(): express.Express {
|
|||
app.use(buildNormalizeRouter(services));
|
||||
app.use(buildEvalRouter(services));
|
||||
app.use(buildAssistantRouter(services));
|
||||
app.use(buildAutoRunsRouter(openaiClient));
|
||||
app.use(buildAutoRunsRouter(services, openaiClient));
|
||||
app.use(buildHistoryRouter());
|
||||
app.use(buildPresetsRouter());
|
||||
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",
|
||||
"updated_at": "2026-04-15T06:12:46.714Z",
|
||||
"updated_at": "2026-04-16T17:54:57.636Z",
|
||||
"connection": {
|
||||
"llmProvider": "local",
|
||||
"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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NDC AI Normalizer Playground</title>
|
||||
<script type="module" crossorigin src="/assets/index-VJV2AL7G.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CaUiKcE3.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bw40I8e3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DkWsdP2H.css">
|
||||
</head>
|
||||
<body>
|
||||
<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 { AssistantSamPanel } from "./components/AssistantSamPanel";
|
||||
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
|
||||
import { AssistantPanel } from "./components/AssistantPanel";
|
||||
import { ConnectionPanel } from "./components/ConnectionPanel";
|
||||
import { HistoryPanel } from "./components/HistoryPanel";
|
||||
import { MetricsPanel } from "./components/MetricsPanel";
|
||||
import { OutputPanel } from "./components/OutputPanel";
|
||||
import { PanelFrame } from "./components/PanelFrame";
|
||||
import { PromptPanel } from "./components/PromptPanel";
|
||||
import { QueryPanel } from "./components/QueryPanel";
|
||||
import { RuntimePanel } from "./components/RuntimePanel";
|
||||
import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults";
|
||||
import { designConfig } from "../../../designconfig";
|
||||
import type {
|
||||
AssistantConversationItem,
|
||||
AssistantAnnotationRecord,
|
||||
AssistantSelectionChip,
|
||||
ConnectionState,
|
||||
HistoryItem,
|
||||
NormalizeResultState,
|
||||
|
|
@ -30,22 +24,10 @@ import type {
|
|||
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
|
||||
const AUTORUNS_LAYOUT_CONFIG_KEY = "ndc_autoruns_layout_config_v1";
|
||||
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
|
||||
const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
|
||||
const DEFAULT_UI_MODE: UiMode = "autoruns";
|
||||
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
||||
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
||||
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 {
|
||||
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(" | ")}`;
|
||||
}
|
||||
|
||||
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() {
|
||||
const [connection, setConnection] = useState<ConnectionState>(DEFAULT_CONNECTION);
|
||||
const [prompts, setPrompts] = useState<PromptState>(DEFAULT_PROMPTS);
|
||||
|
|
@ -123,11 +86,6 @@ export default function App() {
|
|||
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
|
||||
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = 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 [showDecompositionPromptMode, setShowDecompositionPromptMode] = useState(true);
|
||||
const [showDecompositionQueryMode, setShowDecompositionQueryMode] = useState(true);
|
||||
|
|
@ -135,24 +93,6 @@ export default function App() {
|
|||
const [showDecompositionMetricsMode, setShowDecompositionMetricsMode] = useState(true);
|
||||
const [showDecompositionHistoryMode, setShowDecompositionHistoryMode] = 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 skipPresetAutoloadRef = useRef(false);
|
||||
const sharedConnectionSyncReadyRef = useRef(false);
|
||||
|
|
@ -184,16 +124,6 @@ export default function App() {
|
|||
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(() => {
|
||||
const bootstrapSharedConnection = async () => {
|
||||
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
|
||||
|
|
@ -239,7 +169,7 @@ export default function App() {
|
|||
if (cachedAutorunsLayout) {
|
||||
try {
|
||||
const parsed = JSON.parse(cachedAutorunsLayout) as {
|
||||
uiMode?: UiMode;
|
||||
uiMode?: UiMode | "assistant";
|
||||
activeTab?: TabKey;
|
||||
showAutorunsSettingsMode?: boolean;
|
||||
showAutorunsAutoRunsMode?: boolean;
|
||||
|
|
@ -247,11 +177,6 @@ export default function App() {
|
|||
showAutorunsDecompositionMode?: boolean;
|
||||
showAutorunsProgressMode?: boolean;
|
||||
showAutorunsCommentsMode?: boolean;
|
||||
showAssistantConnectionMode?: boolean;
|
||||
showAssistantPromptMode?: boolean;
|
||||
showAssistantChatMode?: boolean;
|
||||
showAssistantCommentsMode?: boolean;
|
||||
showAssistantSamMode?: boolean;
|
||||
showDecompositionConnectionMode?: boolean;
|
||||
showDecompositionPromptMode?: boolean;
|
||||
showDecompositionQueryMode?: boolean;
|
||||
|
|
@ -261,9 +186,7 @@ export default function App() {
|
|||
showDecompositionRuntimeMode?: boolean;
|
||||
prompts?: PromptState;
|
||||
};
|
||||
if (parsed.uiMode === "decomposition") {
|
||||
setUiMode("decomposition");
|
||||
} else if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns") {
|
||||
if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns" || parsed.uiMode === "decomposition") {
|
||||
setUiMode("autoruns");
|
||||
}
|
||||
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
|
||||
|
|
@ -287,21 +210,6 @@ export default function App() {
|
|||
if (typeof parsed.showAutorunsCommentsMode === "boolean") {
|
||||
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") {
|
||||
setShowDecompositionConnectionMode(parsed.showDecompositionConnectionMode);
|
||||
}
|
||||
|
|
@ -448,11 +356,6 @@ export default function App() {
|
|||
showAutorunsDecompositionMode,
|
||||
showAutorunsProgressMode,
|
||||
showAutorunsCommentsMode,
|
||||
showAssistantConnectionMode,
|
||||
showAssistantPromptMode,
|
||||
showAssistantChatMode,
|
||||
showAssistantCommentsMode,
|
||||
showAssistantSamMode,
|
||||
showDecompositionConnectionMode,
|
||||
showDecompositionPromptMode,
|
||||
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(() => {
|
||||
if (!selectedRunId) {
|
||||
setRunTrace([]);
|
||||
|
|
@ -953,493 +655,89 @@ export default function App() {
|
|||
}, [selectedRunId]);
|
||||
|
||||
return (
|
||||
<main
|
||||
className={`app-root ${
|
||||
uiMode === "assistant" || uiMode === "decomposition" || uiMode === "autoruns" ? "app-root-autoruns" : ""
|
||||
}`}
|
||||
>
|
||||
<main className="app-root app-root-autoruns">
|
||||
<header className="app-topbar">
|
||||
<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 type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
|
||||
Декомпозиция
|
||||
</button>
|
||||
<button type="button" className="tab" onClick={saveAutorunsLayout}>
|
||||
Сохранить
|
||||
</button>
|
||||
</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">
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsSettingsMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
|
||||
>
|
||||
Настройки
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
|
||||
>
|
||||
Автопрогоны
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsAssistantMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
|
||||
>
|
||||
Режим ассистента
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsDecompositionMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsDecompositionMode((prev) => !prev)}
|
||||
>
|
||||
Режим декомпозиции
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsProgressMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
|
||||
>
|
||||
Прогресс/регресс
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsCommentsMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
|
||||
>
|
||||
Комментарии
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mode-switch-row mode-switch-row-right">
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsSettingsMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
|
||||
>
|
||||
Настройки
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
|
||||
>
|
||||
Автопрогоны
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsAssistantMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
|
||||
>
|
||||
Режим ассистента
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsProgressMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
|
||||
>
|
||||
Прогресс/регресс
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAutorunsCommentsMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
|
||||
>
|
||||
Комментарии
|
||||
</button>
|
||||
</div>
|
||||
</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">
|
||||
<AutoRunsHistoryPanel
|
||||
connection={connection}
|
||||
modelOptions={modelOptions}
|
||||
modelsBusy={modelsBusy}
|
||||
connectionStatus={connectionStatus}
|
||||
connectionBusy={busy}
|
||||
onConnectionChange={setConnection}
|
||||
onReloadModels={reloadModels}
|
||||
onSaveLocalConfig={saveLocalConfig}
|
||||
onTestConnection={testConnection}
|
||||
prompts={prompts}
|
||||
onPromptsChange={setPrompts}
|
||||
promptPresets={presetList}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onSelectPreset={setSelectedPresetId}
|
||||
onLoadPreset={loadSelectedPreset}
|
||||
onSavePreset={savePreset}
|
||||
onResetDefaults={resetDefaults}
|
||||
onDiffPrevious={diffWithPrevious}
|
||||
presetName={presetName}
|
||||
onPresetNameChange={setPresetName}
|
||||
diffSummary={diffSummary}
|
||||
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
||||
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
||||
showSettingsMode={showAutorunsSettingsMode}
|
||||
showAutoRunsMode={showAutorunsAutoRunsMode}
|
||||
showAssistantMode={showAutorunsAssistantMode}
|
||||
showDecompositionMode={showAutorunsDecompositionMode}
|
||||
showProgressMode={showAutorunsProgressMode}
|
||||
showCommentsMode={showAutorunsCommentsMode}
|
||||
onLog={log}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{assistantCommentModal.open ? (
|
||||
<div
|
||||
className="autoruns-comment-modal-backdrop"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeAssistantCommentModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="autoruns-comment-modal">
|
||||
<h3>Комментарий к ответу ассистента</h3>
|
||||
<p className="muted">Эта разметка хранится отдельно от комментариев автопрогонов.</p>
|
||||
|
||||
{assistantCommentModalQuestion ? (
|
||||
<details className="autoruns-prompt-details" open>
|
||||
<summary>Вопрос пользователя</summary>
|
||||
<p className="autoruns-comment-quote">{assistantCommentModalQuestion.text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
{assistantCommentModalMessage ? (
|
||||
<details className="autoruns-prompt-details" open>
|
||||
<summary>Ответ ассистента</summary>
|
||||
<p className="autoruns-comment-quote">{assistantCommentModalMessage.text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
<div className="autoruns-rating-row" role="group" aria-label="Рейтинг ответа">
|
||||
{[1, 2, 3, 4, 5].map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
className={assistantCommentModal.rating >= value ? "autoruns-rating-dot active" : "autoruns-rating-dot"}
|
||||
onClick={() => setAssistantCommentModal((prev) => ({ ...prev, rating: value }))}
|
||||
disabled={assistantCommentModal.saving}
|
||||
aria-label={`Оценка ${value}`}
|
||||
>
|
||||
{assistantCommentModal.rating >= value ? "●" : "○"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Автор комментария
|
||||
<input
|
||||
value={assistantCommentModal.annotationAuthor}
|
||||
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, annotationAuthor: event.target.value }))}
|
||||
placeholder="manual_reviewer"
|
||||
disabled={assistantCommentModal.saving}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Комментарий
|
||||
<textarea
|
||||
value={assistantCommentModal.comment}
|
||||
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, comment: event.target.value }))}
|
||||
placeholder="Что именно не так в ответе и что проверить."
|
||||
rows={4}
|
||||
disabled={assistantCommentModal.saving}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{assistantCommentModal.error ? <p className="error-text">{assistantCommentModal.error}</p> : null}
|
||||
|
||||
<div className="button-row">
|
||||
<button type="button" onClick={() => void submitAssistantCommentModal()} disabled={assistantCommentModal.saving}>
|
||||
{assistantCommentModal.saving ? "Сохраняю..." : "Готово"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tab"
|
||||
onClick={() => closeAssistantCommentModal()}
|
||||
disabled={assistantCommentModal.saving}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="layout-grid layout-grid-autoruns">
|
||||
<AutoRunsHistoryPanel
|
||||
connection={connection}
|
||||
modelOptions={modelOptions}
|
||||
modelsBusy={modelsBusy}
|
||||
connectionStatus={connectionStatus}
|
||||
connectionBusy={busy}
|
||||
onConnectionChange={setConnection}
|
||||
onReloadModels={reloadModels}
|
||||
onSaveLocalConfig={saveLocalConfig}
|
||||
onTestConnection={testConnection}
|
||||
prompts={prompts}
|
||||
onPromptsChange={setPrompts}
|
||||
promptPresets={presetList}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onSelectPreset={setSelectedPresetId}
|
||||
onLoadPreset={loadSelectedPreset}
|
||||
onSavePreset={savePreset}
|
||||
onResetDefaults={resetDefaults}
|
||||
onDiffPrevious={diffWithPrevious}
|
||||
presetName={presetName}
|
||||
onPresetNameChange={setPresetName}
|
||||
diffSummary={diffSummary}
|
||||
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
||||
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
||||
showSettingsMode={showAutorunsSettingsMode}
|
||||
showAutoRunsMode={showAutorunsAutoRunsMode}
|
||||
showAssistantMode={showAutorunsAssistantMode}
|
||||
showProgressMode={showAutorunsProgressMode}
|
||||
showCommentsMode={showAutorunsCommentsMode}
|
||||
onLog={log}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
AsyncEvalRunStatusResponse,
|
||||
AutoGenPersonalityCatalogResponse,
|
||||
AutoGenHistoryResponse,
|
||||
AutoGenHistoryRecord,
|
||||
AutoGenMode,
|
||||
AutoRunAnnotationsResponse,
|
||||
AutoRunAnnotationRecord,
|
||||
|
|
@ -230,6 +231,8 @@ export const apiClient = {
|
|||
evalTarget?: "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0";
|
||||
compareWithReportFile?: string;
|
||||
questions?: string[];
|
||||
scenarioQuestions?: string[];
|
||||
scenarioTitle?: string;
|
||||
analysisDate?: string;
|
||||
}): Promise<AsyncEvalRunStartResponse> {
|
||||
return request("/eval/run-async/start", {
|
||||
|
|
@ -256,6 +259,8 @@ export const apiClient = {
|
|||
eval_target: input.evalTarget,
|
||||
compare_with_report_file: input.compareWithReportFile,
|
||||
questions: input.questions,
|
||||
scenarioQuestions: input.scenarioQuestions,
|
||||
scenarioTitle: input.scenarioTitle,
|
||||
analysis_date: input.analysisDate
|
||||
})
|
||||
});
|
||||
|
|
@ -342,6 +347,24 @@ export const apiClient = {
|
|||
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?: {
|
||||
session_id?: string;
|
||||
limit?: number;
|
||||
|
|
@ -485,6 +508,28 @@ export const apiClient = {
|
|||
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: {
|
||||
mode: AutoGenMode;
|
||||
count: number;
|
||||
|
|
@ -508,7 +553,7 @@ export const apiClient = {
|
|||
autogen_personality_id?: 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", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input)
|
||||
|
|
|
|||
|
|
@ -16,9 +16,13 @@ interface AssistantPanelProps {
|
|||
onUseMockChange: (value: boolean) => void;
|
||||
onSend: () => Promise<void> | void;
|
||||
onClear: () => void;
|
||||
onSaveSession?: () => void;
|
||||
busy: boolean;
|
||||
saveBusy?: boolean;
|
||||
saveDisabled?: boolean;
|
||||
statusText: string;
|
||||
errorMessage: string;
|
||||
showSaveAction?: boolean;
|
||||
showCommentAction?: boolean;
|
||||
onCommentAssistantMessage?: (item: AssistantConversationItem, index: number) => void;
|
||||
isAssistantMessageCommented?: (item: AssistantConversationItem, index: number) => boolean;
|
||||
|
|
@ -295,9 +299,13 @@ export function AssistantPanel({
|
|||
onUseMockChange,
|
||||
onSend,
|
||||
onClear,
|
||||
onSaveSession,
|
||||
busy,
|
||||
saveBusy = false,
|
||||
saveDisabled = false,
|
||||
statusText,
|
||||
errorMessage,
|
||||
showSaveAction = false,
|
||||
showCommentAction = false,
|
||||
onCommentAssistantMessage,
|
||||
isAssistantMessageCommented,
|
||||
|
|
@ -383,6 +391,16 @@ export function AssistantPanel({
|
|||
>
|
||||
Скопировать техчат
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ interface AutoRunsHistoryPanelProps {
|
|||
showSettingsMode: boolean;
|
||||
showAutoRunsMode: boolean;
|
||||
showAssistantMode: boolean;
|
||||
showDecompositionMode: boolean;
|
||||
showProgressMode: boolean;
|
||||
showCommentsMode: boolean;
|
||||
onLog?: (message: string) => void;
|
||||
|
|
@ -112,6 +111,30 @@ interface AssistantLiveCommentModalState {
|
|||
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 {
|
||||
mode: AutoGenMode;
|
||||
count: number;
|
||||
|
|
@ -259,6 +282,19 @@ function formatDateTime(iso: string | null): string {
|
|||
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 {
|
||||
if (total <= 0) return 0;
|
||||
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 "Без изменений";
|
||||
}
|
||||
|
||||
function getSelectedCase(cases: AutoRunCaseSummary[], caseId: string): AutoRunCaseSummary | null {
|
||||
return cases.find((item) => item.case_id === caseId) ?? null;
|
||||
}
|
||||
|
||||
function renderRatingDots(rating: number): string {
|
||||
const safe = Math.max(1, Math.min(5, Math.round(rating)));
|
||||
return `${"●".repeat(safe)}${"○".repeat(5 - safe)}`;
|
||||
|
|
@ -527,7 +559,6 @@ export function AutoRunsHistoryPanel({
|
|||
showSettingsMode,
|
||||
showAutoRunsMode,
|
||||
showAssistantMode,
|
||||
showDecompositionMode,
|
||||
showProgressMode,
|
||||
showCommentsMode,
|
||||
onLog
|
||||
|
|
@ -598,19 +629,44 @@ export function AutoRunsHistoryPanel({
|
|||
saving: false,
|
||||
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 asyncJobPollTimerRef = useRef<number | null>(null);
|
||||
const isSavedUserSessionsMode = autoGenSettings.mode === "saved_user_sessions";
|
||||
const selectedPersonality = useMemo(
|
||||
() => autogenPersonalities.find((item) => item.id === autoGenSettings.personalityId) ?? autogenPersonalities[0] ?? AUTOGEN_PERSONALITIES[0],
|
||||
[autoGenSettings.personalityId, autogenPersonalities]
|
||||
);
|
||||
const visibleAutoGenHistory = useMemo(
|
||||
() => autoGenHistory.filter((item) => item.mode === autoGenSettings.mode),
|
||||
[autoGenHistory, autoGenSettings.mode]
|
||||
);
|
||||
const selectedAutogenGeneration = useMemo(
|
||||
() => autoGenHistory.find((item) => item.generation_id === selectedAutogenGenerationId) ?? autoGenHistory[0] ?? null,
|
||||
[autoGenHistory, selectedAutogenGenerationId]
|
||||
() => visibleAutoGenHistory.find((item) => item.generation_id === selectedAutogenGenerationId) ?? visibleAutoGenHistory[0] ?? null,
|
||||
[selectedAutogenGenerationId, visibleAutoGenHistory]
|
||||
);
|
||||
|
||||
const activeCase = runDetail ? getSelectedCase(runDetail.cases, selectedCaseId) : null;
|
||||
const visibleAnnotations = useMemo(
|
||||
() => (hideResolvedAnnotations ? annotations.filter((item) => !item.resolved) : annotations),
|
||||
[annotations, hideResolvedAnnotations]
|
||||
|
|
@ -731,11 +787,56 @@ export function AutoRunsHistoryPanel({
|
|||
});
|
||||
}, []);
|
||||
|
||||
const copyRunIdToClipboard = useCallback(
|
||||
async (event: React.SyntheticEvent, runId: string) => {
|
||||
const closeAssistantLiveSaveModal = useCallback((options?: { force?: boolean }) => {
|
||||
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.preventDefault();
|
||||
const value = String(runId ?? "").trim();
|
||||
const value = String(valueRaw ?? "").trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -753,11 +854,11 @@ export function AutoRunsHistoryPanel({
|
|||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
log(`run id copied: ${value}`);
|
||||
log(`${label} copied: ${value}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setErrorText(`Копирование run id: ${message}`);
|
||||
log(`copy run id error: ${message}`);
|
||||
setErrorText(`Копирование ${label}: ${message}`);
|
||||
log(`copy ${label} error: ${message}`);
|
||||
}
|
||||
},
|
||||
[log]
|
||||
|
|
@ -844,6 +945,80 @@ export function AutoRunsHistoryPanel({
|
|||
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(
|
||||
(raw: string) => {
|
||||
const normalized = raw.trim();
|
||||
|
|
@ -1000,6 +1175,9 @@ export function AutoRunsHistoryPanel({
|
|||
setAutoGenBusy(true);
|
||||
setErrorText("");
|
||||
try {
|
||||
if (autoGenSettings.mode === "saved_user_sessions") {
|
||||
throw new Error("Пользовательские сессии сохраняются из живого чата, а не генерируются автоматически.");
|
||||
}
|
||||
const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? "";
|
||||
const promptFingerprint = [
|
||||
prompts.systemPrompt,
|
||||
|
|
@ -1283,16 +1461,19 @@ export function AutoRunsHistoryPanel({
|
|||
|
||||
const useMockForRun = filters.useMock === "true";
|
||||
const effectiveAnalysisDate = normalizeAnalysisDateInput(analysisDate);
|
||||
const useScenarioReplay = generation.mode === "saved_user_sessions";
|
||||
const payload = await apiClient.startEvalRunAsync({
|
||||
connection,
|
||||
prompts,
|
||||
promptVersion: assistantPromptVersion,
|
||||
mode: "single-pass-strict",
|
||||
caseSetFile: generation.saved_case_set_file ?? undefined,
|
||||
caseSetFile: useScenarioReplay ? undefined : generation.saved_case_set_file ?? undefined,
|
||||
useMock: useMockForRun,
|
||||
evalTarget: "assistant_stage1",
|
||||
questions: questionsForRun,
|
||||
analysisDate: effectiveAnalysisDate || undefined
|
||||
questions: useScenarioReplay ? undefined : questionsForRun,
|
||||
scenarioQuestions: useScenarioReplay ? questionsForRun : undefined,
|
||||
scenarioTitle: useScenarioReplay ? generation.title ?? undefined : undefined,
|
||||
analysisDate: useScenarioReplay ? undefined : effectiveAnalysisDate || undefined
|
||||
});
|
||||
|
||||
const liveJob = payload.job;
|
||||
|
|
@ -1307,7 +1488,11 @@ export function AutoRunsHistoryPanel({
|
|||
log(
|
||||
`Запущен 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}` : "") +
|
||||
(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);
|
||||
} catch (error) {
|
||||
|
|
@ -1506,6 +1691,97 @@ export function AutoRunsHistoryPanel({
|
|||
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) => {
|
||||
setAnnotations((prev) =>
|
||||
prev.map((item) =>
|
||||
|
|
@ -1598,11 +1874,11 @@ export function AutoRunsHistoryPanel({
|
|||
|
||||
useEffect(() => {
|
||||
setSelectedAutogenGenerationId((prev) => {
|
||||
if (autoGenHistory.length === 0) return "";
|
||||
if (prev && autoGenHistory.some((item) => item.generation_id === prev)) return prev;
|
||||
return autoGenHistory[0].generation_id;
|
||||
if (visibleAutoGenHistory.length === 0) return "";
|
||||
if (prev && visibleAutoGenHistory.some((item) => item.generation_id === prev)) return prev;
|
||||
return visibleAutoGenHistory[0].generation_id;
|
||||
});
|
||||
}, [autoGenHistory]);
|
||||
}, [visibleAutoGenHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAutogenGeneration) {
|
||||
|
|
@ -1610,7 +1886,7 @@ export function AutoRunsHistoryPanel({
|
|||
return;
|
||||
}
|
||||
setEditableGeneratedQuestions([...selectedAutogenGeneration.questions]);
|
||||
}, [selectedAutogenGeneration?.generation_id]);
|
||||
}, [selectedAutogenGeneration]);
|
||||
|
||||
useEffect(() => {
|
||||
setLimitInput(String(filters.limit));
|
||||
|
|
@ -1708,7 +1984,9 @@ export function AutoRunsHistoryPanel({
|
|||
return {
|
||||
...prev,
|
||||
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
|
||||
: prev.mode,
|
||||
count:
|
||||
|
|
@ -1949,140 +2227,155 @@ export function AutoRunsHistoryPanel({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Автогенерация вопросов</h4>
|
||||
<h4>Автопрогоны</h4>
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Режим генерации
|
||||
Режимы
|
||||
<select
|
||||
value={autoGenSettings.mode}
|
||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, mode: event.target.value as AutoGenMode }))}
|
||||
>
|
||||
<option value="codex_creative">codex_creative</option>
|
||||
<option value="qwen_seed">qwen_seed</option>
|
||||
<option value="saved_user_sessions">Пользовательские сессии</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Кол-во
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
value={autogenCountInput}
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value;
|
||||
if (raw === "" || /^\d+$/.test(raw)) {
|
||||
setAutogenCountInput(raw);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => commitAutogenCountInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
commitAutogenCountInput((event.target as HTMLInputElement).value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Личность автогенерации
|
||||
<select
|
||||
value={autoGenSettings.personalityId}
|
||||
onChange={(event) =>
|
||||
setAutoGenSettings((prev) => ({
|
||||
...prev,
|
||||
personalityId: event.target.value as AutoGenPersonalityId
|
||||
}))
|
||||
}
|
||||
>
|
||||
{autogenPersonalities.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Кто генерирует
|
||||
<input
|
||||
value={autoGenSettings.generatedBy}
|
||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, generatedBy: event.target.value }))}
|
||||
placeholder="manual_reviewer"
|
||||
/>
|
||||
</label>
|
||||
<label className="full-width">
|
||||
Промпт личности
|
||||
<textarea
|
||||
className="autoruns-personality-prompt"
|
||||
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
|
||||
onChange={(event) =>
|
||||
setAutoGenSettings((prev) => ({
|
||||
...prev,
|
||||
personalityPrompts: {
|
||||
...prev.personalityPrompts,
|
||||
[prev.personalityId]: event.target.value
|
||||
{!isSavedUserSessionsMode ? (
|
||||
<>
|
||||
<label>
|
||||
Кол-во
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
value={autogenCountInput}
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value;
|
||||
if (raw === "" || /^\d+$/.test(raw)) {
|
||||
setAutogenCountInput(raw);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => commitAutogenCountInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
commitAutogenCountInput((event.target as HTMLInputElement).value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Личность автогенерации
|
||||
<select
|
||||
value={autoGenSettings.personalityId}
|
||||
onChange={(event) =>
|
||||
setAutoGenSettings((prev) => ({
|
||||
...prev,
|
||||
personalityId: event.target.value as AutoGenPersonalityId
|
||||
}))
|
||||
}
|
||||
}))
|
||||
}
|
||||
placeholder="Текст промпта для выбранной личности автогенерации"
|
||||
style={{ height: `${autogenPersonalityPromptHeight}px` }}
|
||||
onMouseUp={captureAutogenPromptHeight}
|
||||
onTouchEnd={captureAutogenPromptHeight}
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoGenSettings.persistToEvalCases}
|
||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, persistToEvalCases: event.target.checked }))}
|
||||
/>
|
||||
Сохранять кейс-сет в `eval_cases`
|
||||
</label>
|
||||
>
|
||||
{autogenPersonalities.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Кто генерирует
|
||||
<input
|
||||
value={autoGenSettings.generatedBy}
|
||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, generatedBy: event.target.value }))}
|
||||
placeholder="manual_reviewer"
|
||||
/>
|
||||
</label>
|
||||
<label className="full-width">
|
||||
Промпт личности
|
||||
<textarea
|
||||
className="autoruns-personality-prompt"
|
||||
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
|
||||
onChange={(event) =>
|
||||
setAutoGenSettings((prev) => ({
|
||||
...prev,
|
||||
personalityPrompts: {
|
||||
...prev.personalityPrompts,
|
||||
[prev.personalityId]: event.target.value
|
||||
}
|
||||
}))
|
||||
}
|
||||
placeholder="Текст промпта для выбранной личности автогенерации"
|
||||
style={{ height: `${autogenPersonalityPromptHeight}px` }}
|
||||
onMouseUp={captureAutogenPromptHeight}
|
||||
onTouchEnd={captureAutogenPromptHeight}
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoGenSettings.persistToEvalCases}
|
||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, persistToEvalCases: event.target.checked }))}
|
||||
/>
|
||||
Сохранять кейс-сет в `eval_cases`
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Дата анализа (срез)
|
||||
<input
|
||||
type="date"
|
||||
value={analysisDate}
|
||||
onChange={(event) => setAnalysisDate(normalizeAnalysisDateInput(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div className="button-row">
|
||||
<button type="button" className="tab" disabled={!analysisDate} onClick={() => setAnalysisDate("")}>
|
||||
Сбросить дату среза
|
||||
</button>
|
||||
{!isSavedUserSessionsMode ? (
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Дата анализа (срез)
|
||||
<input
|
||||
type="date"
|
||||
value={analysisDate}
|
||||
onChange={(event) => setAnalysisDate(normalizeAnalysisDateInput(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div className="button-row">
|
||||
<button type="button" className="tab" disabled={!analysisDate} onClick={() => setAnalysisDate("")}>
|
||||
Сбросить дату среза
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="button-row">
|
||||
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
||||
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
||||
</button>
|
||||
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
||||
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
||||
</button>
|
||||
{!isSavedUserSessionsMode ? (
|
||||
<>
|
||||
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
||||
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
||||
</button>
|
||||
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
||||
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="autoruns-run-launch-btn"
|
||||
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0}
|
||||
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration}
|
||||
onClick={() => void runAutogenCampaign()}
|
||||
>
|
||||
{autogenRunBusy ? "Запускаю..." : "Запустить прогоны"}
|
||||
{autogenRunBusy ? "Запускаю..." : "Запустить прогон"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="autoruns-form-grid">
|
||||
<label className="full-width">
|
||||
Кейс-сет для запуска
|
||||
{isSavedUserSessionsMode ? "Сохраненная сессия" : "Кейс-сет для запуска"}
|
||||
<select
|
||||
value={selectedAutogenGenerationId}
|
||||
onChange={(event) => setSelectedAutogenGenerationId(event.target.value)}
|
||||
disabled={autoGenHistory.length === 0}
|
||||
disabled={visibleAutoGenHistory.length === 0}
|
||||
>
|
||||
{autoGenHistory.length === 0 ? <option value="">нет генераций</option> : null}
|
||||
{autoGenHistory.map((item) => (
|
||||
{visibleAutoGenHistory.length === 0 ? (
|
||||
<option value="">
|
||||
{isSavedUserSessionsMode ? "нет сохраненных сессий" : "нет генераций"}
|
||||
</option>
|
||||
) : null}
|
||||
{visibleAutoGenHistory.map((item) => (
|
||||
<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>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -2101,7 +2394,11 @@ export function AutoRunsHistoryPanel({
|
|||
</button>
|
||||
</div>
|
||||
{editableGeneratedQuestions.length === 0 ? (
|
||||
<p className="muted">Список вопросов пуст. Сгенерируйте пачку или восстановите из выбранной генерации.</p>
|
||||
<p className="muted">
|
||||
{isSavedUserSessionsMode
|
||||
? "Список вопросов пуст. Сначала сохраните живую пользовательскую сессию."
|
||||
: "Список вопросов пуст. Сгенерируйте пачку или восстановите из выбранной генерации."}
|
||||
</p>
|
||||
) : (
|
||||
<div className="autoruns-generated-questions-list">
|
||||
{editableGeneratedQuestions.map((question, index) => (
|
||||
|
|
@ -2110,41 +2407,93 @@ export function AutoRunsHistoryPanel({
|
|||
<button
|
||||
type="button"
|
||||
className="autoruns-remove-question-btn"
|
||||
onClick={() =>
|
||||
setEditableGeneratedQuestions((prev) => prev.filter((_, itemIndex) => itemIndex !== index))
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSavedUserSessionsMode) {
|
||||
requestDeleteSavedSessionQuestion(index);
|
||||
return;
|
||||
}
|
||||
setEditableGeneratedQuestions((prev) => prev.filter((_, itemIndex) => itemIndex !== index));
|
||||
}}
|
||||
title="Удалить вопрос из запуска"
|
||||
aria-label="Удалить вопрос из запуска"
|
||||
>
|
||||
+
|
||||
×
|
||||
</button>
|
||||
</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">
|
||||
{autogenHistoryBusy ? <p className="muted">Загружаю историю автогенераций...</p> : null}
|
||||
{!autogenHistoryBusy && autoGenHistory.length === 0 ? <p className="muted">История автогенераций пока пустая.</p> : null}
|
||||
{autoGenHistory.slice(0, 30).map((item) => (
|
||||
{autogenHistoryBusy ? (
|
||||
<p className="muted">
|
||||
{isSavedUserSessionsMode ? "Загружаю сохраненные пользовательские сессии..." : "Загружаю историю автогенераций..."}
|
||||
</p>
|
||||
) : null}
|
||||
{!autogenHistoryBusy && visibleAutoGenHistory.length === 0 ? (
|
||||
<p className="muted">
|
||||
{isSavedUserSessionsMode ? "Сохраненные пользовательские сессии пока пусты." : "История автогенераций пока пустая."}
|
||||
</p>
|
||||
) : null}
|
||||
{visibleAutoGenHistory.slice(0, 30).map((item) => (
|
||||
<article
|
||||
key={item.generation_id}
|
||||
className={selectedAutogenGenerationId === item.generation_id ? "autoruns-autogen-item selected" : "autoruns-autogen-item"}
|
||||
onClick={() => setSelectedAutogenGenerationId(item.generation_id)}
|
||||
>
|
||||
<header>
|
||||
<strong>{formatDateTime(item.created_at)}</strong>
|
||||
<span>{item.mode}</span>
|
||||
<strong>{item.title ?? formatDateTime(item.created_at)}</strong>
|
||||
<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>
|
||||
<div className="autoruns-run-meta">
|
||||
id={item.generation_id} | count={item.count}
|
||||
<div className="autoruns-run-meta autoruns-run-id-row">
|
||||
<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 className="autoruns-run-meta">
|
||||
домен={item.domain ?? "общий"}
|
||||
{item.generated_by ? ` | автор=${item.generated_by}` : ""}
|
||||
режим={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}` : ""}
|
||||
</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}
|
||||
</article>
|
||||
|
|
@ -2219,11 +2568,11 @@ export function AutoRunsHistoryPanel({
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
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) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
void copyRunIdToClipboard(event, run.run_id);
|
||||
void copyIdentifierToClipboard(event, run.run_id, "run id");
|
||||
}
|
||||
}}
|
||||
title="Скопировать run id"
|
||||
|
|
@ -2424,9 +2773,13 @@ export function AutoRunsHistoryPanel({
|
|||
onUseMockChange={setAssistantLiveUseMock}
|
||||
onSend={sendAssistantLiveMessage}
|
||||
onClear={resetAssistantLiveSession}
|
||||
onSaveSession={openAssistantLiveSaveModal}
|
||||
busy={assistantLiveBusy}
|
||||
saveBusy={assistantLiveSaveModal.saving}
|
||||
saveDisabled={!assistantLiveSessionId.trim() || assistantLiveConversation.length === 0 || assistantLiveBusy}
|
||||
statusText={assistantLiveStatus}
|
||||
errorMessage={assistantLiveError}
|
||||
showSaveAction
|
||||
showCommentAction
|
||||
onCommentAssistantMessage={openAssistantLiveCommentModal}
|
||||
isAssistantMessageCommented={isAssistantLiveMessageCommented}
|
||||
|
|
@ -2435,42 +2788,6 @@ export function AutoRunsHistoryPanel({
|
|||
</div>
|
||||
) : 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 ? (
|
||||
<section className="autoruns-col">
|
||||
<div className="autoruns-col-header">
|
||||
|
|
@ -2746,6 +3063,104 @@ export function AutoRunsHistoryPanel({
|
|||
) : null}
|
||||
</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 ? (
|
||||
<div
|
||||
className="autoruns-comment-modal-backdrop"
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ export interface RuntimeRun {
|
|||
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 AutoRunTrend = "up" | "down" | "flat";
|
||||
export type AutoGenMode = "qwen_seed" | "codex_creative";
|
||||
export type AutoGenMode = "qwen_seed" | "codex_creative" | "saved_user_sessions";
|
||||
export type ManualCaseDecision =
|
||||
| "covered_ok"
|
||||
| "covered_but_bad_answer"
|
||||
|
|
@ -317,6 +317,7 @@ export interface AutoGenHistoryRecord {
|
|||
generation_id: string;
|
||||
created_at: string;
|
||||
mode: AutoGenMode;
|
||||
title: string | null;
|
||||
count: number;
|
||||
domain: string | null;
|
||||
questions: string[];
|
||||
|
|
@ -330,6 +331,9 @@ export interface AutoGenHistoryRecord {
|
|||
prompt_fingerprint: string | null;
|
||||
autogen_personality_id: string | null;
|
||||
autogen_personality_prompt: string | null;
|
||||
source_session_id?: string | null;
|
||||
saved_session_file?: string | null;
|
||||
saved_case_set_kind?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1566,14 +1566,13 @@ button:disabled {
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: rotate(45deg);
|
||||
box-shadow: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.autoruns-remove-question-btn:hover {
|
||||
background: transparent;
|
||||
color: rgb(var(--rgb-active-text));
|
||||
color: rgb(var(--rgb-active));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
|
@ -1772,6 +1771,37 @@ button:disabled {
|
|||
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) {
|
||||
:root {
|
||||
--mode-column-width: 400px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue